leetcode 260 137 只出现一次的数字Ⅲ、Ⅱ 位运算“异或”的巧用(二)

题目260

原题地址

260 .只出现一次的数字 III
给定一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。
示例 :
输入: [1,2,1,3,2,5]
输出: [3,5]
注意:
结果输出的顺序并不重要,对于上面的例子, [5, 3] 也是正确答案。
你的算法应该具有线性时间复杂度。你能否仅使用常数空间复杂度来实现?

本题依然可以使用哈希表来得到时间复杂度为O(N)的解法。不过本文主要解释一下位运算解法的难点。

关于异或

基础知识已在只出现一次的数字中讲解,此处不再重复。

如今数组中只出现一次的数字变成了两个,按照上一题的套路,我们将数组中的数字逐一异或会得到什么呢?
假设只出现一次的数字分别为a,b,则最终我们得到的数字为c=a^b。
然而就像我们无法只根据a+b的结果分别求出a,b一样,只是知道a^b也无法知晓a和b本身。
接下来就是题解的巧妙与难理解之处了。

利用a^b把数组元素分为两组

既然a和b都只出现了一次 ,那么一定有:

a!=b
a^b!=0
a^b的结果对应的二进制数字至少有一位为1

到这里你能想到将数组元素分组的依据和目的了吗?

我们的目的是把a和b分到不同的组中,那该如何实现呢?

上个步骤中我们得到了最终数字c(等于a^b)。我们以a=13(二进制1101),b=7(二进制0111)为例。

      ---   ---
a=    |1| 1 |0| 1   
b=    |0| 1 |1| 1   
--------------------------
a^b=  |1| 0 |1| 0
      ---   ---     //仅当在某一位a和b不相等时,a^b的此位才为1

首先我们找到a^b中任意一个不为0的二进制位。
由异或的性质,也可以观察如上的例子,可以发现当且仅当a和b在某一位不相等时,a^b的此位为1
利用这个性质,我们可以找到一个数T,令这个数与a或b做位的“与(&)”运算时,可以得到不同的结果。

这个T就是题解中所谓的掩码。我们利用仅含一位1的掩码对数组元素进行分组,可以发现如下规律。

假设T的第x位为1,其他位为0,另其与任意一数m做与运算,则:
当m的第x位为1时,T&m=T;
当m的第x位为0时,T&m=0;

那么,接上例,可以看到a^b(1010)从低数第二位为1,我们令T=0010。对数组中的每一个数nums[i],若nums[i]&T=0,则将nums[i]分到组A,否则分到组B。
其中a&T=0分到组A;b&T=T分到组B。
在这里插入图片描述
可见我们成功的利用位运算将a和b分到了不同的组中。

分完组我们就大功告成了吗?倒也没有,不过接近了。
对于数组中的其它数,我们也应用同样的规则进行分组,其实我们并不关心有多少数会被分配到A,有多少会分配到B,我们需要注意的是:相同的数一定会被分到同一组!!!(数字相同,二进制表示亦相同,第x位的数字自然也就一样了,按规则会被分到同一组)

由于除了a和b其他数字都出现了两次,可知A组除了a,其他任何数字一定都出现了两次,B组同理。
题目由此转变成了上一题,数组中只有一个数字出现了一次,可以应用之前的知识进行求解了。

令数组A中数字逐一异或,最终所得数字一定是a;B组同理。

总结求解步骤及代码

假设只出现了一次的数字为a和b:
1.逐一异或原始数组nums中的元素,得到结果a^b
2.找到a^b中一个为1的二进制位。令数T在该位为1,其余位为0。
3.对数组元素进行分组,若num[i]&T=0分到A,否则分到B。
4.逐一异或A中的数字,得到a;逐一异或B中的数字,得到b,结束。(这一步可以与分组同时进行,详见代码)

class Solution {
public:
    vector<int> singleNumber(vector<int>& nums) {
        int tmp=0;
        for(int i=0;i<nums.size();++i)//逐一异或原数组元素
        {
            tmp^=nums[i];
        }//循环结束tmp即为a^b
        int cmp=1;
        while((cmp&tmp)==0)//从最低位开始找到a^b中不为0的位,并以此作为掩码
        {
            cmp<<=1;
        }
        int n1=0,n2=0;
        for(int i=0;i<nums.size();++i)
        {
            if((nums[i]&cmp)==0)//分组,确定分组后直接进行异或操作
            {
                n1^=nums[i];
            }else{
                n2^=nums[i];
            }
        }
        vector<int> result(2);
        result[0]=n1;
        result[1]=n2;
        return result;
    }
};

