位运算的刷题应用

不申请临时变量的整数交换

private static void swap(int a, int b) {
    a = a ^ b;
    b = a ^ b;
    a = a ^ b;
}
复制代码

关于这样作行之有效的缘由

先来解释b 为何会变成 a。算法

首先,异或运算是符合交换律和结合律的,就是说 a ^ b ^ c 等于 a ^ c ^ b 等于 (a ^ b) ^ c。编程

因此,第二条语句就能够等价为 : b = a ^ b ^ b; ,由于(b ^ b) = 0 ,因此 b = a ^ 0 = a。数组

再来解释 a 为何会变成 b。ui

结合以上说法,当运行到 第三行代码时,a = a ^ b 而 b = a,因此 a = a ^ b = a ^ b ^ a。spa

同上,结合异或运算的交换律, a = (a ^ a) ^ b = b。code

其实,只要是互逆运算就能够进行这种不用临时变量的交换运算,如 加减法,乘除法。教程

public static void swap(int a,int b) {
    a = a + b;
    b = a - b;
    a = a - b;
}
复制代码
public static void swap(int a,int b) {
    a = a * b;
    b = a / b;
    a = a / b;
}
复制代码

不使用 + 来计算整数的加法(写加法器)

public static int plus(int a, int b) {
    while (b != 0) 
    {
        int _a = a ^ b;
        int _b = (a & b) << 1;
        a = _a;
        b = _b;
    }
    return a;
}
复制代码

关于这样作行之有效的缘由

主要利用异或运算来完成,异或运算有一个别名叫作:不进位加法。ip

那么a ^ b就是a和b相加以后,该进位的地方不进位的结果,相信这一点你们没有疑问,可是须要注意的是,这个加法是在二进制下完成的加法。get

而后下面考虑哪些地方要进位?it

什么地方须要进位呢? 天然是a和b里都是1的地方

a & b就是a和b里都是1的那些位置,那么这些位置左边都应该有一个进位1,a & b << 1 就是进位的数值(a & b的结果全部左移一位)。

那么咱们把不进位的结果和进位的结果加起来,就是实际中a + b的和。

a + b = (a ^ b) + (a & b << 1)

令:

a' = a ^ b, b' = (a & b) << 1 => a + b = (a ^ b) + (a & b << 1) = a' + b'

而后反复迭代,这个过程是在二进制下模拟加法的运算过程,进位不可能一直持续,因此b最终会变为0,也就是没有须要进位的了,所以重复作上述操做就能够 最终求得a + b的值。

用 O(1) 时间检测整数 n 是不是 2 的幂次

N若是是2的幂次,则N知足两个条件。

N > 0

N的二进制表示中只有一个1, 注意只能有1个。

由于N的二进制表示中只有一个1,因此使用N & (N - 1)将N惟一的一个1消去,应该返回0。

bool checkPowerOf2(int n) {
    return n > 0 && (n & (n - 1)) == 0;
}
复制代码

x & (x - 1)的妙用

x & (x - 1)消去x最后一位的1。

计算在一个 32 位的整数的二进制表式中有多少个 1

由x & (x - 1)消去x最后一位的1可知。不断使用 x & (x - 1) 消去x最后一位的1,计算总共消去了多少次便可。

public int countOnes(int num) {
    int count = 0;
    while (num != 0) // 不能是 > 0,由于要考虑负数
    {
        num = num & (num - 1);
        count++;
    }
    return count;
}
复制代码

若是要将整数A转换为B,须要改变多少个bit位?

这个应用是上面一个应用的拓展

思考将整数A转换为B,若是A和B在第i(0 <=i < 32)个位上相等,则不须要改变这个BIT位,若是在第i位上不相等,则须要改变这个BIT位。

因此问题转化为了A和B有多少个BIT位不相同!

联想到位运算有一个异或操做,相同为0,相异为1,因此问题转变成了计算A异或B以后这个数中1的个数!

public int countOnes(int num) {
    int count = 0;
    while (num != 0) // 不能是 > 0,由于要考虑负数
    {
        num = num & (num - 1);
        count++;
    }
    return count;
}

public int bitSwapRequired(int a, int b) {
    return countOnes(a ^ b);
}
复制代码

应用:给定一个含不一样整数的集合,返回其全部的子集

思路就是使用一个正整数二进制表示的第i位是1仍是0来表明集合的第i个数取或者不取。 因此从0到2^n-1总共2^n个整数,正好对应集合的2^n个子集。以下是就是 整数 <=> 二进制 <=> 对应集合 之间的转换关系。

public List<List<Integer>> bitSubsets(int[] nums) 
{
    Arrays.sort(nums);
    List<List<Integer>> list = new ArrayList<>();
    int n = nums.length;
    for (int i = 0; i < (1 << n); ++i) {
        List<Integer> subset = new ArrayList<>();
        for (int j = 0; j < n; j++) {
            if((1 << j & i) != 0) {
                subset.add(nums[j]);
            }
        }
        list.add(subset);
    }
    return list;
}
复制代码

