算法题背后的数学之 136. Single Number

125 阅读1分钟

算法题背后的数学之 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;
    }
}

但是为什么这样就能得到正确答案呢?

解释

我们先来看一下异或运算的结果是如何分布的。

异或运算的真值表

异或运算的真值表如下(这里用来表示异或运算)

aabbaba ⊕ b
000000
001111
110011
111100

如果是 31 bit 数进行异或运算的话,有 8 种情况,具体如下 ⬇️

  • 000=00=00 ⊕ 0 ⊕ 0 = 0 ⊕ 0 = 0
  • 001=01=10 ⊕ 0 ⊕ 1 = 0 ⊕ 1 = 1
  • 010=10=10 ⊕ 1 ⊕ 0 = 1 ⊕ 0 = 1
  • 011=11=00 ⊕ 1 ⊕ 1 = 1 ⊕ 1 = 0
  • 100=10=11 ⊕ 0 ⊕ 0 = 1 ⊕ 0 = 1
  • 101=11=01 ⊕ 0 ⊕ 1 = 1 ⊕ 1 = 0
  • 110=00=01 ⊕ 1 ⊕ 0 = 0 ⊕ 0 = 0
  • 111=01=11 ⊕ 1 ⊕ 1 = 0 ⊕ 1 = 1

通过观察这些计算结果,会发现

  • 异或运算满足交换律,可能也满足结合律(待验证)➡️ 能否在交换律结合律的基础上来证明开头的解法正确呢?
  • 异或运算的结果似乎只和参与计算的数中 1 的个数有关 ➡️ 能否严格证明这个猜测呢?

我们分别在这两个发现的基础上进行证明

证明思路 1: 基于 交换律/结合律 的证明

我们对加法和乘法非常熟悉,它们都满足交换律结合律

以整数加法为例,

  • 它满足交换律是指,对任意两个整数 aa, bb 而言,a+b=b+aa + b = b + a 总是成立。
  • 它满足结合律是指,对任意三个整数 aa, bb, cc 而言, (a+b)+c=a+(b+c)(a + b) + c = a + (b + c) 总是成立。

异或运算似乎也满足交换律结合律。 我们尝试证明或者找到反例来证伪。

证明异或运算满足交换律

观察异或运算的 真值表 后,可以得出,对任意的 1 bitaa, bb, 都有 ab=baa ⊕ b = b ⊕ a。 由于 aabb 的取值只能是 0011aba ⊕ b 一共只有 44 种情况,逐个验证即可证明。

证明异或运算满足结合律

现在需要证明,对任意 1 bitaa, bb, cc,都有 (ab)c=a(bc)(a ⊕ b) ⊕ c = a ⊕ (b ⊕ c)

由于 aa, bb, cc 只有 88 种可能的取值组合,我们逐一验证即可。

具体过程如下 ⬇️

  1. (00)0=00=0(00)(0 ⊕ 0) ⊕ 0 = 0 ⊕ 0 = 0 ⊕ (0 ⊕ 0)
  2. (00)1=01=0(01)(0 ⊕ 0) ⊕ 1 = 0 ⊕ 1 = 0 ⊕ (0 ⊕ 1)
  3. (01)0=10=01=0(10)(0 ⊕ 1) ⊕ 0 = 1 ⊕ 0 = 0 ⊕ 1 = 0 ⊕ (1 ⊕ 0)
  4. (01)1=11=0=00=0(11)(0 ⊕ 1) ⊕ 1 = 1 ⊕ 1 = 0 = 0 ⊕ 0 = 0 ⊕ (1 ⊕ 1)
  5. (10)0=10=1(00)(1 ⊕ 0) ⊕ 0 = 1 ⊕ 0 = 1 ⊕ (0 ⊕ 0)
  6. (10)1=11=1(01)(1 ⊕ 0) ⊕ 1 = 1 ⊕ 1 = 1 ⊕ (0 ⊕ 1)
  7. (11)0=00=0=11=1(10)(1 ⊕ 1) ⊕ 0 = 0 ⊕ 0 = 0 = 1 ⊕ 1 = 1 ⊕ (1 ⊕ 0)
  8. (11)1=01=10=1(11)(1 ⊕ 1) ⊕ 1 = 0 ⊕ 1 = 1 ⊕ 0 = 1 ⊕ (1 ⊕ 1)

88 种情况下 (ab)c=a(bc)(a ⊕ b) ⊕ c = a ⊕ (b ⊕ c) 都成立,所以异或运算满足结合律

在证明了异或运算满足交换律结合律之后,我们可以推出一个更好用的结论 ⬇️

结论 1 及其证明

结论 1: nn 个 (n2n\ge2) 1 bit 数进行异或运算时,更换任意两个数的位置,最终的结果都不变。

为了便于画图,我们以 n=7n = 7 为例来进行说明(但其实对任意的 n2n\ge2,结论都是成立的)。 a,b,c,d,e,f,ga, b, c, d, e, f, g 都是 1 bit 的数,我们要对这 77 个数字进行异或运算,算式是