题目137

原题地址

137 . 只出现一次的数字 II
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现了三次。找出那个只出现了一次的元素。
说明:
你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
示例 1:
输入: [2,2,3,2]
输出: 3
示例 2:
输入: [0,1,0,1,0,1,99]
输出: 99

关于这道题,精选题解有详细的讲解,相比之下官方题解就显得不明不白了。
在此我只打算对此题解中不好理解的部分展开解释一下,并对其中的位运算提供另一种思路。还请结合着精选题解阅读(也许你直接就看懂了,尴尬)。

思路


摘自精选题解:

考虑数字的二进制形式,对于出现三次的数字,各 二进制位 出现的次数都是 3 的倍数。
因此,统计所有数字的各二进制位中 1 的出现次数,并对 3 求余,结果则为只出现一次的数字。


请把此思路默念三遍! 之后的所有代码和操作都是为了以某种形式来实现这个思路。
精选题解中的方法二即是对此比较直观的实现,可以先看方法二再回头理解方法一。

我们想办法记录数组中所有数字每一位的出现次数,并将次数对3取余。由于一共有三种状态,那么对每一位都使用一个包含2个二进制位的数来记录状态,并用两个数two(记录高位),one(记录低位)来记录这些二进制。
以数组[3,5,7,11]为例

two,one是什么,图示:
在这里插入图片描述

将two,one的对应位拼接成一个2位的二进制数,这个数就表示了此位的出现次数模3的结果,可见对于拼接而成的每个二进制数,随着此位出现次数的增加会以00→01→10→00→…的顺序循环。

这里使用位运算的目的是,找到一种方法,使所有数位的计数能在同时按照此规律进行循环。也就是说,每当遍历到一个新的元素nums[i]时,我们要想办法同时计算出新的two,one的每一位(不要求同时的话就是精选题解的方法二)。

求解

精选题解中似乎是用了数字电路的知识化简出了最终的计算公式。数学语言的好处是清晰精炼,可惜就是不太容易看懂……

数电的知识不熟,去学习估计也要不少时间。不过看着状态位只有两位,状态总数比较少,这里我们尝试按自己的思路推导出two,one的计算方法。
由于逢3取模,每一位two,one的组合其实只有三种可能

twoone
00
01
10

如果nums[i]的某一位为0,则two,one均保持不变。
如果nums[i]的某一位为1,则该位的two,one有以下几种情况:

two的该位为1,说明此时two,one的状态为10,按照上述的状态循环,下一个状态应为00,即需要对two,one的此位进行置0操作。

two的该位为0,则同二进制加法的规则相同,此时若one和nums[i]的此位同时为1,则one需要置0,并向two进1

依据以上情况,我们可以总结出求解one,two的方法:

1、找到two和nums[i]同时为1的位,a=two&nums[i],用来最后将one和two的这些位 置0
2、找到one和nums[i]同时为1的位,b=one&nums[i],这些位会向two进1,由于此时two一定为0,所以将two置1即可
3、one先按照二进制加法的规则计算即可,1+1=0;0+1=1;1+0=1;0+0=0,这一步恰好可以由one^nums[i]完成
4、利用步骤1的结果,将one和two中对应的位 置0,此步可以通过one=one&(~a)来完成,two同理

可能的化简:
one=(one^nums[i])&[~(two&nums[i])]=?
two=[two^(one&nums[i])]&[~(two&nums[i])]=?
尝试化简,但是不会化简到精选题解的答案……不知道从根本上两种位操作是否有区别

按照题意,所有其它数均出现3次,也就是说其他数字引入的次数最终一定在上述规则的运作下转变到00的状态,而只出现了一次的数字会使状态转至01
观察可以发现,当遍历完整个数组时,two一定为0,而one则恰好为只出现了一次的数字。

至此,代码便水到渠成了。

代码

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int one=0,two=0;
        for(int i=0;i<nums.size();++i)
        {
            int a=(two&nums[i]);//需要最后置0的位
            int b=(one&nums[i]);//需要向two进1的位
            one^=nums[i];//one按照二进制加法规则进行计算
            two^=b;//two接受one的进位,由于状态中有进位时two一定为0,two|=b或者two^=b均可
            one&=(~a);//one的对应位置0
            two&=(~a);//two的对应位置0
        }
        return one;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值