【题目】
如何不用任何额外变量交换两个整数的值?
【难度】
士 ★☆☆☆
【解答】
如果给定整数a和b,用以下三行代码即可交换a和b的值。
a = a ^ b;
b = a ^ b;
a = a ^ b;
如何理解这三行代码的具体功能呢?首先要理解关于异或运算的特点:
● 假设a异或b的结果记为c,c就是a整数位信息和b整数位信息的所有不同信息。比如,a=4=100,b=3=011,a^b=c=000。
● a异或c的结果就是b。比如a=4=100,c=000,a^c=011=3=b。
● b异或c的结果就是a。比如b=3=011,c=000,b^c=100=4=a。
所以,在执行上面三行代码之前,假设有a信息和b信息。执行完第一行代码之后,a变成了c,b还是b;执行完第二行代码之后,a仍然是c,b变成了a;执行完第三行代码之后,a变成了b,b仍然是a。过程结束。
位运算的题目基本上都带有靠经验累积才会做的特征,也就是在准备阶段需要做足够多的题,面试时才会有良好的感觉。
【题目】
给定两个32位整数a和b,返回a和b中较大的。
【要求】
不用任何比较判断。
【难度】
校 ★★★☆
【解答】
第一种方法。得到a-b的值的符号,就可以知道是返回a还是返回b。具体请参看如下代码中的getMax1方法。
public int flip(int n) {
return n ^ 1;
}
public int sign(int n) {
return flip((n >> 31) & 1);
}
public int getMax1(int a, int b) {
int c = a - b;
int scA = sign(c);
int scB = flip(scA);
return a * scA + b * scB;
}
sign函数的功能是返回整数n 的符号,正数和0返回1,负数则返回0。flip函数的功能是如果n 为1,返回0,如果n 为0,返回1。所以,如果a-b的结果为0或正数,那么scA为1,scB为0;如果a-b的值为负数,那么scA为0,scB为1。scA和scB必有一个为1,另一个必为0。所以return a * scA + b * scB;,就是根据a-b的值的状况,选择要么返回a,要么返回b。
但方法一是有局限性的,那就是如果a-b的值出现溢出,返回结果就不正确。
第二种方法可以彻底解决溢出的问题,也就是如下代码中的getMax2方法。
public int getMax2(int a, int b) {
int c = a - b;
int sa = sign(a);
int sb = sign(b);
int sc = sign(c);
int difSab = sa ^ sb;
int sameSab = flip(difSab);
int returnA = difSab * sa + sameSab * sc;
int returnB = flip(returnA);
return a * returnA + b * returnB;
}
解释一下getMax2方法。
如果a的符号与b的符号不同(difSab==1,sameSab==0),则有:
● 如果a为0或正,那么b为负(sa==1,sb==0),应该返回a;
● 如果a为负,那么b为0或正(sa==0,sb==1),应该返回b。
如果a的符号与b的符号相同(difSab==0,sameSab==1),这种情况下,a-b的值绝对不会溢出:
● 如果a-b为0或正(sc==1),返回a;
● 如果a-b为负(sc==0),返回b;
综上所述,应该返回a * (difSab * sa + sameSab * sc) + b * flip(difSab * sa + sameSab * sc)。
【题目】
给定两个32位整数a和b,可正、可负、可0。不能使用算术运算符,分别实现a和b的加减乘除运算。
【要求】
如果给定的a和b执行加减乘除的某些结果本来就会导致数据的溢出,那么你实现的函数不必对那些结果负责。
【难度】
尉 ★★☆☆
【解答】
用位运算实现加法运算。如果在不考虑进位的情况下,a^b就是正确结果,因为0加0为0(0&0),0加1为1(0&1),1加0为1(1&0),1加1为0(1&1)。
例如:
a: 001010101
b: 000101111
无进位相加,即a^b:001111010
在只算进位的情况下,也就是只考虑a加b的过程中进位产生的值是什么,结果就是(a&b)<<1,因为在第i 位上只有1与1相加才会产生i -1位的进位。
例如:
a: 001010101
b: 000101111
只考虑进位的值,即(a&b)<<1:000001010
把完全不考虑进位的相加值与只考虑进位的产生值再相加,就是最终的结果。也就是说,一直重复这样的过程,直到进位产生的值完全消失,说明所有的过程都加完了。
例如:
a: 001010101
b: 000101111
———————————————————————
上边两值的^结果: 001111010
上边两值的&<<1结果:000001010
———————————————————————
上边两值的^结果: 001110000
上边两值的&<<1结果:000010100
———————————————————————
上边两值的^结果: 001100100
上边两值的&<<1结果:000100000
———————————————————————
上边两值的^结果: 001000100
上边两值的&<<1结果:001000000
———————————————————————
上边两值的^结果: 000000100
上边两值的&<<1结果:010000000
———————————————————————
上边两值的^结果: 010000100
上边两值的&<<1结果:000000000
———————————————————————
最后&<<1结果为0,则过程终止,返回010000100。具体请参看如下代码中的add方法。
public int add(int a, int b) {
int sum = a;
while (b ! = 0) {
sum = a ^ b;
b = (a & b) << 1;
a = sum;
}
return sum;
}
用位运算实现减法运算。实现a-b只要实现a+(-b)即可,根据二进制数在机器中表达的规则,得到一个数的相反数,就是这个数的二进制数表达取反加1(补码)的结果。具体请参看如下代码中的negNum方法。实现减法运算的全部过程请参看如下代码中的minus方法。
public int negNum(int n) {
return add(~n, 1);
}
public int minus(int a, int b) {
return add(a, negNum(b));
}
用位运算实现乘法运算。a*b的结果可以写成a*20 *b0+a*21 *b1+…+a*2i *bi+…+ a*231 *b31,其中,bi为0或1代表整数b的二进制数表达中第i 位的值。举一个例子,a=22= 000010110,b=13=000001101,res=0。
a: 000010110
b: 000001101
res:000000000
b的最左侧为1,所以res=res+a,同时b右移一位,a左移一位。
a: 000101100
b: 000000110
res:000010110
b的最左侧为0,所以res不变,同时b右移一位,a左移一位。
a: 001011000
b: 000000011
res:000010110
b的最左侧为1,所以res=res+a,同时b右移一位,a左移一位。
a: 010110000
b: 000000001
res:001101110
b的最左侧为1,所以res=res+a,同时b右移一位,a左移一位。
a: 101100000
b: 000000000
res:100011110
此时b为0,过程停止,返回res= 100011110,即286。
不管a和b是正、负,还是0,以上过程都是对的,因为都满足a*b=a*20 *b0+a*21 *b1+…+a*2i *bi+…+a*231 *b31。具体请参看如下代码中的multi方法。
public int multi(int a, int b) {
int res = 0;
while (b ! = 0) {
if ((b & 1) ! = 0) {
res = add(res, a);
}
a <<= 1;
b >>>= 1;
}
return res;
}
用位运算实现除法运算,其实就是乘法的逆运算。先举例说明一种最普通的情况,a和b都不为负数,假设a=286= 100011110,b=22= 000010110,res=0:
a: 100011110
b: 000010110
res:000000000
b向右位移31位、30位、……、4位时,得到的结果都大于a。而当b向右位移3位的结果为010110000,此时a>=b。根据乘法的范式,如果b*res=a,则a=b*20 *res0+b*21 *res1+…+b*2i *resi+…+b*231 *res31。因为b在向右位移31位、30位、……、4位时,得到的结果都比a大,说明a包含不下b*231 ~b*24 的任何一个,所以res4~res31这些位置上应该都为0。而b在向右位移3位时,a>=b,说明a可以包含一个b*23 ,即res3=1。接下来看剩下的a,即a-b*23 ,还能包含什么。
a: 001101110
b: 000010110
res:000001000
b向右位移2位之后为001011000,此时a>=b,说明剩下的a可以包含一个b*22 ,即res2=1,然后让剩下的a减掉一个b*22 ,看还能包含什么。
a: 000010110
b: 000010110
res:000001100
b向右位移1位之后大于a,说明剩下的a不能包含b*21 。b向右位移0位之后a==b,说明剩下的a还能包含一个b*20 ,即res0=1。当剩下的a再减去一个b之后,结果为0,说明a已经完全被分解干净,结果就是此时的res,即000001101=13。
以上过程其实就是先找到a能包含的最大部分,然后让a减去这个最大部分,再让剩下的a找到次大部分,并依次找下去。
以上过程只适用于当a和b都不是负数的时候,所以,如果a和b中有一个为负数或者都为负数时,可以先把a和b转成正数,计算完成后再看res的真实符号是什么就可以。
具体请参看如下代码中的div方法,sign方法是判断整数n 是否为负,负数返回true,否则返回false。
public boolean isNeg(int n) {
return n < 0;
}
public int div(int a, int b) {
int x = isNeg(a) ? negNum(a) : a;
int y = isNeg(b) ? negNum(b) : b;
int res = 0;
for (int i = 31; i > -1; i = minus(i, 1)) {
if ((x >> i) >= y) {
res |= (1 << i);
x = minus(x, y << i);
}
}
return isNeg(a) ^ isNeg(b) ? negNum(res) : res;
}
除法实现还剩非常关键的最后一步。以上方法可以算绝大多数的情况,但我们知道32位整数的最小值为-2147483648,最大值为2147483647,最小值的绝对值比最大值的绝对值大1,所以,如果a或b等于最小值,是转不成相对应的正数的。可以总结一下:
● 如果a和b都不为最小值,直接使用以上过程,返回div(a,b)。
● 如果a和b都为最小值,a/b的结果为1,直接返回1。
● 如果a不为最小值,而b为最小值,a/b的结果为0,直接返回0。
● 如果a为最小值,而b不为最小值,怎么办?
第1~3情况处理都比较容易,对于情况4就棘手很多。我们举个简单的例子说明本书是如何处理这种情况的。为了方便说明,我们假设整数的最大值为9,而最小值为-10。当a和b属于[0,9]的范围时,我们可以正确地计算a/b。当a和b都属于[-9,9]时,我们可以计算,也就是情况1;当a和b都等于-10时,我们也可以计算,就是情况2;当a属于[-9,9],而b等于-10时,我们也能计算,就是情况3;当a等于-10,而b属于[-9,9]时,如何计算呢?
1.假设a=-10,b=5。
2.计算(a+1)/b的结果,记为c。对本例来讲就是-9/5的结果,c=-1。
3.计算c*b的结果。对本例来讲,-1*5=-5。
4.计算a-(c*b),即-10-(-5)=-5。
5.计算(a-(c*b))/b的结果,记为rest,意义是修正值,即-5/5=-1。
6.返回c+rest的结果。
也就是说,既然我们对最小值无能为力,那么就把最小值增加一点,计算出一个结果,然后根据这个结果再修正一下,得到最终的结果。
除法运算的全部过程请参看如下代码中的divide方法。
public int divide(int a, int b) {
if (b == 0) {
throw new RuntimeException("divisor is 0");
}
if (a == Integer.MIN_VALUE && b == Integer.MIN_VALUE) {
return 1;
} else if (b == Integer.MIN_VALUE) {
return 0;
} else if (a == Integer.MIN_VALUE) {
int res = div(add(a, 1), b);
return add(res, div(minus(a, multi(res, b)), b));
} else {
return div(a, b);
}
}
【题目】
给定一个32位整数n ,可为0,可为正,也可为负,返回该整数二进制表达中1的个数。
【难度】
尉 ★★☆☆
【解答】
最简单的解法。整数n 每次进行无符号右移一位,检查最右边的bit是否为1来进行统计。具体请参看如下代码中的count1方法。
public int count1(int n) {
int res = 0;
while (n ! = 0) {
res += n & 1;
n >>>= 1;
}
return res;
}
如上方法在最复杂的情况下要经过32次循环,下面看一个循环次数只与1的个数有关的解法,如下代码中的count2方法。
public int count2(int n) {
int res = 0;
while (n ! = 0) {
n &= (n - 1);
res++;
}
return res;
}
每次进行n&=(n-1)操作,接下来在while循环中就可以忽略掉bit位上为0的部分。
例如,n=01000100,n-1=01000011,n&(n-1)=01000000,说明处理到01000100之后,下一步还得处理,因为01000000! =0。n=01000000,n-1=00111111,n&(n-1)=00000000,说明处理到01000000之后,下一步就不用处理,因为接下来没有1。所以,n&=(n-1)操作的实质是抹掉最右边的1。
与count2方法复杂度一样的是如下代码中的count3方法。
public int count3(int n) {
int res = 0;
while (n ! = 0) {
n -= n & (~n + 1);
res++;
}
return res;
}
每次进行n-=n&(~n+1)操作时,这也是移除最右侧的1的过程。等号右边n & (~n + 1)的含义是得到n中最右侧的1,这个操作在位运算的题目中经常出现。例如,n=01000100,n&(~n+1)=00000100,n-(n&(~n+1))=01000000。n=01000000,n&(~n+1)=01000000,n-(n&(~n+1)) = 00000000。接下来不用处理了,因为没有1。
接下来介绍一种看上去很“超自然”的方法,叫作平行算法,参看如下代码中的count4方法。
public int count4(int n) {
n = (n & 0x55555555) + ((n >>> 1) & 0x55555555);
n = (n & 0x33333333) + ((n >>> 2) & 0x33333333);
n = (n & 0x0f0f0f0f) + ((n >>> 4) & 0x0f0f0f0f);
n = (n & 0x00ff00ff) + ((n >>> 8) & 0x00ff00ff);
n = (n & 0x0000ffff) + ((n >>> 16) & 0x0000ffff);
return n;
}
下面解释一下这个过程。
0x55555555即01010101010101010101010101010101。(n & 0x55555555) + ((n>>>1) &0x55555555)的结果描述了每两个bit成一组1的数量分布。以n=-1(11111111111111 11111111111111)为例进行说明,n=(n & 0x55555555) + ((n >>> 1) & 0x55555555)为10101010101010101010101010101010,可以看到每两个bit成一组1的数量状况为10,也就是每组2个。
接下来,0x33333333即00110011001100110011001100110011,所以(n & 0x33333333) +((n >>> 1) & 0x33333333)就描述了4个bit成一组1的数量分布。此时n=(n & 0x33333333) +((n >>> 1) & 0x33333333)为01000100010001000100010001000100,它就代表4个bit位成一组的1数量状况为0100,也就是每组4个。
接下来n依次为00001000000010000000100000001000,代表8个bit位成一组1的数量状况为00001000,也就是每组8个。00000000000100000000000000010000代表16个bit成一组1的数量状况为0000000000010000,也就是每组16个。00000000000000000000000000100000代表32个bit成一组1的数量状况为00000000000000000000000000100000,也就是每组32个。
类似并归的过程,组与组之间的数量合并成一个大组,进行下一步的并归。
除此之外,还有很多极为逆天的算法可以解决这个问题,比如MIT hackmem算法等。有兴趣的读者可以去网上查找,但对面试来说,那些方法实在太偏、难、怪,所以本书不再介绍。
【题目】
给定一个整型数组arr,其中只有一个数出现了奇数次,其他的数都出现了偶数次,打印这个数。
【进阶】
有两个数出现了奇数次,其他的数都出现了偶数次,打印这两个数。
【要求】
时间复杂度为O (N ),额外空间复杂度为O (1)。
【难度】
尉 ★★☆☆
【解答】
整数n 与0异或的结果是n ,整数n 与整数n 异或的结果是0。所以,先申请一个整型变量,记为eO。在遍历数组的过程中,把eO和每个数异或(eO=eO^当前数),最后eO的值就是出现了奇数次的那个数。这是什么原因呢?因为异或运算满足交换律与结合律。为了方便说明,我们假设A,B,C这三个数出现了偶数次,D这个数出现了奇数次,并且出现的顺序为:C,B,D,A,A,B,C。因为异或运算满足交换律和结合律,所以任意调整异或的顺序也不会改变最终eO的值,那么按照原始顺序异或得到的eO结果与按照如下顺序异或出的eO结果是相同的:A,A,B,B,C,C,D。而按照这个顺序的异或最终结果就是D。也就是说,先异或还是后异或某一个数,对最终的结果是没有任何影响的,最终结果等同于连续异或同一个出现偶数次的数之后,再连续异或下一个出现偶数次的数,等到所有出现偶数次的数异或完,异或结果肯定是0,最后再去异或出现奇数次的数,最终结果自然是出现奇数次的树。所以对任何排列的数组,只要这个数组有一个数出现了奇数次,另外的数出现了偶数次,最终异或结果都是出现了奇数次的数。请参看printOddTimesNum1方法。
public void printOddTimesNum1(int[] arr) {
int eO = 0;
for (int cur : arr) {
eO ^= cur;
}
System.out.println(eO);
}
如果只有a和b出现了奇数次,那么最后的异或结果eO就是a^b。所以,如果数组中有两个出现了奇数次的数,最终的eO一定不等于0。那么肯定能在32位整数eO上找到一个不等于0的bit位,假设是第k 位不等于0。eO在第k 位不等于0,说明a和b的第k 位肯定一个是1另一个是0。接下来再设置一个变量记为eOhasOne,然后再遍历一次数组。在这次遍历时,eOhasOne只与第k 位上是1的整数异或,其他的数忽略。那么在第二次遍历结束后,eOhasOne就是a或者b中的一个,而eO^eOhasOne就是另外一个出现奇数次的数。请参看printOddTimesNum2方法。
public static void printOddTimesNum2(int[] arr) {
int eO = 0, eOhasOne = 0;
for (int curNum : arr) {
eO ^= curNum;
}
int rightOne = eO & (~eO + 1);
for (int cur : arr) {
if ((cur & rightOne) ! = 0) {
eOhasOne ^= cur;
}
}
System.out.println(eOhasOne + " " + (eO ^ eOhasOne));
}
【题目】
给定一个整型数组arr和一个大于1的整数k 。已知arr中只有1个数出现了1次,其他的数都出现了k 次,请返回只出现了1次的数。
【要求】
时间复杂度为O (N ),额外空间复杂度为O (1)。
【难度】
尉 ★★☆☆
【解答】
以下的例子是两个七进制数的无进位相加,即忽略进位的相加,比如:
七进制数a: 6 4 3 2 6 0 1
七进制数b: 3 4 5 0 1 1 1
无进位相加结果: 2 1 1 2 0 1 2
可以看出,两个七进制的数a和b,在i 位上无进位相加的结果就是(a(i)+b(i))%7。同理,k 进制的两个数c和d,在i 位上无进位相加的结果就是(c(i)+d(i))%k。那么,如果k 个相同的k 进制数进行无进位相加,相加的结果一定是每一位上都是0的k 进制数。
理解了上述过程之后,解这道题就变得简单了,首先设置一个变量eO,它是一个32位的k 进制数,且每个位置上都是0。然后遍历arr,把遍历到的每一个整数都转换为k 进制数,然后与eO进行无进位相加。遍历结束时,把32位的k 进制数eORes转换为十进制整数,就是我们想要的结果。因为k 个相同的k 进制数无进位相加,结果一定是每一位上都是0的k 进制数,所以只出现一次的那个数最终就会剩下来。具体请参看如下代码中的onceNum方法。
public int onceNum(int[] arr, int k) {
int[] eO = new int[32];
for (int i = 0; i ! = arr.length; i++) {
setExclusiveOr(eO, arr[i], k);
}
int res = getNumFromKSysNum(eO, k);
return res;
}
public void setExclusiveOr(int[] eO, int value, int k) {
int[] curKSysNum = getKSysNumFromNum(value, k);
for (int i = 0; i ! = eO.length; i++) {
eO[i] = (eO[i] + curKSysNum[i]) % k;
}
}
public int[] getKSysNumFromNum(int value, int k) {
int[] res = new int[32];
int index = 0;
while (value ! = 0) {
res[index++] = value % k;
value = value / k;
}
return res;
}
public int getNumFromKSysNum(int[] eO, int k) {
int res = 0;
for (int i = eO.length - 1; i ! = -1; i--) {
res = res * k + eO[i];
}
return res;
}