随笔-位运算的一些小技巧

351 阅读5分钟

1.只出现一次的数字

这个小技巧来源于leetcode第136题。在一个数组中,其他数字都出现了2次,只有一个数字出现了一次,如何找出它。

代码如下:

    public int singleNumber(int[] nums) {
        int single = 0;
        for (int num : nums) {
            single ^= num;
        }
        return single;
    }

这题的思路在于位运算中的异或操作。在异或操作中:

1^1=0,
1^0=1,
0^1=1,
0^0=0

从异或操作的结果不难得出结论,同一位上的数字,出现偶数次的话,异或出的结果就是0,出现奇数次的话,异或出的结果就是1。

a^a^b => (a^a)^b => 0^b => b

所以上题可以用异或的方法解决。

2.只出现一次的数字II

这个小技巧还是来自于leetcode,原题为137题。相比上一道题目,该题中,其他数字都出现了3次,只有一个数字出现1次,如何找出它?

经过上一题之后,我们知道,使用异或计算可以得到出现奇数次的数字。那么同样的出现奇数次,怎么区分1次和3次就成了解题关键。代码如下:

    public int singleNumber(int[] nums) {
        int seenOnce = 0, seenTwice = 0;
        for (int num : nums) {
            seenOnce = ~seenTwice & (seenOnce ^ num);
            seenTwice = ~seenOnce & (seenTwice ^ num);
        }
        return seenOnce;
    }

在这种情况下我们可以使用2个掩码,分别记录出现1次和2次的数字。 计算过程如上图。当一个数字只出现一次的时候,第一位掩码会记录下来。出现第二次的时候,第一位掩码消失了,第二位掩码则会记录。而出现3次的时候,两个掩码都消失了。这样就可以区分出1次和3次了。

3.统计int二进制中出现1的个数

直接统计的话我们需要进行32次的位运算加上32次判断加上0-32次加法运算才能统计出二进制中出现了多少个1.那么有没有办法使用更少的运算统计呢?答案就在JDK的源代码中。

    public static int bitCount(int i) {
        i = i - ((i >>> 1) & 0x55555555);
        i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);
        i = (i + (i >>> 4)) & 0x0f0f0f0f;
        i = i + (i >>> 8);
        i = i + (i >>> 16);
        return i & 0x3f;
    }

上述代码取自Integer的bitCount方法。使用上述方法只需要进行15次运算,就可以统计出结果了。

咋一看代码是不是有点云里雾里。莫慌,待我细细盘点。要想统计32位二进制中有多少1,我们不妨先思考下,先不论多少位,有没有办法通过一次计算就得出二进制中1的数量?答案是有的。我们可以先看下2位二进制如何计算1的数量:

    public int bitCount(int i) {
        return i - ((i >> 1) & 1);
    }

上述方法大家可以试一下,当i处于0-3 (也就是二进制00,01,10,11) 的范围内时,通过上述运算可以直接计算出i的二进制中1的数量。那么这样一来,第一步就可以理解了。

i = i - ((i >>> 1) & 0x55555555);

这行代码,只不过吧我们上述2位二进制的计算,给平摊至32位二进制计算中。也就是会得出16组结果。

输入 i = 11 01 
 	i>>>1 = 01 10
    (i>>>1) & (01 01) = 01 00
    i = 01 00 = 10 01
    结果就是前两位有2个1 后两位只有1个1.

第一步完成后,后面的就简单啦,只需要吧分摊在16组中的结果加起来就行。举个例子:

输入 i = 10 01
    i&0011 = 00 01
    i>>>2 = 00 10
    00 01 + 00 10 = 00 11
    通过这样的操作,可以把每两位的结果合并,一次合并后就是每4位的结果。

那么从2位至32位需要经过几次合并呢?就是 2->4->8->16->32 就可以得到最终结果了。

4.判断int计算后是否溢出

在数字的计算中,经常需要判断两个数字相加或相减后有无溢出。如果不判断的话可能会有bug。那么这个判断过程,我们也是可以通过位运算来进行的。

相加的判断

    public static int addExact(int x, int y) {
        int r = x + y;
        if (((x ^ r) & (y ^ r)) < 0) {
            throw new ArithmeticException("integer overflow");
        }
        return r;
    }

代码节选自JDK的源代码Math.addExact;我们知道两个不同符号的数,进行相加操作是不会溢出的。只有相同符号的数相加才会溢出。所以我们可以分开讨论,两个正数和两个负数分别相加的情况。

首先,两个正数相加结果不会为负数,但是结果溢出的话就会得到一个负数。所以我们可以基于此判断。上述代码中r为相加的结果。如果r溢出了,r的最左边二进制位必为1.而x和y这两是正数,他们的最左边二进制位必为0.所以当x和y都大于0的时候,((x ^ r) & (y ^ r))的结果小于0的话,可以肯定是溢出了。

负数的话也是同样的道理,两个负数相加,得到一个正数的话,那肯定也是不正常的。

至于一正一负,((x ^ r) & (y ^ r))该表达式的结果也必然大于0.

相减的判断

    public static int subtractExact(int x, int y) {
        int r = x - y;
        if (((x ^ y) & (x ^ r)) < 0) {
            throw new ArithmeticException("integer overflow");
        }
        return r;
    }

这个判断原理和相加是一样的。两个相同符号数相减操作不会溢出。所以(x ^ y)可以判断是否是相同符号。如果是相同符号,那么必然不会溢出,如果是不同符号,在对比结果和x的符号。如果结果和x的符号不一样,那就是发生溢出了。