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

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。

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

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

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

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;
}

关于这样做行之有效的原因

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

那么a ^ b就是a和b相加之后,该进位的地方不进位的结果,相信这一点大家没有疑问,但是需要注意的是,这个加法是在二进制下完成的加法。

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

什么地方需要进位呢? 自然是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 上还有相应的编程练习,去练习一下会获得更好的效果。