算法题背后的数学之 136. Single Number
题目链接: 136. Single Number
这道题的一个经典解法是对输入 nums 中所有的数字进行异或运算,得到的结果就是要找的数字。示例 java 代码如下 ⬇️
class Solution {
public int singleNumber(int[] nums) {
int ans = 0;
for (int num : nums) {
ans = ans ^ num;
}
return ans;
}
}
但是为什么这样就能得到正确答案呢?
解释
我们先来看一下异或运算的结果是如何分布的。
异或运算的真值表
异或运算的真值表如下(这里用⊕来表示异或运算)
如果是 3 个 1 bit 数进行异或运算的话,有 8 种情况,具体如下 ⬇️
通过观察这些计算结果,会发现
- 异或运算满足交换律,可能也满足结合律(待验证)➡️ 能否在交换律和结合律的基础上来证明开头的解法正确呢?
- 异或运算的结果似乎只和参与计算的数中
1的个数有关 ➡️ 能否严格证明这个猜测呢?
我们分别在这两个发现的基础上进行证明
证明思路 1: 基于 交换律/结合律 的证明
我们对加法和乘法非常熟悉,它们都满足交换律和结合律。
以整数加法为例,
- 它满足交换律是指,对任意两个整数 , 而言, 总是成立。
- 它满足结合律是指,对任意三个整数 , , 而言, 总是成立。
异或运算似乎也满足交换律和结合律。 我们尝试证明或者找到反例来证伪。
证明异或运算满足交换律
观察异或运算的 真值表 后,可以得出,对任意的 1 bit 数 , , 都有 。
由于 和 的取值只能是 或 , 一共只有 种情况,逐个验证即可证明。
证明异或运算满足结合律
现在需要证明,对任意 1 bit 数 , , ,都有 。
由于 , , 只有 种可能的取值组合,我们逐一验证即可。
具体过程如下 ⬇️
种情况下 都成立,所以异或运算满足结合律。
在证明了异或运算满足交换律和结合律之后,我们可以推出一个更好用的结论 ⬇️
结论 1 及其证明
结论 1: 个 () 1 bit 数进行异或运算时,更换任意两个数的位置,最终的结果都不变。
为了便于画图,我们以 为例来进行说明(但其实对任意的 ,结论都是成立的)。
都是 1 bit 的数,我们要对这 个数字进行异或运算,算式是
可以把这个算式的计算步骤转化成一个树形的结构 ⬇️
比如说我们想交换 和 的位置,那么就要证明以下两者的计算结果相等
注意到以上两个算式可以写成
如果我们能证明以下两者 ⬇️ 的计算结果相等,那么在两者的最后都加上 ,运算结果自然还是相等的。
为了便于描述,我们记
通过运用交换律和结合律,可以得到 ⬇️
graph LR
subgraph g1["graph 1"]
n1(("⊕")) --> X
n1 --> Y
n2(("⊕")) --> Z
n2 --> n1
end
subgraph g2["graph 2"]
n2_1(("⊕")) --> Y2["Y"]
n2_1 --> X2["X"]
n2_2(("⊕")) --> n2_1
n2_2 --> Z2["Z"]
end
subgraph g3["graph 3"]
n3_1(("⊕")) --> Y3["Y"]
n3_1 --> n3_2(("⊕"))
n3_2 --> X3["X"]
n3_2 --> Z3["Z"]
end
subgraph g4["graph 4"]
n4_1(("⊕")) --> X4["X"]
n4_1 --> Z4["Z"]
n4_2(("⊕")) --> n4_1
n4_2 --> Y4["Y"]
end
g1 --> |应用交换律| g2
g2 --> |应用结合律| g3
g3 --> |应用交换律| g4
为了便于描述, 我们将 称为 结论 2
结论 2 是什么意思呢,从异或运算构成的树来说,每个节点都可以和它左下方的那个节点进行交换(如果左下方有节点的话)。
利用 结论 2,可以得到
反复运用 结论 2,可以得到
这样说比较抽象,画成图是这样的 ⬇️,其实就是反复把 和它左下方的那个节点进行交换。
现在 已经移动到预期的位置了,我们再来处理 。我们还是使用结论 2, 现在将 和 交换一次,再将 和 交换一次,此时 就会到预期的位置了 ⬇️
由于 的位置不再变化,所以上图中把 节点统一标记成黄颜色了。
虽然上面的几张图讲的是一个特例,但是不难想到,对一般的情形而言,这个移动过程也是成立的。 这样我们用结论 2证明了结论 1是正确的。 我把结论 1再写一遍 ⬇️
结论 1: 个 ()
1 bit数进行异或运算时,更换任意两个数的位置,最终的结果都不变。
请注意,在证明结论 1的过程中,我们只用到了以下两个事实。
- 异或运算满足结合律
- 异或运算满足交换律
所以对任意满足交换律和结合律的二元运算符 ,我们都可以得到类似的结果 ⬇️
结论 1 的推广
更通用的结论:如果一个二元运算符 既满足结合律又满足交换律,那么对 个 () 数进行 运算时,更换任意两个数的位置,最终的结果都不变。
例如整数上的加法既满足结合律也满足交换律,所以
这类的加法小技巧我们在小学时遇到过不少。
我们还回到异或运算,假设一共有 个 1 bit 数进行异或运算,其中有 个取值为 ,我们反复利用结论 1,就可以把这 个 移动到算式的左端 ⬇️
然后再反复利用交换律,就可以把这 个 两两配对,算式变为以下两种情况之一。
如果 是奇数,算式变为 ⬇️
如果 是偶数,算式变为 ⬇️
由于 ,所以
- 当 是奇数时,结果为
- 当 是偶数时,结果为
回到题目 136. Single Number,
假设只出现一次的那个数字是 ,那么对任意一个 bit 而言,
- 如果 在这个
bit为 ,那么所有数字异或之后,在这个bit上的运算结果也会是 (因为这bit上 的出现次数一定是偶数)。 - 如果 在这个
bit为 ,那么所有数字异或之后,在这个bit上的运算结果也会是 因为这bit上 的出现次数一定是奇数)。
所以计算结果和 在每个 bit 上都相等,那么 就等于最终的计算结果了。
证毕。
证明思路 2: 数学归纳法
观察 异或运算的真值表 中的数据后,我们发现,异或运算的结果是否为 1,似乎与参与运算的数中的 1 的个数有关系。
看起来是这样的 ⬇️
“奇偶性猜测”及其证明
我们把参与异或运算的数中, 的出现次数记为 。
- 如果 是奇数,则结果为
- 如果 是偶数,则结果为
我们把它称为“奇偶性猜测”吧。
我们可以试试用数学归纳法来证明它(或者尝试找到反例来证伪它)。
的情况
如果参与运算的数中没有 ,那么整个算式会是这样 ⬇️
由于 ,反复应用它,就能得到整个算式等于 ,所以 时,“奇偶性猜测”成立。
的情况
如果参与运算的数中恰有 个 ,那么整个算式会是这样 ⬇️
假设这个 左边有 个 ,右边有 个 (注意: 和 都允许是 )。
为了便于画图说明,这里随便举个例子,比如说 , 吧,完整的计算过程如下 ⬇️
不难看出,前面 个异或的运算结果是 ,而 ,之后的 个异或运算都是 ,所以最终结果一定 。 所以当 ( 表示参与运算的数中 的个数)时,“奇偶性猜测”是正确的。
的情况
时,整个算式会是这样 ⬇️
刚才已经分析过 的情况了,所以下图红色框里的运算结果是 。
而 ,所以下图黄色框里的运算结果是
考虑到 ,反复应用它,就会得到整个算式的结果为 。
的情况
这样说来 时,“奇偶性猜测”也成立,同理可证 ,,... 时,“奇偶性猜测”都成立,所以对任意自然数 ,“奇偶性猜测”都成立。
回到题目 136. Single Number,因为只有一个数字恰出现了 次,为了便于表述,我们以这个数字 为例来进行说明( 具体是多少并不重要),我们把 所对应的 bit 分别称为 ,最低位是 ( 都是 )。
- : 所有参与运算的数中, 的数字一定是奇数个(因为 的 是 ,而其他数字都是成对出现的)
- : 所有参与运算的数中, 的数字一定是奇数个(因为 的 是 ,而其他数字都是成对出现的)
- : 所有参与运算的数中, 的数字一定是偶数个(因为 的 是 ,而其他数字都是成对出现的)
- : 所有参与运算的数中, 为 的数字一定是偶数个(因为 的 是 ,而其他数字都是成对出现的)
- : 所有参与运算的数中, 为 的数字一定是奇数个(因为 的 是 ,而其他数字都是成对出现的)
- : 所有参与运算的数中, 为 的数字一定是偶数个(因为 的 是 ,而其他数字都是成对出现的)
- ...
- : 所有参与运算的数中, 为 的数字一定是偶数个(因为 的 是 ,而其他数字都是成对出现的)
所以通过让 nums 中的所有数字都参与异或运算,就可以得到正确结果。