美文网首页
Single Number (Bit Manipulation)

Single Number (Bit Manipulation)

作者: futurehau | 来源:发表于2016-07-28 17:35 被阅读72次

    题目描述

    给你一个数组,除了x,数组中每个元素出现2次,让你求解出这个x。
    [leetcode136]https://leetcode.com/problems/single-number/

    思路

    1. 使用hashMap是非常容易理解并求解的不再累述。
    2. 接下来就是我们的位操作,这一类题的一个通用的解法。

    算法步骤

    对这个数组的每一位进行异或,最后的结果就是那个单数来的数。

    算法原理

    异或操作满足:ab=ba,aa=0,0a=a
    所以一趟异或操作之后,那些出现偶数次的数对结果的贡献就没有影响,这些所有的出现偶数次的数异或之后为0,而0与那个单独出现的数x异或之后就为x。

    算法代码

    public class Solution {
        public int singleNumber(int[] nums) {
            int res=0;
            for(int i:nums)
                res^=i;
            return res;
        }
    }
    

    算法原理很简单,实现也很简单。但这里需要注意的是,那些出现多次的必须是出现偶数次,不然这个算法就失效了,那么对于出现奇数次的怎么办呢?接下来拓展详细描述这一类题目的解法。

    拓展一

    [leetcode]https://leetcode.com/problems/single-number-ii/
    这次数组中除了x外,每个元素出现3次,需要你找出x。注意到次数3为奇数了,上述方法失效。

    思路

    1. 当然了也可以使用一个hash,类似于上题的hash解法,不用改动代码即可实现。当然了,这种解法很low。不在详述。
    public class Solution {
        public int singleNumber(int[] nums) {
            Map<Integer,Integer> hashMap=new HashMap<Integer,Integer>();
            for(int i=0;i<nums.length;i++){
                if(!hashMap.containsKey(nums[i]))
                    hashMap.put(nums[i],1);
                else
                    hashMap.put(nums[i],2);
            }
            for(int i=0;i<nums.length;i++){
                if(hashMap.get(nums[i])==1)
                    return nums[i];
            }
            return -1;
        }
    }
    
    1. 使用位操作
      接下来介绍三种使用位操作的方法,其实他们的本质是一样的。

    位操作方法一

    算法步骤

    1. 因为int是32位,所以我们考虑使用一个长度为32的数组,初始化为0。
    2. 记res为最后的结果,由于res也为32位的int,我们只需要确定,res的每一位为1还是0即可,所以这里使用了一个外层32次的循环,内层函数每执行一次,确定了res的一个比特位。
    3. 假设现在需要确定res的第i比特位,内层函数找出原数组中在第i比特位上为1的数,统计这些数的个数,然后对3取模,结果要么是0,要么是1(因为本题中那个特殊的数只出现一次)这个结果就是我们的res的第i比特位的值。
    4. 32趟走完时候我们可以确定res的每一个比特位是0还是是1,也就可以确定res的值了。

    算法原理

    因为原数组中除了数res外,每个数都出现三次,所以我考察这些数的对应比特位。那些出现三次的数,对应为1的比特位相加后为3的倍数,只有这个只出现1次的数的那些为1的bit位,这些位数上count为3n+1,所以我们相加取模时候就可以确定哪些比特位为1。
    比如[1,2,2,2,3,3,3]

     num      a  b
      1    ->  0   1
      2    ->  1   0
      3    ->  1   1
    

    2和3都出现了三次,所以我们把对应比特位1的个数相加的话a比特位一共6个1,b比特位一共4个1,对3取模后a比特为0,b比特位1。我们就知道结果为0。

    算法代码

    public class Solution {
        public int singleNumber(int[] nums) {
            int[] count=new int[32];
            int res=0;
            for(int i=0;i<32;i++){
                for(int j=0;j<nums.length;j++){
                    if(((nums[j]>>i)&1)==1)
                        count[i]++;
                }
                res|=((count[i]%3)<<i);
            }
            return res;
        }
    }
    //当然,一般化,假如出现多次的数出现次数为k,只需要模k即可。
    //另外,如果那个特殊的数不是出现1次,
    //修改res|=((count[i]%3)<<i)即可,
    //例如假如出现2次,
    //只需要修改为res|=((count[i]%3==0?0:1)<<i)
    

    位操作方法二

    算法步骤

    1. 使用三个变量,ones twos xthree,如果二进制表示中,某个数位上"1"出现一次,那么ones在这个数位上就是"1",同理twos表示出现两次。
    2. 看看ones twos 的变化规则。新进来一个数nums[i],考察ones&nums[i],如果ones在这个比特位上为1,表面之前这个比特位"1"出现过一次,现在nums[i]在这个比特位上又为1,那么自然就是出现了两次"1",所以twos在这个比特位上就应该为1。ones的变化规则很简单,异或就行了。需要注意的是如果某个比特位在ones和twos中都为1,那么就代表出现了三次,需要把这个比特位清零。
    3. 遍历结束后,ones就是出现一次的数,twos就是出现两次的数。

    算法原理

    从比特位来考虑,出现三次的数在遍历结束之后对ones和twos都没有影响,因为他们最后会在他们比特位表示为"1"的位置上出现三次,他们的影响被代码

    xthree = ~(ones & twos);
                ones&=xthree;
                twos&=xthree;
    

    抹去,最后ones的各个比特位就是只出现一次的那个数的比特位,twos的各个比特位就是只出现两次的那个数的比特位。

    算法代码

    public class Solution {
        public int singleNumber(int[] nums) {
            //2,2,3,2
            int ones=0;//二进制表示中“1”出现一次的数位
            int twos=0;//二进制表示中“1”出现两次的数位
            int xthree=0;
            for(int i=0;i<nums.length;i++){
                twos|=(ones&nums[i]);//出现两次,当然是“之前”位置上首先就是“1”(ones),然后输入的数树在这个位置上又是“1”
                ones^=nums[i];//出现一次,当然是异或就行了,这样出现一次的变为0,出现0次的变为1
                xthree = ~(ones & twos);//一个数位在ones和twos同时为1,表示当前数位出现了3次,应该把这个数位变为0
                ones&=xthree;
                twos&=xthree;
            }
            return ones;
        }
    }
    //拓展,two最终记录的是出先两次的
    //http://www.cnblogs.com/daijinqiao/p/3352893.html
    //[1,2,2,2,3,3,4,4,4,5,5,5]
    //[1,2,2,3,3,3,4,4,4,5,5,5]
    

    位操作方法三

    这个方法其实和方法二一样,只不过搞得有点像数电设计中的状态转移。

    算法步骤&原理

    a表示32位的int,a的每一位怎么确定,在遍历原数组过程中,考察原数组中数的比特位表示,如果该位上"1" 出现次数为2,那么该位就为1,否则为0
    同理
    a表示32位的int,b的每一位怎么确定,在遍历原数组过程中,考察原数组中数的比特位表示,如果该位上"1" 出现次数为1,那么该位就为1,否则为0
    那么我们很容易得到下面的状态转移表
    (ai为a的i比特位,bi为b的i比特位,ci为新遍历到的数的i比特位)

     ai    bi    ci     ai’   bi'
     0     0     0      0     0
     0     1     0      0     1
     1     0     0      1     0
     0     0     1      0     1
     0     1     1      1     0
     1     0     1      0     0
    

    我们需要找到出现次数为1的比特位,也就是bi'为1
    顺便找到出现次数为2的比特位,也就是ai'为1
    所以ai'=aibici|~aibici
    bi'=aibici|aibici
    我们就可以确定出现次数为1的比特位,也就可以确定出现次数为1的数。
    这里的代码是同时找了次数为1的和为2的(这种理解是特殊的数出现一次或两次,二者选其一)

    算法代码

    public class Solution {
        public int singleNumber(int[] nums) {
            int a=0;
            int b=0;
            for(int c:nums){
                int tmpa=a&(~b)&(~c)|((~a)&b&c);
                b=(~a&~b&c)|(~a&b&~c);
                a=tmpa;
            }
            return a|b;
        }
    }
    

    当然,如果了解转台转移化简的话,可以简化为

     for(int c:nums){
                int tmpa=a&(~c)|(b&c);
                b=(~a&~b&c)|(b&~c);
                a=tmpa;
            }
    

    拓展二

    [leetcode260]https://leetcode.com/problems/single-number-iii/
    这次是数组中有两个不同的数都只出现过一次,其他数都出现两次,让找出这两个数。

    算法描述

    1. 对这些数进行异或操作得到ones
    2. 移位与操作得到once的比特位表示的第一个非“0”位置,并把一个数赋值为仅该位置为1,其他位置为0
    3. 用这个数去和数组中的数做与运算,根据与运算结果是否为0可以把原数组中的数分为两个阵营,分别对两个阵营进行异或操作,最终得到的两个结果就是所求的两个数。

    算法原理

    有两个数a,b只出现一次,其他两次,那么所有数进行异或之后的结果ones,ones比特位表示为1的位置肯定就是a和b在这个位置为一个1一个0,那么就可以使用这个位置把a和b分到不同的阵营中,两个阵营都是包含a/b和一堆成对出现的数,对他们进行异或操作即可求出a/b。

    算法代码

    public class Solution {
      public int[] singleNumber(int[] nums) {
          int one=0;
          for(int i:nums)
              one^=i;
          int tmp=1;
          while((one&1)!=1){
              one>>=1;
              tmp<<=1;
          }
          int res1=0;
          int res2=0;
          for(int i:nums){
              if((i&tmp)==0){
                  res1^=i;
              }
              else
                  res2^=i;
          }
          int[] res=new int[2];
          res[0]=res1;
          res[1]=res2;
          return res;
      }
    }
    

    当然了,继续拓展的话,如果有两个数出现1次,其他书出现三次,那么就是拓展一中的方法先找到为出现次数为1的两个数共同影响产生的结果ones,然后找到一个为“1”的比特位,然后分阵营,就变为两个拓展一类型的问题。
    如果一个一次一个两次其他三次,那么一次的就是ones,两次的就是twos。

    相关文章

      网友评论

          本文标题:Single Number (Bit Manipulation)

          本文链接:https://www.haomeiwen.com/subject/jvpajttx.html