您可以通过多种方式来思考这个问题,具体取决于您的问题描述的限制条件。
如果你确实知道一个元素是重复的,那么有很多方法可以解决这个问题。一种特别聪明的解决方案是使用按位异或运算符。 XOR 具有以下有趣的特性:
- XOR 是关联的,所以 (x ^ y) ^ z = x ^ (y ^ z)
- XOR 是可交换的:x ^ y = y ^ x
- XOR 是它自己的逆:x ^ y = 0 iff x = y
- XOR 的标识为零:x ^ 0 = x
这里的属性 (1) 和 (2) 意味着当对一组值进行 XOR 时,将 XOR 应用于元素的顺序无关紧要。您可以根据需要重新排序元素或对它们进行分组。属性 (3) 意味着如果你多次异或相同的值,你会得到零,属性 (4) 意味着如果你对任何东西与 0 进行异或,你会得到原来的数字。将所有这些属性放在一起,您会得到一个有趣的结果:如果对一组数字进行 XOR,则结果是该组中出现奇数次的所有数字的 XOR。这样做的原因是,当您对出现偶数次的数字进行异或运算时,您可以将这些数字的异或分解成一组对。每对通过 (3) 异或到 0,并且所有这些零的组合异或通过 (4) 返回零。因此,所有偶重数都被抵消了。
要使用它来解决原始问题,请执行以下操作。首先,将列表中的所有数字异或。这给出了出现奇数次的所有数字的异或,最终是从 1 到 (n-1) 的所有数字,除了重复项。现在,将这个值与从 1 到 (n-1) 的所有数字进行异或运算。然后,这使得 1 到 (n-1) 范围内的所有先前未被取消的数字都被取消,只留下重复的值。此外,这在 O(n) 时间内运行并且仅使用 O(1) 空间,因为所有值的 XOR 都适合单个整数。
在您的原始帖子中,您考虑了一种替代方法,该方法利用从 1 到 n-1 的整数之和为 n(n-1)/2 这一事实。但是,您担心这会导致整数溢出并导致问题。在大多数机器上你是对的,这会导致溢出,但是(在大多数机器上)这不是问题,因为算术是使用固定精度整数完成的,通常是 32 位整数。当发生整数溢出时,得到的数字不是没有意义的。相反,它只是您计算实际结果后得到的值,然后丢弃除最低 32 位之外的所有内容。从数学上讲,这称为模运算,计算机中的运算是模 232 完成的。不过,更一般地,假设整数以某个固定 k 的模 k 存储。
幸运的是,您从普通算术中了解和喜爱的许多算术定律仍然适用于模算术。我们只需要更准确地使用我们的术语。如果 x 和 y 除以 k 时余数相同,则我们说 x 与 y 模 k 一致(表示为 x ≡k y)。这在物理机器上工作时很重要,因为当大多数硬件上发生整数溢出时,结果值与模 k 的真值一致,其中 k 取决于字长。幸运的是,以下定律适用于模算术:
例如:
- 如果 x ≡k y 和 w ≡k z,则 x + w ≡k y + z
- 如果 x ≡k y 和 w ≡k z,则 xw ≡k yz。
这意味着如果您想通过查找数组元素的总和并减去预期的总数来计算重复值,即使存在整数溢出,一切都会正常进行,因为标准算术仍然会产生硬件中的相同值(模 k)。也就是说,您也可以使用基于 XOR 的方法,它根本不需要考虑溢出。 :-)
如果你不能保证一个元素是重复的,但你可以修改元素数组,那么有一个漂亮的算法可以找到重复的值。 This earlier SO question 描述了如何实现这一点。直观地说,您可以尝试使用bucket sort 对序列进行排序,其中元素数组本身也被回收以保存存储桶的空间。
如果你不能保证一个元素是重复的,并且你不能修改元素的数组,那么问题就更难了。这是一个经典的(而且很难!)面试问题,据说 Don Knuth 花了 24 小时才解决。诀窍是将问题简化为cycle-finding 的实例,方法是将数组视为从数字 1-n 到 1-(n-1) 的函数,然后查找该函数的两个输入。然而,生成的算法,称为Floyd's cycle-finding algorithm,非常漂亮和简单。有趣的是,它与您在线性时间和恒定空间中检测链表中的循环所使用的算法相同。我建议您查一下,因为它会定期出现在软件面试中。
有关算法的完整描述以及分析、正确性证明和 Python 实现,请查看解决问题的 this implementation。
希望这会有所帮助!