巧用异或运算

应用一:数组中,只有一个数出现一次,剩下都出现两次,找出出现一次的数

由于只有一个数刚好出现一个,剩下的都出现过两次,因此只要将全部的数异或起来,就能够获得惟一的那个数,由于相同的数出现的两次,异或两次等价于没有任何操做!

public int singleNumber(int[] nums) {
    int result = 0, n = nums.length;
    for (int i = 0; i < n; i++)
    {
        result ^= nums[i];
    }
    return result;
}
复制代码

应用二:数组中,只有一个数出现一次,剩下都出现三次,找出出现一次的数

由于其余数是出现三次的,也就是说,对于每个二进制位,若是只出现一次的数在该二进制位为1,那么这个二进制位在所有数字中出现次数没法被3整除。

对于每一位,咱们让Two,One表示当前位的状态。

咱们看Two和One里面的每一位的定义,对于ith(表示第i位):

若是Two里面ith是1,则ith当前为止出现1的次数模3的结果是2

若是One里面ith是1,则ith目前为止出现1的次数模3的结果是1

注意Two和One里面不可能ith同时为1,由于这样就是3次,每出现3次咱们就能够抹去(消去)。那么最后One里面存储的就是每一位模3是1的那些位,综合起来One也就是最后咱们要的结果。

若是B表示输入数字的对应位,Two+和One+表示更新后的状态

那么新来的一个数B,此时跟原来出现1次的位作一个异或运算,&上~Two的结果(也就是否是出现2次的),那么剩余的就是当前状态是1的结果。

同理Two ^ B (2次加1次是3次,也就是Two里面ith是1,B里面ith也是1,那么ith应该是出现了3次,此时就能够消去,设置为0),咱们至关于会消去出现3次的位。

可是Two ^ B也多是ith上Two是0,B的ith是1,这样Two里面就混入了模3是1的那些位!!!怎么办?咱们得消去这些!咱们只须要保留不是出现模3余1的那些位ith,而One是刚好保留了那些模3余1次数的位,`取反不就是否是模3余1的那些位ith么?最终对(~One+)取一个&便可。

public int singleNumber(int[] nums) {
    int ones = 0, twos = 0;
    for(int i = 0; i < nums.length; i++)
    {
        ones = (ones ^ nums[i]) & ~twos;
        twos = (twos ^ nums[i]) & ~ones;
    }
    return ones;
}
复制代码

应用三:数组中,只有两个数出现一次,剩下都出现两次,找出出现一次的这两个数

有了第一题的基本的思路,咱们能够将数组分红两个部分,每一个部分里只有一个元素出现一次,其他元素都出现两次。那么使用这种方法就能够找出这两个元素了。不妨假设出现一个的两个元素是x,y,那么最终全部的元素异或的结果就是等价于++res = x^y++。

++而且res!=0++

为何呢? 若是res 等于0,则说明x和y相等了!!!!

由于res不等于0,那么咱们能够必定能够找出res二进制表示中的某一位是1。

对于x和y,必定是其中一个这一位是1,另外一个这一位不是1!!!细细琢磨, 由于若是都是0或者都是1,怎么可能异或出1

对于原来的数组,咱们能够根据这个位置是否是1就能够将数组分红两个部分。++x,y必定在不一样的两个子集中。++

并且对于其余成对出现的元素,要么都在x所在的那个集合,要么在y所在的那个集合。对于这两个集合咱们分别求出单个出现的x 和 单个出现的y便可。

public int[] singleNumber(int[] nums) {
    //用于记录,区分“两个”数组
    int diff = 0;
    for(int i = 0; i < nums.length; i ++) 
    {
        diff ^= nums[i];
    }
    //取最后一位1
    //先介绍一下原码,反码和补码
    //原码,就是其二进制表示(注意,有一位符号位)
    //反码,正数的反码就是原码,负数的反码是符号位不变,其他位取反
    //补码,正数的补码就是原码,负数的补码是反码+1
    //在机器中都是采用补码形式存
    //diff & (-diff)就是取diff的最后一位1的位置
    diff &= -diff;
    
    int[] rets = {0, 0}; 
    for(int i = 0; i < nums.length; i ++) 
    {
        //分属两个“不一样”的数组
        if ((nums[i] & diff) == 0) 
        {
            rets[0] ^= nums[i];
        }
        else 
        {
            rets[1] ^= nums[i];
        }
    }
    return rets;
}
复制代码

最后

位运算的妙用到这就告一段落了,以上内容有一大部分是来自 九章算法位运算入门教程 , 在 LintCode 上还有相应的编程练习,去练习一下会得到更好的效果。

相关文章
相关标签/搜索