https://leetcode.com/problems/single-number-ii/
题目描述:
Given a non-empty array of integers, every element appears three times except for one, which appears exactly once. Find that single one.
Note:
要求时间复杂度O(n),空间复杂的O(1)
解题思路(方法一):
1、这道题和上一道题一样,要求时间复杂度是线性的,要求不能使用空间复杂度是O(1)的。但不同的是这次除了一个数只出现一次,其余所有数都出现了三次。我们又要怎么解决这道题呢?
2、同样直接上讨论区代码,再次膜拜大神。大神使用了一种统计的方法,不过不是我等平常思维的统计每个数出现了几次,而是开了一个长度为32的数组,统计每个二进制位出现了几次,最后对3取模(如果是出现了K次就对K取模),取模完哪一位不是3的整倍数,就说明只出现了一次的那个数,在这个位上为1,最终可以求出最后的结果。以下举例说明。
举例说明:
c++中存储一个int型整数,都是32位的空间,我们也开32位的数组。但以下为了表示简便,我们只用最后的4位,就足够了。
假定我们的array of integers为[1,2,2,1,1,2,4,4,5,4],写成二进制位就是:
1:0001
2:0010
2:0010
1:0001
1:0001
2:0010
4:0100
4:0100
5:0101
4:0100
T:0434
R:0101
(T表示total,合计,每一列的和。R表示对3取模完之后的结果)
然后对T中的数值,每一位都对3取模,可以看到:出现了3的整数倍次的,取模完结果都是0;出现了非3的整数倍次的,即只出现了一次的那个数,取模完结果都为1,说明只出现一次的那个数,在当前这个位有出现过,最后也可以求出这个值。
不得不赞叹二进制位的神奇,可以发挥出“记录”的效果。这要是三进制位,就不能这样子处理了。二进制位为1,表示出现过,在这种“1个只出现1次,其余都出现了n次”的题目中,可以发挥出奇效。
不过似乎不是O(n)的时间复杂度?
代码(cpp):
class Solution {
public:
int singleNumber(vector<int>& s) {
vector<int> t(32);//开辟一个32位的数组
int i,j,n;
for (i = 0; i < s.size(); ++i){
n = s[i];
for (j = 31; j >= 0; --j)
{
t[j]+=(n&1);//统计当前这个数的二进制位情况
n >>= 1;
if (!n)
break;
}
}
int result= 0;//表示最后的取模完的结果
for (j = 31; j >= 0; --j){
n = t[j] % 3;
if (n)
result+=(1<<(31-j));
}
return result;
}
};
解题思路(方法二):
上一篇博客中提出的方法很容易理解,但是不是O(n)的时间复杂度,而是O(n^2),这点应该很多朋友都能看出来。
今天给大家分享一个O(n)的方法,先贴出简洁的代码给大家欣赏一下。这个方法同样参考于discuss区。
代码:(python)
class Solution:
def singleNumber(self, nums: List[int]) -> int:
a = 0
b = 0
for i in range(len(nums)):
b = (b ^ nums[i]) & ~a
a = (a ^ nums[i]) & ~b
return b
短短几行代码,简洁扼要地完成了任务。以下举例详细说明为什么能这样子做,以及推测要如何产生这样子的想法。
举例说明:
数组为[2,2,2,3],一共有四个元素,进行四次循环。
第一次循环,b=(0000^0010)&1111=0010=2,a=(0000^0010)&1101=0000=0
第二次循环,b=(0010^0010)&1111=0000=0,a=(0000^0010)&1111=0010=2
第三次循环,b=(0000^0010)&1101=0000=0,a=(0010^0010)&1111=0000=0
第四次循环,b=(0000^0011)&1111=0011=3,a=(0000^0011)&1100=0000=0
不知道大家有没有发现,某个值nums[i]第一次出现的时候,b把它记录了下来,这时候a=0;接着第二次出现的时候,b被清空了,记录到了a里面;接着第三次出现的时候,b和a都被清空了。
如果一个数组中,所有的元素除了一个特殊的只出现一次,其他都出现了三次,那么根据我们刚刚观察到的结论,最后这个特殊元素必定会被记录在b中。
有些朋友会说,但是不一定数组都是刚好3个2都在一起,3个4都在一起,都能够满足刚刚这样子的做法。
上上篇博客136题中,笔者本人提出了异或其实是满足交换律和结合律的,而且&这个操作也是满足交换律和结合律的,所以无论3个2会不会一起出现,结果都是会刚好抵消的。
所以上述的方法可以解决这个问题。
怎么想出这种方法的:
其实discuss区的大神是设计了一种方法,借由这种方法推出了a和b的变换方式…
我们想要达到的效果其实是——
a | b | 作用 | |
---|---|---|---|
初始状态 | 0 | 0 | |
第一次碰见某个数x: | 0 | x | 把x记录在b中 |
第二次碰见某个数x: | x | 0 | 把x记录在a中 |
第三次碰见某个数x: | 0 | 0 | 把a和b都清空,可以处理其他数 |
还记得我们之前处理“所有元素都出现两次,只有一个特殊元素出现一次”的问题吗?其实我们那会想要达到的状态也是——
a | |
---|---|
初始状态 | 0 |
第一次碰见某个数x: | x |
第二次碰见某个数x: | 0 |
那么这次我们同样利用异或运算,看能不能设计出一种变换的方法让a和b按照上述变换规则,进行转换。
b=0时碰到x,就变成x;b=x时再碰到x,就变成0,这个不就是异或吗?所以我们也许可以设计b=b xor x。
但是当b=0时再再碰到x,这时候b还是要为0,但这时候不同的是a=x,而前两种情况都是a=0。所以我们可以设计成:b=(b xor x)&~a
同样道理,我们可以设计出:a=(a xor x)&~b
试着解释一下,就是当a为0时候(~a=1),保留b的异或结果。当b非0时候,a的异或结果无效(b & ~b = 0),很有意思。
感想:
异或其实已经内含了“判断”的过程。想一下我们“所有元素都出现两次,只有一个特殊元素出现一次”的问题,两个相同的数字异或结果就是0,其实就是一个判断过程,只要数字出现过一次,它就会永久记得你。巧妙使用二进制以及组合逻辑操作符可以解决这类问题。