LeetCode137 Single Number II 只出现一次的数字 II

  • 题目描述

  给定一个非空整数数组,除了某个元素只出现一次之外,其余每个元素都出现了三次,找出那个只出现一次的元素。

  说明:算法应该具有线性时间复杂度,并且不使用额外空间。

  • 分析

  首先,假如说题目没有说明里面的要求,那么就非常简单,可以使用一个map用来存储元素和元素出现的次数,遍历完数组之后,遍历这个map,就可以找到只出现一次的元素。此时时间复杂度为O(n),空间复杂度为O(n)。而说明要求空间复杂度为O(1),不满足。需要另寻它法。

  如果出现多次的元素,其次数不是3,而是2,那么这个问题相信也就难不倒大部分人了。此时,可以利用位运算,利用异或运算其“同零异一”的特性,来解决问题。比如数组只有三个元素:2,2,3. 三个数字的二进制表示为:10,10,11。那么对三个数进行异或运算,出现两次的数,在对应的bit位上,由于“同零”的特性,变为0,那么所有的出现两次的数字异或运算后的结果就是零;而只出现一次的数字,其与零异或运算的结果就是它自身。如上例中10,10,11的异或运算结果是11,就是“3”这个数字本身。因此可以很简洁地写出代码。

1 int singleNumber(vector<int>& nums) {
2         int a=0;
3         for(auto n:nums) a^=n;
4         return a;
5     }

  但是题目中的条件是其余每个元素都出现了三次。

  我们仍然延续位运算的思想。

  考虑数组9,7,7,7,其二进制表示为:1001,0111,0111,0111。分别统计 从左到右四个bit位上的0和1分布情况。最高位,1出现了1次;第二位,1出现了3次;第三位,1出现了3次;最低位,1出现了4次。很容易发现,中间两位1出现的次数为3的倍数,这两位也只在数字7的二进制表示中出现,而不在9的二进制表示中出现;最高位和最低位1出现的次数模3的余数为1,这两位都在9的二进制表示中出现。

  因此,可以得出结论,在此题的背景下,统计所有数组所有int型元素在32个bit位上出现1的数目,如果某个位上1出现的次数模3余数为1,那么1一定在那个“唯一”的数字中出现;反之,某个位上1出现的次数为3的整数倍,那么1一定不在“唯一”的数字中出现。据此,可以写出代码。

 1 int singleNumber(vector<int>& nums) {
 2         vector<int> cnt(32,0);
 3         for(auto n:nums)
 4         {
 5             for(int i=0;i<32;i++)
 6             {
 7                 if(n&1) cnt[i]++;
 8                 n>>=1;
 9             }
10         }
11         int ans=0;
12         for(int i=0;i<32;i++)
13         {
14             if(cnt[i]%3) ans+=1<<i;
15         }
16         return ans;
17     }

  上面的代码在LeetCode上能AC,时间复杂度O(n),空间复杂度O(1)。

  但是还可以更快吗?像上面的直接使用异或运算那样?

  上面的算法我们记录了32个bit位上每一个位置1出现的次数,实际上我们需要的是1出现的次数模3的余数值。模3的结果只能有0,1,2,换成二进制表示是00,01,10. 因此我们可以用两个int型数字a和b来记录所有bit位的信息。例如,a的最后一位bit位值为0,b的最后一位bit位的值为1,那么两者联合起来表示的意思就是:数组所有的数在最后一位bit位出现1的次数之和模3的余数是01,即1。其他bit位类推。

  下面我们的目标就是,找到a和b和数组元素的某种位运算组合,使得a,b能完整地表达出我们需要的信息。

  假设这种运算已经进行到数组第k-1个元素,数组下一个元素为k。此时a和b中某一个bit位——任意的一位,假设是从最低位往最高位数,第p=10位——的值分别为pa,pb。元素k的第10位值为pk。pa,pb,pk的取值无非就是0或者1。对于新来的元素k,根据pk的取值,pa,pb也需要发生相应的变化。见下图

  如上图所示,目标明确了:找到某种pa,pb,pk之间的位运算,使得pa,pb分别编程next pa和next pb。

  我们可以先看pb和pk。首先看一下pb和pk异或运算的结果。

  可以看到,前面5种情况,pb^pk的值和next pb一致,只有最后一种情况不一致。说明仅仅依靠pb^pk是不够的。我们发现,a的信息还没有利用。最后一种情况下,pb^pk=1,pa=1,目标是pb^pk与pa的某种位运算得到next pb=0。尝试一下,我们发现此时,(pb^pk)&(~a)=next pb。计算一下其他几种情况下是否也满足。

  计算发现,此时next pb = (pb^pk)&(~a)!

  同理我们可以根据pa,pk和pb组合出next pa的结果。经过推理尝试,发现next pa = (pa^pk)&(~next pb)

  上述推导出的bit运算对32个bit位都适用,于是最终得到公式:

  b=(b^k)&(~a);

  a=(a^k)&(-b);

  对数组中的元素依次进行上式的运算,最后,我们想求得只出现一次的数,就是每个bit位上1出现的次数之和模3余1的bit位组成的数,那就是b。代码如下

 1 class Solution {
 2 public:
 3     int singleNumber(vector<int>& nums) {
 4         int a=0,b=0;
 5         for(auto n:nums)
 6         {
 7             b=(b^n)&(~a);
 8             a=(a^n)&(~b);
 9         }
10         return b;
11     }
12 };

  上面的代码在LeetCode能AC。

  那么,a和b只有上面这一种组合位运算能得到next a和next b吗?其实有很多种,比如笔者就发现了下面另外一种:

  a=((b&n)^n)^(a&~b);
  b=(b^n)^(a&n);

1 int singleNumber(vector<int>& nums) {
2         int a=0,b=0;
3         for(auto n:nums){
4             a=((b&n)^n)^(a&~b);
5             b=(b^n)^(a&n);
6         }
7         return a;
8     }

  上面的代码同样AC。

  更多的方法等你去发现和总结。

posted @ 2020-02-29 22:02  p_is_p  阅读(224)  评论(0编辑  收藏  举报