让给定的数组是长度为 N 的 A。假设在给定的数组中,单个空槽被 0 填充。
我们可以使用许多方法找到这个问题的解决方案,包括Counting sort 中使用的算法。但是,就有效的时间和空间使用而言,我们有两种算法。一种主要使用加法、减法和乘法。另一个使用 XOR。从数学上讲,这两种方法都可以正常工作。但在编程上,我们需要使用主要措施评估所有算法,例如
- 限制(如输入值很大 (
A[1...N]) 和/或
输入值很大(N))
- 涉及的条件检查次数
- 涉及的数学运算的数量和类型
等等。这是因为时间和/或硬件(硬件资源限制)和/或软件(操作系统限制,编程语言限制等)等方面的限制。让我们列出并评估它们各自的优缺点。
算法一:
在算法 1 中,我们有 3 个实现。
-
使用数学公式(1+2+3+...+N=(N(N+1))/2)计算所有数字的总和(包括未知的缺失数字)。在这里,N=100。计算所有给定数字的总和。从第一个结果中减去第二个结果将得到缺失的数字。
Missing Number = (N(N+1))/2) - (A[1]+A[2]+...+A[100])
-
使用数学公式(1+2+3+...+N=(N(N+1))/2)计算所有数字的总和(包括未知的缺失数字)。在这里,N=100。从该结果中,减去每个给定的数字会得到缺失的数字。
Missing Number = (N(N+1))/2)-A[1]-A[2]-...-A[100]
(Note:虽然第二个实现的公式是从第一个派生的,但从数学的角度来看,两者是相同的。但从编程的角度来看,两者是不同的,因为第一个公式比第二个更容易发生位溢出一个(如果给定的数字足够大)。即使加法比减法快,第二种实现减少了由大值相加引起的位溢出的机会(它没有完全消除,因为仍然存在很小的机会,因为(@ 987654331@) 在公式中)。但是两者都同样容易通过乘法发生位溢出。限制是两个实现只有在N(N+1)<=MAXIMUM_NUMBER_VALUE 时才能给出正确的结果。对于第一个实现,额外的限制是它只有在以下情况下才能给出正确的结果Sum of all given numbers<=MAXIMUM_NUMBER_VALUE.)
-
计算所有数字的总和(包括未知的缺失数字)并在同一循环中并行减去每个给定数字。这消除了乘法位溢出的风险,但加减法容易发生位溢出。
//ALGORITHM
missingNumber = 0;
foreach(index from 1 to N)
{
missingNumber = missingNumber + index;
//Since, the empty slot is filled with 0,
//this extra condition which is executed for N times is not required.
//But for the sake of understanding of algorithm purpose lets put it.
if (inputArray[index] != 0)
missingNumber = missingNumber - inputArray[index];
}
在编程语言(如 C、C++、Java 等)中,如果表示整数数据类型的位数是有限的,那么上述所有实现都容易因为求和、减法和乘法而发生位溢出,导致在输入值较大(A[1...N])和/或输入值较大(N)的情况下会出现错误结果。
算法二:
我们可以使用异或的性质来解决这个问题,而不用担心位溢出的问题。而且 XOR 比求和更安全、更快。我们知道异或的性质,即两个相同数的异或等于 0(A XOR A = 0)。如果我们计算从 1 到 N 的所有数字的 XOR(这包括未知的缺失数字),然后根据这个结果,对所有给定的数字进行 XOR,共同的数字被抵消(因为A XOR A=0)最后我们得到丢失的号码。如果我们没有位溢出问题,我们可以使用求和和基于 XOR 的算法来获得解决方案。但是,使用异或的算法比使用加法、减法和乘法的算法更安全、更快。并且可以避免加减乘法带来的额外烦恼。
在算法1的所有实现中,我们都可以使用异或来代替加减法。
假设,XOR(1...N) = XOR of all numbers from 1 to N
实施 1 => Missing Number = XOR(1...N) XOR (A[1] XOR A[2] XOR...XOR A[100])
实施 2 => Missing Number = XOR(1...N) XOR A[1] XOR A[2] XOR...XOR A[100]
实施 3 =>
//ALGORITHM
missingNumber = 0;
foreach(index from 1 to N)
{
missingNumber = missingNumber XOR index;
//Since, the empty slot is filled with 0,
//this extra condition which is executed for N times is not required.
//But for the sake of understanding of algorithm purpose lets put it.
if (inputArray[index] != 0)
missingNumber = missingNumber XOR inputArray[index];
}
算法 2 的所有三个实现都可以正常工作(从编程的角度来看也是如此)。一种优化是,类似于
1+2+....+N = (N(N+1))/2
我们有,
1 XOR 2 XOR .... XOR N = {N if REMAINDER(N/4)=0, 1 if REMAINDER(N/4)=1, N+1 if REMAINDER(N/4)=2, 0 if REMAINDER(N/4)=3}
我们可以通过数学归纳法证明这一点。因此,我们可以使用这个公式来减少 XOR 操作的次数,而不是通过 XOR 来计算 XOR(1...N) 的值。
此外,使用上述公式计算 XOR(1...N) 有两种实现方式。实施明智,计算
// Thanks to https://a3nm.net/blog/xor.html for this implementation
xor = (n>>1)&1 ^ (((n&1)>0)?1:n)
比计算快
xor = (n % 4 == 0) ? n : (n % 4 == 1) ? 1 : (n % 4 == 2) ? n + 1 : 0;
所以,优化后的 Java 代码是,
long n = 100;
long a[] = new long[n];
//XOR of all numbers from 1 to n
// n%4 == 0 ---> n
// n%4 == 1 ---> 1
// n%4 == 2 ---> n + 1
// n%4 == 3 ---> 0
//Slower way of implementing the formula
// long xor = (n % 4 == 0) ? n : (n % 4 == 1) ? 1 : (n % 4 == 2) ? n + 1 : 0;
//Faster way of implementing the formula
// long xor = (n>>1)&1 ^ (((n&1)>0)?1:n);
long xor = (n>>1)&1 ^ (((n&1)>0)?1:n);
for (long i = 0; i < n; i++)
{
xor = xor ^ a[i];
}
//Missing number
System.out.println(xor);