题目
LeetCode: 137. 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:
Your algorithm should have a linear runtime complexity. Could you implement it without using extra memory?
你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
Example 1:
Input: [2,2,3,2]
Output: 3
Example 2:
Input: [0,1,0,1,0,1,99]
Output: 99
解法一(较为复杂)
大致思路
-
将输入数组中的每个数看做是2进制数,由于待查找的数字(设为
X
)只出现1次,而其他数字都出现了3次,因此对于这些2进制数而言,每个bit位上1出现的次数之和必定为能够整除3,或者余数为1(包括符号位),而余数为1的bit位,即代表待查找数X
上相同bit位上的值为1,而待查找数上其他的bit位上为0,因为其他bit位上数值1出现次数必定为3的整数倍(包括0),这表明代表此bit位上的1都来自于出现次数为3的数字,而非待查找数字X
。 -
确定了大致思路之后,那么接下来的工作就是统计输入数组中的所有数各个bit位上1出现的次数之和,然后找出数值1出现次数之和除以3的余数为1的bit位,将对应bit位置为1,其余bit位置为0,即可得待查找数
X
。
统计各bit位数值1出现次数:
-
假设
a,b
为统计某个bit位1出现次数的2个计数位(因为待计数范围为0~2,而2个计数位可以表示0~3,因此使用2个计数位),c
为当前bit位下一个出现的值(0或1)。 -
设
a
为高位,b
为低位,当a=0,b=0
时,则表明当前bit位1出现的次数为0;当a=0,b=1
时,则表明当前bit位1出现的次数为1;当a=1,b=0
时,则表明当前bit为1出现的次数为2;当1出现3次时,则将计数位a和b置0,表明一个循环。 -
假设
a,b
计数器对应的bit位每次接收到待统计的值c
后,变化后的对应值为a',b'
,则各种可能情况如下表所示:
计数器a,b代表值 | a | b | c | a’ | b’ | 计数器a’,b’代表值 |
---|---|---|---|---|---|---|
0 | 0 | 0 | 1 | 0 | 1 | 1 |
1 | 0 | 1 | 1 | 1 | 0 | 2 |
2 | 1 | 0 | 1 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 1 | 0 | 0 | 1 | 1 |
2 | 1 | 0 | 0 | 1 | 0 | 2 |
小结:
由上表可知,从计数器从低位到高位的数值有以下规律(类似于进位,低位确定了才能确定高位):
b'
为1时,只有两种情况,即a=0, b=0,c=1
或a=0,b=1,c=0
时,两种情况不会同时出现,且每个情况只会产生一个结果,故有表达式:
b' = ((~a) & (~b) & c) | ((~a) & b & (~c)) = (~a) &((~b) & c | b & (-c)) = (~a) & b^c
- 即
b' = (~a) & (b^c)
a'
为1时,基于低位计数器b'
的值,只有两种情况,即a=0, b'=0, c=1
或a=1, b'=0, c=0
时,两种情况不会同时出现,且每个情况只会产生一个结果,故有表达式:
a' = ((~a) & (~b') & c) | (a & (~b') & (~c)) = (~b') & ((~a) & c | a & (~c)) = (~b') & a^c
- 即
a' = (~b') & (a^c)
PS:
上式最后的化简,是根据逻辑代数的分配律以及异或运算等价式所得:
- 逻辑代数分配律:
a & (b|c) = a&b | a&c
- 异或运算等价式:
a^b = a&(~b) | (~a)&b
总结:
-
虽然上述所有的分析和操作都是针对於单个bit,但是由于每个操作都是按位运算,因此很容易就扩展到整个数的所有bit位上,故不论是10进制还是2进制数,此统计过程都适用。
-
最后对各个bit位上出现的次数求余时,和之前类似,可以使用按位运算实现。当且仅当
a'=0,b'=1
时,对应bit位上1出现的次数为1,即代表待查找数X
的此bit位上的数值也为1,故设此bit位的值为num
,则有num=(~a') & b'
,而在此题中,b'=1
只有一种情况,故直接有num = b'
代码
-
时间复杂度:O(n)
-
空间复杂度:O(1)
class Solution {
public int singleNumber(int[] nums) {
// 计数器
int a = 0, b = 0;
// 遍历原始数组,统计各个bit位上1出现的次数
for (int c : nums) {
b = (~a) & (b ^ c);
a = (~b) & (a ^ c);
}
// 最后根据计数器a,b获取只出现1次1的bit位,最终按位与即为只出现1次的数字
// return (~a) & b;
return b;
}
}
推广
使用本文介绍的方法,同样适用于求解形如此题的一些问题,只要待查找元素的出现次数小于其他元素,且其他元素出现次数相同,如:
给定一个非空整数数组,除了某个元素只出现2次以外,其余每个元素均出现了5次。找出那个只出现了2次的元素。
计数器变化表:
计数器a,b,c代表值 | a | b | c | d | a’ | b’ | c’ | 计数器a’,b’,c’代表值 |
---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 1 |
1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 2 |
2 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 3 |
3 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 4 |
4 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 1 |
2 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 2 |
3 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 3 |
4 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 4 |
推导公式:
c'
= (~a)&(~b)&(~c)&d | (~a)&b&(~c)&d | (~a)&(~b)&c&(~d) | (~a)&b&c&(~d)
= (~a)&(~c)&d | (~a)&c&(~d)
= (~a)&(c^d)
b'
= (~a)&(~b)&(~c')&d | (~a)&b&c'&d | (~a)&b&(~c')&(~d) | (~a)&b&c'&(~d)
= (~a)&(~c')&(b^d) | (~a)&b&c
a'
= (~a)&(~b')&(~c')&d | a&(~b')&(~c')&(~d')
= (a^d)&(~b')&(~c')
num = (~a)&b&(~c);
代码:
class Solution {
public int singleNumber(int[] nums) {
// 计数器
int a = 0, b = 0, c = 0;
// 遍历原始数组,统计各个bit位上1出现的次数
for (int d : nums) {
c = (~a) & (c ^ d);
b = (~a) & (~c) & (b ^ d) | (~a) & b & c;
a = (a ^ d) & (~b) & (~c);
}
// 最后根据计数器a,b,c获取出现2次1的bit位
return (~a) & b & (~c);
}
}
解法二(便于理解,较为简单)
主要思想
-
上述的绘制变化情况表,推导公式,化简公式的求解方式,需要一次性穷举所有可能的情况,这样当数字出现次数较大时,光绘制变化情况表就要耗费大量的时间,更不用说还需要求解推导公式,对公式进行化简,因此上述求解方式综合来看并非是最优解法。
-
另一种思路依旧是采用计数器计数的方式,但与上述方法不同,而是让计数器正常计数,当某个bit位上1出现次数达到循环点时(如在本题中,即为3),则将对应bit位的所有计数器置为0,重新开始计数,视为新一轮循环。
计数器计数方式:
每次更新计数器时,从高位开始更新,当计数器低位全为1,且当前bit位的下一个待统计值也为1,则表明当前高位更新后的值需要加1。
假设a,b,c
为计数器,依次从高到低,i
为当前bit位的下一个待统计值,则当b=1,c=1,i=1
时,计数器a
的值需要加1;同理,当c=1,i=1
时,计数器b
的值需要加1;当i=1
时,计数器c
的值需要加1。由于各个计数器之间是相互独立按位运算,需要避免进位的影响,需要使用不进位相加的方式进行按位运算(即异或运算),故有:
a = a^(b & c & i)
b = b^(c & i)
c = c^i
假设有一标记bit变量mark
,同于标记当前bit位上1出现的次数是否到达循环点(假设本题为5),则当标志位满足a=1,b=0,c=1
时,则mark
等于0,表明需要将当前bit位上的计数器都置为0,重新开始循环,其他情况下mark
为1,故有:
mark = ~(a & (~b) & c)
当mark
为1时,计数器正常计数,当mark
为0时,则对应bit位表明到达循环点,需要将对应的计数器置为0,每次求得mark
的最新值后,还要在最后还需要更新计数器:
a = a & mark
b = b & mark
c = c & mark
对于本题:
只需要两个计数器a,b
,计数循环点为3(a=1,b=1
),故有:
-
a = a^(b & i)
-
b = b^i
-
mark = ~(a & b)
-
a &= mark
-
b &= mark
将1出现次数为1的bit位置为1,则得到最终要查找的数:
num = (~a)&b
由于本题中待查找数出现次数为奇数,因此只要将最低位计数器b
为1时的对应的bit位置为1,即可:
num = b
代码
-
时间复杂度:O(n)
-
空间复杂度:O(1)
class Solution {
public int singleNumber(int[] nums) {
// 设置计数器,以及标记变量
int a = 0, b = 0, mark = 1;
// 遍历原始数组,统计各个bit位上数值1出现的次数
for (int num : nums) {
a ^= b & num;
b ^= num;
// 计算标记变量mark,更新计数器的值,若达到循环点3(即a=1,b=1),则将计数器对应bit位置为0,否则保持不变
mark = ~(a & b);
a &= mark;
b &= mark;
}
// 由于本题中,待查找数出现次数为奇数次,因此只要每个bit位的最低计数位为1,则待查找数对应bit位也为1
return b;
}
}
推广
使用本文介绍的方法,同样适用于求解形如此题的一些问题,只要待查找元素的出现次数小于其他元素,且其他元素出现次数相同,如:
给定一个非空整数数组,除了某个元素只出现2次以外,其余每个元素均出现了5次。找出那个只出现了2次的元素。
class Solution {
public int singleNumber(int[] nums) {
// 设置计数器,以及标记变量
int a = 0, b = 0, c = 0, mark = 1;
// 遍历原始数组,统计各个bit位上数值1出现的次数
for (int i : nums) {
a ^= b & c & i;
b ^= c & i;
c ^= i;
// 计算标记变量mark,并更新计数器的值,若达到循环点5(即a=1,b=0,c=1)
// 则将计数器对应bit位置为0,否则保持不变
mark = ~(a & (~b) & c);
a &= mark;
b &= mark;
c &= mark;
}
// 当最终计数器a=0,b=1,c=0时,则表明待查找数对应bit位值为1
return (~a) & b & (~c);
}
}
参考资料
LeetCode 137. Single Number II-Discuss
Leetcode 137:只出现一次的数字 II(最详细的解法!!!)