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的符号不一样,那就是发生溢出了。