abcdefga ⊕ b ⊕ c ⊕ d ⊕ e ⊕ f ⊕ g

可以把这个算式的计算步骤转化成一个树形的结构 ⬇️

image.png

比如说我们想交换 ccff 的位置,那么就要证明以下两者的计算结果相等

  • abcdefga ⊕ b ⊕ c ⊕ d ⊕ e ⊕ f ⊕ g
  • abfdecga ⊕ b ⊕ f ⊕ d ⊕ e ⊕ c ⊕ g

注意到以上两个算式可以写成

  • (abcdef)g(a ⊕ b ⊕ c ⊕ d ⊕ e ⊕ f) ⊕ g
  • (abfdec)g(a ⊕ b ⊕ f ⊕ d ⊕ e ⊕ c) ⊕ g

如果我们能证明以下两者 ⬇️ 的计算结果相等,那么在两者的最后都加上 g⊕ g,运算结果自然还是相等的。

  • abcdefa ⊕ b ⊕ c ⊕ d ⊕ e ⊕ f
  • abfdeca ⊕ b ⊕ f ⊕ d ⊕ e ⊕ c

为了便于描述,我们记

  • X=abcdX = a ⊕ b ⊕ c ⊕ d
  • Y=eY = e
  • Z=fZ = f

通过运用交换律结合律,可以得到 (XY)Z=(XZ)Y(X ⊕ Y) ⊕ Z = (X ⊕ Z) ⊕ Y ⬇️

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

为了便于描述, 我们将 (XY)Z=(XZ)Y(X ⊕ Y) ⊕ Z = (X ⊕ Z) ⊕ Y 称为 结论 2

结论 2 是什么意思呢,从异或运算构成的树来说,每个节点都可以和它左下方的那个节点进行交换(如果左下方有节点的话)。

image.png

利用 结论 2,可以得到

abcdef=abcdfea ⊕ b ⊕ c ⊕ d ⊕ e ⊕ f = a ⊕ b ⊕ c ⊕ d ⊕ f ⊕ e

image.png

反复运用 结论 2,可以得到

abcdfe=abcfdea ⊕ b ⊕ c ⊕ d ⊕ f ⊕ e = a ⊕ b ⊕ c ⊕ f ⊕ d ⊕ e
abcfde=abfcdea ⊕ b ⊕ c ⊕ f ⊕ d ⊕ e = a ⊕ b ⊕ f ⊕ c ⊕ d ⊕ e

这样说比较抽象,画成图是这样的 ⬇️,其实就是反复把 ff 和它左下方的那个节点进行交换。

image.png

现在 ff 已经移动到预期的位置了,我们再来处理 cc。我们还是使用结论 2, 现在将 ccdd 交换一次,再将 ccee 交换一次,此时 cc 就会到预期的位置了 ⬇️

image.png

由于 ff 的位置不再变化,所以上图中把 ff 节点统一标记成黄颜色了。

虽然上面的几张图讲的是一个特例,但是不难想到,对一般的情形而言,这个移动过程也是成立的。 这样我们用结论 2证明了结论 1是正确的。 我把结论 1再写一遍 ⬇️

结论 1: nn 个 (n2n\ge2) 1 bit 数进行异或运算时,更换任意两个数的位置,最终的结果都不变。

请注意,在证明结论 1的过程中,我们只用到了以下两个事实。

  • 异或运算满足结合律
  • 异或运算满足交换律

所以对任意满足交换律和结合律的二元运算符 ff,我们都可以得到类似的结果 ⬇️

结论 1 的推广

更通用的结论:如果一个二元运算符 ff 既满足结合律又满足交换律,那么对 nn 个 (n2n\ge2) 数进行 ff 运算时,更换任意两个数的位置,最终的结果都不变。

例如整数上的加法既满足结合律也满足交换律,所以

73+91+27+9=73+27+91+9=100+91+9=100+(91+9)=100+100=20073 + 91 + 27 + 9 = 73 + 27 + 91 + 9 = 100 + 91 + 9 = 100 + (91 + 9) = 100 + 100 = 200

这类的加法小技巧我们在小学时遇到过不少。

我们还回到异或运算,假设一共有 nn1 bit 数进行异或运算,其中有 mm 个取值为 11,我们反复利用结论 1,就可以把这 mm11 移动到算式的左端 ⬇️

111...100...01 ⊕ 1 ⊕ 1 ... ⊕ 1 ⊕ 0 ⊕ 0 ... ⊕ 0

然后再反复利用交换律,就可以把这 mm11 两两配对,算式变为以下两种情况之一。

如果 mm 是奇数,算式变为 ⬇️

(11)(11)(11)...100...0(1 ⊕ 1) ⊕ (1 ⊕ 1) ⊕ (1 ⊕ 1) ... ⊕ 1 ⊕ 0 ⊕ 0 ... ⊕ 0

如果 mm 是偶数,算式变为 ⬇️

(11)(11)...(11)00...0(1 ⊕ 1) ⊕ (1 ⊕ 1) ... ⊕ (1 ⊕ 1) ⊕ 0 ⊕ 0 ... ⊕ 0

