zoukankan      html  css  js  c++  java
  • 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不包含
  • 相关阅读:
    LOJ164 高精度除法
    CQOI2013 新Nim游戏 和 BZOJ1299 巧克力棒
    UOJ514 通用测评号 和 CF891E Lust
    CF526F Pudding Monsters 和 CF997E Good Subsegments
    UOJ513 清扫银河
    SNOI2020 水池
    NOI2015 品酒大会 和 SNOI2020 字符串
    SNOI2020 生成树
    BJOI2020 封印
    UOJ523 半前缀计数
  • 原文地址:https://www.cnblogs.com/BigJunOba/p/9646487.html
Copyright © 2011-2022 走看看