7
位运算

不用额外变量交换两个整数的值

【题目】

如何不用任何额外变量交换两个整数的值?

【难度】

士 ★☆☆☆

【解答】

如果给定整数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);
                  }
          }

整数的二进制表达中有多少个1

【题目】

给定一个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));
          }

在其他数都出现k 次的数组中找到只出现一次的数

【题目】

给定一个整型数组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;
          }