由于 11=01 ⊕ 1 = 0,所以

  • mm 是奇数时,结果为 11
  • mm 是偶数时,结果为 00

回到题目 136. Single Number, 假设只出现一次的那个数字是 XX,那么对任意一个 bit 而言,

  • 如果 XX 在这个 bit00,那么所有数字异或之后,在这个 bit 上的运算结果也会是 00(因为这 bit11 的出现次数一定是偶数)。
  • 如果 XX 在这个 bit11,那么所有数字异或之后,在这个 bit 上的运算结果也会是 11 因为这 bit11 的出现次数一定是奇数)。

所以计算结果和 XX 在每个 bit 上都相等,那么 XX 就等于最终的计算结果了。

证毕。

证明思路 2: 数学归纳法

观察 异或运算的真值表 中的数据后,我们发现,异或运算的结果是否为 1,似乎与参与运算的数中的 1 的个数有关系。 看起来是这样的 ⬇️

“奇偶性猜测”及其证明

我们把参与异或运算的数中, 11 的出现次数记为 kk

  • 如果 kk 是奇数,则结果为 11
  • 如果 kk 是偶数,则结果为 00

我们把它称为“奇偶性猜测”吧。

我们可以试试用数学归纳法来证明它(或者尝试找到反例来证伪它)。

k=0k = 0 的情况

如果参与运算的数中没有 11,那么整个算式会是这样 ⬇️

000...000 ⊕ 0 ⊕ 0 ... 0 ⊕ 0

由于 00=00 ⊕ 0 = 0,反复应用它,就能得到整个算式等于 00,所以 k=0k = 0 时,“奇偶性猜测”成立。

k=1k = 1 的情况

如果参与运算的数中恰有 1111,那么整个算式会是这样 ⬇️

000....1...000 ⊕ 0 ⊕ 0 .... 1 ... 0 ⊕ 0

假设这个 11 左边有 mm00,右边有 nn00 (注意: mmnn 都允许是 00)。

为了便于画图说明,这里随便举个例子,比如说 m=5m = 5n=2n = 2 吧,完整的计算过程如下 ⬇️

image.png

不难看出,前面 mm 个异或的运算结果是 11,而 10=11 ⊕ 0 = 1,之后的 nn 个异或运算都是 10=11 ⊕ 0 = 1,所以最终结果一定 11。 所以当 k=1k = 1kk 表示参与运算的数中 11 的个数)时,“奇偶性猜测”是正确的。

k=2k = 2 的情况

k=2k = 2 时,整个算式会是这样 ⬇️

000....1...1...000 ⊕ 0 ⊕ 0 .... 1 ... ⊕ 1 ... 0 ⊕ 0

刚才已经分析过 k=1k = 1 的情况了,所以下图红色框里的运算结果是 11

image.png

11=01 ⊕ 1 = 0,所以下图黄色框里的运算结果是 00

image.png

考虑到 00=00 ⊕ 0 = 0,反复应用它,就会得到整个算式的结果为 00

k>2k > 2 的情况

这样说来 k=2k = 2 时,“奇偶性猜测”也成立,同理可证 k=3k = 3k=4k = 4,... 时,“奇偶性猜测”都成立,所以对任意自然数 kk,“奇偶性猜测”都成立。

回到题目 136. Single Number,因为只有一个数字恰出现了 11 次,为了便于表述,我们以这个数字 x=(二进制的)10011x = (二进制的) 10011 为例来进行说明(xx 具体是多少并不重要),我们把 1001110011 所对应的 bit 分别称为 b4,b3,b2,b1,b0b_4, b_3, b_2, b_1, b_0,最低位是 b0b_0b31,b30,...,b6,b5b_{31}, b_{30},..., b_6, b_5 都是 00)。

  • b0b_0: 所有参与运算的数中,b0=1b_0 = 1 的数字一定是奇数个(因为 xxb0b_011,而其他数字都是成对出现的)
  • b1b_1: 所有参与运算的数中,b1=1b_1 = 1 的数字一定是奇数个(因为 xxb1b_111,而其他数字都是成对出现的)
  • b2b_2: 所有参与运算的数中,b2=1b_2 = 1 的数字一定是偶数个(因为 xxb2b_200,而其他数字都是成对出现的)
  • b3b_3: 所有参与运算的数中,b3b_311 的数字一定是偶数个(因为 xxb3b_300,而其他数字都是成对出现的)
  • b4b_4: 所有参与运算的数中,b4b_411 的数字一定是奇数个(因为 xxb4b_411,而其他数字都是成对出现的)
  • b5b_5: 所有参与运算的数中,b5b_511 的数字一定是偶数个(因为 xxb5b_500,而其他数字都是成对出现的)
  • ...
  • b31b_{31}: 所有参与运算的数中,b31b_{31}11 的数字一定是偶数个(因为 xxb31b_{31}00,而其他数字都是成对出现的)

所以通过让 nums 中的所有数字都参与异或运算,就可以得到正确结果。