先来看一道面试题:
给出 3n + 1 个数字,除其中一个数字之外其他每个数字均出现三次,要求找出这个数字。
例如:给出 [1,1,2,3,3,3,2,2,4,1],返回 4。要求做到一次遍历,O(1) 的空间复杂度。
解法一:
将每个数字都用二进制表示,那么可以把每一位的结果进行相加(十进制加),然后再对 3 取余,那么每一位的结果只能是 0 或者 1,而这个最终的结果就是要找出的那个数字。
python code
def singleNumber(self, nums):
counts = [0] * 32
result = 0
for i in range(32):
for num in nums:
if (num >> i) & 1:
counts[i] += 1
result |= (counts[i] % 3) << i
return result
这个解法时间复杂度是 O(n),但需要 32 次遍历,严格来说并不符合题目要求。
注意实现语言:在 python 中,int 类型的位数是随操作系统而变的。在 64 位系统上运行以上的代码在碰到负数时(涉及到补码)就会有问题。而 java, c, c++,默认的 int 都是 32 位,所以在上述代码中 hardcode 32 不会有问题。
解法二:
解法一之所以要用十进制加(并用一个额外数组来存储)是因为一个二进制 bit 无法存储四种状态:0 -> 1 -> 2 -> 3 。(如果将对 3 取余也算在内,那么只有三个状态 0 -> 1 -> 2 -> 0)
那么,为什么不用两个 bit 来表示呢?答案是肯定的:00 -> 01 -> 10 -> 00
。
而且这两个 bit 表示的状态变化是可以枚举的:假设用 A
, B
来表示当前的状态,C
表示输入,则 f(A)
和 f(B)
可以表示变化后的状态。
A | B | C | f(A) | f(B) |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
0 | 0 | 1 | 0 | 1 |
0 | 1 | 0 | 0 | 1 |
0 | 1 | 1 | 1 | 0 |
1 | 0 | 0 | 1 | 0 |
1 | 0 | 1 | 0 | 0 |
还记得这个表的术语吗?出计算机必修课《数理逻辑》:
真值表
。
现在的问题就变成能不能从上面的真值表推导出 f(A) 和 f(B) 的公式?一种暴力的方法就是肉眼推导,毕竟也只有 6 种状态变化。譬如可以看出 f(B) 有 5 种情况下都等于 B xor C
;只有第 6 种情况是个例外 。再仔细观察,可以发现当 A 等于 1 时,f(B) 都等于 0。所以结合可以得到:f(B) = B xor C & ~A
(~A
为对 A 取反)。
继续观察
f(A)
前,要注意一下f(A)
的输入变量:在写程序时,如果先计算出了f(B)
,那么f(A)
的输入就应该是 A,f(B),C,而不是 A,B,C —— 因为 f(B) 已经是 B 的下一个状态了。
继续观察真值表,不难得出 f(A) = A xor C & ~f(B)
。
至此,我们就可以用 AB 来表示某个 bit 上的状态变化,同理,对于题目中的数字上的所有 bit 都可以适用(因为没有进位):
那么现在就可以写程序来求解了:遍历所有数字,每个位上的两进制表示与 AB 进行刚才推导出的函数计算,最终的 B 就是所求的数字。(如果题目更改为 3n + 2 个数字,只有一个数字出现 2 次,则返回 A )
python code
def singleNumber(nums):
a, b = 0, 0
for num in nums:
b = (b ^ num) & ~a
a = (a ^ num) & ~b
return b
卡诺图
刚才我们用的「肉眼」 观察法显然是非正规军。。经过理论验证的方法是卡诺图法(Karnaugh map)。
由 f(B) 的卡诺图:
AB\C | 0 | 1 |
---|---|---|
0 0 | 1 | |
0 1 | 1 | |
1 1 | ||
1 0 |
tip: 注意 AB 和 C 的值变化都只能变动 1 位,如 AB 就不能从 00 变至 11。听起来熟悉吧,这东西叫做格雷码(Grey Code)。
可得出:
f(B) = ~A & B & ~C | ~A & ~B & C
= ~A & ( B & ~C | ~B & C)
= ~A & (B xor C)
由 f(A) 的卡诺图:
AC\f(B) | 0 | 1 |
---|---|---|
0 0 | ||
0 1 | 1 | |
1 1 | ||
1 0 | 1 |
可得出:
f(A) = ~A & C & ~f(B) | A & ~C & ~f(B)
= ~f(B) & (~A & C | A & ~C)
= ~f(B) & (A xor C)
举一反三
如果题目改为:
给出
5n + 1
个数字,除其中一个数字之外其他每个数字均出现 五 次,要求找出这个数字。
尝试着给出解吧 :-)