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 0
(i=3时)a = a - b * 2
3

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
2
(i=2时)
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
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 1
(i=0时)b向左移动一位后a==b,说明剩下的a还能包含一个b * 2
0
,即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不包含

 

posted @ 2018-09-14 14:40  BigJunOba  阅读(254)  评论(0编辑  收藏  举报