OptimalSolution(8)--位运算
一、不用额外变量交换两个整数的值
如果给定整数a和b,用以下三行代码即可交换a和b的值。a = a ^ b; b = a ^ b; a = a ^ b;
a = a ^ b :假设a异或b的结果记为c,c就是a整数位信息和b整数位信息的所有不同信息。例如,a=4=100,b=3=011,a^b=c=000
b = a ^ b :a异或c的结果就是b。比如a=4=100,c=000,a^c=011=3=b,也就是b = a ^ b ^ b = a
a = a ^ b :b异或c的结果就是a。比如b=3=011,c=000,b^c=100=4=a,也就是a = a ^ b ^ a =
二、不用任何比较判断找出两个数中较大的数
问题:给定两个32位整数a和b,返回a和b中较大的。
1.得到a-b的值的符号,如果a-b的值出现溢出,返回结果就不正确
sign函数返回整数n的符号,整数和0返回1,负数返回0。如果a-b的结果为0或整数,那么scA=1,scB=0,return a ;如果a-b的值为负数,那么scA=0,scB=1,return b;
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; }
2.彻底解决溢出的问题
情况1:如果a和b的符号不同(disSab == 1,sameSab==0),则有
如果a为0或正,b为负(sa == 1,sb == 0),那么returnA与sc无关,为sa==1,returnB=0,返回a
如果a为负,b为0或正(sa==0,sb==1),那么returnA==0,returnB=1,返回b
情况2:如果a和b的符号相同(difSab==0,sameSab=1),那么此时a-b的值绝对不会溢出:
如果a-b为0或正(sc==1),那么returnA=sc=1,returnB=0,返回a
如果a-b为负(sc==0),那么returnA=0,returnB=1,返回b
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 = fiip(returnA); return a * returnA + b * return B; }
三、整数的二进制表达式中有多少个1
问题:给定一个32位整数n,可为0,可为正,可为负,返回该整数二进制表达式中1的个数
1.整数n每次进行无符号右移(>>>)一位,检查最右边的bit是否为1。需要经过32次循环
public int count1(int n){ int res = 0; while(n!=0){ res += n & 1; n >>> = 1; } }
2.循环次数只和1的个数有关的解法。每进行一次n &= (n-1);操作,接下来在while循环中就可以忽略掉bit位上为0的部分。
例如,n=01000100,n-1=01000011,n&(n-1)=01000000,res=1,然后,n=01000000,n-1=00111111,n&(n-1)=00000000,res=2,结束。
因此,n&(n-1)操作实际上是抹掉n最右边的那一个1。
public int count2(int n){ int res = 0; while(n != 0){ n &= (n-1); res++; } return res; }
3.同方法2,只不过是将n&(n-1)操作改成n -= n & (~n+1),也是移除最右侧的1的过程。n & (~n+1)是得到n中最右侧的1
例如:n=01000100,~n=10111011,~n+1=10111100,n & (~n+1) = 00000100,n - n & (~n+1) = 01000100,同理。
四、在其他数都出现偶数次的数组中找到出现奇数次的数
问题一:只有一个数出现了奇数次,其他的数都出现了偶数次
public void printOddTmesNum1(int[] arr){ int eO = 0; for(int cur : arr){ eO ^= cur; } System.out.println(eO); }
问题二:有两个数出现了奇数次,其他的数出现了偶数次
主要关注:int rightOne = eO & (~eO + 1);这个操作是得到eO最右边的1表示的数,例如01000100经过操作后变成00000100
public 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 & right) != 0){ eOhasOne ^= cur; } } System.out.println(eOhasOne + " " + (eO ^ eOhasOne)); }
五、在其他数都出现k次的数组中找到只出现一次的数
问题:给定一个整型数组arr和一个大于1的整数k,已知arr中只有1个数出现了1次,其他的数都出现了k次,返回只出现1次的数
两个七进制的数,忽略进位相加: a : 6 4 3 2 6 0 1 b : 3 4 5 0 1 1 1 c : 2 1 1 2 0 1 2
思路:上面的计算中,第i位上无进位相加的结果就是c[i] = (a[i] + b[i])%7。同理,k进制的两个数a和b,在第i位上相加的结果就是c[i] = (a[i] + b[i])%k。那么,如果k个相同的k进制数进行无进位相加,根据c[i] = (k * a[i] + k * b[i])%k,可知,相加的结果一定是每一位上都是0的k进制数。
解法:设置一个变量eO,它是一个32位的k进制数,且每个位置上都是0。然后遍历arr,把遍历到的每一个整数都转换为k进制数,然后与e0进行无进位相加。遍历结束后,把32位的k进制数eORes转换成十进制就是要求的结果。
函数1:将十进制的数转换成32位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; }
函数2:将表示k进制的数组转换成十进制的数
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; }
函数3:将十进制的value转换成curKSysNum数组表示的32位k进制数后无进位地加到eO数组的每一位上
public void setExclusiveOf(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; } }
函数4,将arr中所有的数转换成32位k进制后加到eO变量的每一位上,然后将eO变量转换成十进制的数并返回
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; }
六、只用位运算不用算术运算实现整数的加减乘除运算
题目:给定两个32位整数a和b,可正,可负,可0。不能使用算术运算符,分别实现a和b的加减乘除运算。如果给定的a和b执行加减乘除的某些结果本来就会导致数据的溢出,那么不用为那些结果负责。
1.用位运算实现加法运算
注意:初始化sum=a,是为了考虑当b为0时,无法进入while循环执行sum = a ^ b;这个操作。
public int add(int a, int b){ int sum = a; while( b != 0){ sum = a ^ b; b = (a & b) << 1; a = sum; } return sum; }
分析实现过程:
1.如果不考虑进位,a^b就是正确结果,因为1加1=0,1加0=1,0加1=1,0+0=0 例如: a:0 0 1 0 1 0 1 0 1 b:0 0 0 1 0 1 1 1 1 c:0 0 1 1 1 1 0 1 0 2.在只算进位的情况下,也就是a加b过程中由于进位产生的值是什么,就是(a&b)<<1,因为在第i位上只有1和1相加才会产生上一位即i-1位的进位
a:0 0 1 0 1 0 1 0 1
b:0 0 0 1 0 1 1 1 1
d:0 0 0 0 0 1 0 1 0(从右数第1位和第3位需要进位,因此在相加的过程中,第2位和第4位上需要加上1,因此(a&b)<<1) 3.把第1步的不考虑进位的相加值与第2步的只考虑进位的产生值再相加,就是最终的结果。由于过程中可能还会产生进位,所以需要重复直到进位产生的值完全消失。
a:0 0 1 0 1 0 1 0 1 b:0 0 0 1 0 1 1 1 1
c:0 0 1 1 1 1 0 1 0
d:0 0 0 0 0 1 0 1 0
c:0 0 1 1 1 0 0 0 0
d:0 0 0 0 1 0 1 0 0
c:0 0 1 1 0 0 1 0 0
d:0 0 0 1 0 0 0 0 0
c:0 0 1 0 0 0 1 0 0
d:0 0 1 0 0 0 0 0 0
c:0 0 0 0 0 0 1 0 0
d:0 1 0 0 0 0 0 0 0
c:0 1 0 0 0 0 1 0 0(返回)
d:0 0 0 0 0 0 0 0 0
2.用位运算实现减法运算
实现a-b,只要实现a+(-b)即可。一个数的相反数,就是这个数的二进制数表达取反加1(补码)。
public int negNum(int n){ return add(~n, 1); } public int minus(int a, int b){ return add(a, negNum(b)); }
3.用位运算实现乘法运算
a*b=a * 20 * b0 + a * 21 * b1 + a * 22 * b2 + ... + a * 231 * b31(bi表示的是二进制中第i位的值,从左起0开始)
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=22=000010110,b=13=000001101,res=0 a:0 0 0 0 1 0 1 1 0 b:0 0 0 0 0 1 1 0 1 r:0 0 0 0 0 0 0 0 0
b的最右侧是1,所以res = res + a,同时b右移一位,a左移一位
a:0 0 0 1 0 1 1 0 0
b:0 0 0 0 0 0 1 1 0
r:0 0 0 0 1 0 1 1 0
b的最右侧是0,res不变,同时b右移一位,a左移一位
a:0 0 1 0 1 1 0 0 0
b:0 0 0 0 0 0 0 1 1
r:0 0 0 0 1 0 1 1 0
b的最右侧是1,res = res + a,同时b右移一位,a左移一位
a:0 1 0 1 1 0 0 0 0
b:0 0 0 0 0 0 0 0 1
r:0 0 1 1 0 1 1 1 0
b的最右侧是1,res = res + a,同时b右移一位,a左移一位
a:1 0 1 1 0 0 0 0 0
b:0 0 0 0 0 0 0 0 0
r:1 0 0 0 1 1 1 1 0
b为0,返回res=100011110=286
4.用位运算实现除法运算
用位运算实现除法运算,其实就是乘法的逆运算。
(1)a和b都不为负数或者如果a和b中有一个负数或者都为负数时,可以先把a和b转成正数,计算完成后再看res的真实符号即可(正负得负、负负得正、正正得正)。
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 >= (y << i)){ res |= (1<<i); x = minus(x, y<<i); } } return isNeg(a) ^ isNeg(b) ? negNum(res) : res; }
如果b*res=a,那么a=b * 20 * res0 + b * 21 * res1 + b * 22 * res2 + ... + b * 231 * res31
分析执行过程:让b向左移动i次,即b * 2i,然后观察a是否b * 2i,如果大于,就令res的第i位等于1,然后让a - b * 2i为a,然后反复操作。
假设a=286=100011110,b=22=000010110,res=0 a:1 0 0 0 1 1 1 1 0 b:0 0 0 0 1 0 1 1 0 r:0 0 0 0 0 0 0 0 03
(i=3时)a = a - b * 2
2
a:0 0 1 1 0 1 1 1 0
b:0 0 0 0 1 0 1 1 0
r:0 0 0 0 0 1 0 0 0
(i=2时)a = a - b * 2
(i=2时)1
a:0 0 0 0 1 0 1 1 0
b:0 0 0 0 1 0 1 1 0
r:0 0 0 0 0 1 1 0 0
(i=1时)b向左移动一位后大于a,说明a已经不能包含b * 2
,0
a:0 0 0 0 1 0 1 1 0
b:0 0 0 0 1 0 1 1 0
r:0 0 0 0 0 1 1 0 1
(i=0时)b向左移动一位后a==b,说明剩下的a还能包含一个b * 2
,即res0=1,此时说明a已经被完全分解干净,返回res=000001101=13
(2)以上方法可以算绝大多数情况,但是int类型的整数最小值为-2147483648,最大值为2147483647,最小值的绝对值比最大值的绝对值大1,所以,如果a或b等于最小值,是转不成相对应的正数的(~n + 1)。
即有下面四种情况:
- 如果a和b都不为最小值,直接使用div(a,b)
- 如果a和b都为最小值,直接返回1
- 如果a不为最小值,而b为最小值,直接返回0
- 如果a为最小值,b不为最小值,怎么办?
假设整数的最大值为9,最小值为-10,当a和b都属于[-9,9]时,也就是情况1;当a和b都等于-10时,也就是情况2;当a属于[-9,9],而b等于-10时,也就是情况3;
那么,当a=-10,而b属于[-9,9]时,
第一步:假设a=-10,b=5
第二步:计算(a+1)/b的结果,记为c,即c=-9/5=-1
第三步:计算c*b的结果,即-1*5=-5
第四步:计算(a - (c * b))/b,记为rest,意义是修正值,即(-10 - (-5))/5=-1,得到的是修正值,即rest=-1
第五步:返回c+rest,即-9
即a/b的值可以表示为
综上,除法运算的全部过程为:(注意要有异常处理的过程。)
public int divide(int a, int b){ if(b==0){ throw new RuntimeException("divided 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(res, b)), b)); } else{ return div(a, b); } }
七、O(n)时间复杂度得到输入数组中某两个数异或的最大值。
例如:[3, 10, 5, 25, 2, 8]中,5^25的最大值是28
思路:比特位操作。
解法:
生成变量max,表示
生成变量mask,表示,
XOR性质,A^B=C → A^B^B=C^B → A=C^B 则tmp ^ prefix = max →
3 → 0 0 0 1 1
10 → 0 1 0 1 0
5 → 0 0 1 0 1
25 → 1 1 0 0 1
2 → 0 0 0 1 0
8 → 0 1 0 0 0
i=4时,mask=10000, set={00000,10000},tmp=10000,prefix=00000, max=10000
i=3时,mask=11000, set={00000,01000,11000},tmp=11000,prefix=00000,max=11000
i=2时,mask=11100, set={00000,01000,00100,11000},tmp=11100,prefix=00100,max=11100
i=1时,mask=11110, set={00010,01010,00100,11000,01000},tmp=11110,set中不包含
i=0时,mask=11111, set={00011,01010,00101,11001,00010,01000},tmp=11111,set不包含