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不包含
  • 相关阅读:
    1451. Rearrange Words in a Sentence
    1450. Number of Students Doing Homework at a Given Time
    1452. People Whose List of Favorite Companies Is Not a Subset of Another List
    1447. Simplified Fractions
    1446. Consecutive Characters
    1448. Count Good Nodes in Binary Tree
    709. To Lower Case
    211. Add and Search Word
    918. Maximum Sum Circular Subarray
    lua 时间戳和时间互转
  • 原文地址:https://www.cnblogs.com/BigJunOba/p/9646487.html
Copyright © 2011-2022 走看看