• 下面这张图可以很好的帮助你理解负数的移位运算符:

    到这里,我想你应该对位运算有了初步的认识,在这里把上面提到的部分案例执行对比一下让你看一下可能会理解的更清晰:

    位运算小技巧

    在这里有些常用的位运算小技巧。

    判断奇偶数

    正常判断奇数偶数的时候我们会这样写:

    if( n % 2 == 1)
        // n 是个奇数
    }

    使用位运算可以这么写:

    if(n & 1 == 1){
        // n 是个奇数。
    }

    其核心就是判断二进制的最后一位是否为1,如果为1那么结果加上2^0=1一定是个奇数,否则就是个偶数。

    交换两个数

    对于传统的交换两个数,我们需要使用一个变量来辅助完成操作,可能会是这样:

    int team = a;
    a = b;
    b = team;

    但是使用位运算可以不需要借助额外空间完成数值交换:

    a=a^b;//a=a^b
    b=a^b;//b=(a^b)^b=a^0=a
    a=a^b;//a=(a^b)^(a^b^b)=0^b=0

    原理已经写在注释里面了,是不是感觉非常diao呢?

    二进制枚举

    在遇到子集问题的处理时候,我们有时候会借助二进制枚举来遍历各种状态(效率大于dfs回溯)。这种就属于排列组合的问题了,对于每个物品(位置)来说,就是使用和不使用的两个状态,而在二进制中刚好可以用1和0来表示。而在实现上,通过枚举数字范围分析每个二进制数字各符号位上的特征进行计算求解操作即可。

    二进制枚举的代码实现为:

    for(int i = 0; i < (1<<n); i++) //从0~2^n-1个状态
    {
      for(int j = 0; j < n; j++) //遍历二进制的每一位 共n位
      {
        if(i & (1 << j))//判断二进制数字i的第j位是否存在
        {
          //操作或者输出
        }
      }
    }

    位运算经典问题

    有了上面的位运算基础,我们怎么用位运算处理实际问题呢?或者有哪些经典的问题可以用位运算来解决呢。

    不用加减乘除做加法

    题目描述

    分析:这道题咋一听可能没啥思路,简单研究一下位运算还是能独立推出来和理解的。

    当然,解决这题前,需要了解上面的四种位运算。还要知道二进制的运算:0+0=0,0+1=1,1+1=0(进位)

    对于加法的一个二进制运算。如果不进位那么就是非常容易的。这时候相同位都为0则为0,0和1则为1.满足这种运算的异或(不相同取1,相同取0)和或(有一个1则为1)都能满足.【建议收藏】面试官会的位运算奇淫技巧-LMLPHP但事实肯定有进位的运算啊!看到上面操作的不足之后,我们肯定还需要解决进位的问题对于进位的两数相加,这种核心思想为

    实现代码为:

    public class Solution {
         public int Add(int num1,int num2) {
      /*
       *  5+3   5^3(0110)   5&3(0001) 
       *  0101    
       *  0011 
       */

      int a=num1^num2;
      int b=num1&num2;
      b=b<<1;
      if(b==0)return a;
      else {
       return Add(a, b);
      }        
      }
    }

    当然,这里也可以科普一下二进制求加法:average = (a&b) + ((a^b)>>1) ;

    二进制中1的个数

    这是一道经典题,在剑指offer上也有对应题目,其具体题目要求输入一个整数,输出该数二进制表示中1的个数(其中负数用补码表示)

    对于这个问题,不用位运算将它转成二进制字符串直接枚举字符'1'的个数也可以直接求出来,但是这样做是没有灵魂的并且效率比较差。这里提供两种解决思路

    法一:大家知道每个类型的数据它的背后实际都是二进制操作。大家知道int的数据类型的范围是(-2^31,2^31 -1)。并且int有32位。但是并非32位全部用来表示数据。真正用来表示数据大小的也是31位。最高位用来表示正负

    首先要知道:【建议收藏】面试官会的位运算奇淫技巧-LMLPHP

    其次还要知道位运算&与。两个十进制与运算.每一位同1为1。所以我们用2的正数次幂与知道的数分别进行与运算操作。如果结果不为0,那么就说明这位为1.(前面31个都是大于0的最后一个与结果是负数但是如果该位为1那么结果肯定不为0)

    具体代码实现为:

    public int NumberOf1(int n) {
      int va=0;
      for(int i=0;i<32;i++)
      {
        if((n&(1<<i))!=0)
        {           
          va++;
        }
      }
      return va;       
    }

    法二是运用n&(n-1)。n如果不为0,那么n-1就是二进制第一个为1的变为0,后面全为1.这样的n&(n-1)一次运算就相当于把最后一个1变成0.这样一直到运算的数为0停止计算次数就好了,如下图共进行三次运算那么n的二进制中就有三个1。【建议收藏】面试官会的位运算奇淫技巧-LMLPHP实现代码为:

    public class Solution {
        public int NumberOf1(int n) {
        int count=0;
        while (n!=0) {
         n=n&(n-1);
         count++;
        }
        return count;
     }
    }

    只出现一次的(一个)数字①

    问题描述:

    分析:

    这是一道简单的面试题,面试官常问怎么样用不太复杂的方法找出数组中仅出现一次的数字(其他均出现两次),暴力枚举或者使用其他的存储结构都不够优化,而这个问题最高效的答案就是使用位运算。首先你要注意两点:

    具体的操作就是用0开始和数组中每个数进行异或,得到的值和下个数进行异或,最终获得的值就是出现一次(奇数次)的值。

    class Solution {
        public int singleNumber(int[] nums) {
            int value=0;
            for(int i=0;i<nums.length;i++)
            {
                value^=nums[i];
            }
            return value;
        }
    }

    只出现一次的(一个)数字②

    问题描述:

    分析:

    这题和上一题的思路略有不同,这题其他数字出现了3次,那么我们如果直接使用位运算异或操作的话无法直接找到结果,就需要巧妙的运用二进制的其他特性:判断整除求余操作。即判断所有数字二进制1的总个数和0的总个数一定有一个不是三的整数倍,如果0不是三的整数倍那么就说明结果的该位二进制数字为0,同理否则为1.

    在具体的操作实现上,问题中给出数组中的数据在int范围之内,那么我们就可以在实现上可以对int的32个位每个位进行依次判断该位1的个数求余3后是否为1,如果为1说明结果该位二进制为1可以将结果加上去。最终得到的值即为答案。

    具体代码为:

    class Solution {
        public int singleNumber(int[] nums) {
            int value=0;
            for(int i=0;i<32;i++)
            {
                int sum=0;
                for(int num:nums)
                {
                    if(((num>>i)&1)==1)
                    {
                        sum++;
                    }
                }
                if(sum%3==1)
                    value+=(1<<i);
            }
            return value;
        }
    }

    只出现一次的(两个)数字③

    题目描述

    思路

    上面的问题处理和理解起来可能比较容易,但是这个问题可能稍微复杂一点,但是这题可以通过特殊的手段转化为上面只出现一次的一个数字问题来解决,当然核心的位运算也是异或^

    具体思路就是想办法将数组逻辑上一分为二!先异或一遍到最后得到一个数,得到的肯定是a^b(假设两个数值分别为a和b)的值。在看异或^的属性:不同为1,相同为0. 也就是说最终这个结果的二进制为1的位置上a和b是不相同的。而我们可以找到这个第一个不同的位,然后将数组中的数分成两份,该位为0的进行异或求解得到其中一个结果a,该位为1的进行异或求解得到另一个结果b。

    具体可以参考下图流程:

    实现代码为:

    public int[] singleNumbers(int[] nums) {
        int value[]=new int[2];
        if(nums.length==2)
            return  nums;
        int val=0;//异或求的值
        for(int i=0;i<nums.length;i++)
        {
            val^=nums[i];
        }
        int index=getFirst1(val);
        int num1=0,num2=0;
        for(int i=0;i<nums.length;i++)
        {
            if(((nums[i]>>index)&1)==0)//如果这个数第index为0 和num1异或
                num1^=nums[i];
            else//否则和 num2 异或
                num2^=nums[i];
        }
        value[0]=num1;
        value[1]=num2;
        return  value;
    }

    private int getFirst1(int val) {
        int index=0;
        while (((val&1)==0&&index<32))
        {
            val>>=1;// val=val/2
            index++;
        }
        return index;
    }

    结语

    当然,上面的问题可能有更好的解法,也有更多经典位运算问题将在后面归纳总结,希望本篇的位运算介绍能够让你有所收获,对位运算能有更深一点的认识。对于很多问题例如博弈问题等二进制位运算能够很巧妙的解决问题,日后也会分享相关内容,敬请期待!


    推荐阅读

      跳表 | 会跳的链表原来这么diao

      我和蓝桥杯的那两年

    原创不易,如果觉得有所收获,希望大家点赞、分享、在看一键三连帮忙扩散,谢谢!

    【建议收藏】面试官会的位运算奇淫技巧-LMLPHP

    本文分享自微信公众号 - bigsai(bigsai)。
    如有侵权,请联系 support@oschina.cn 删除。
    本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

    03-14 08:37