1. 概述

在Java中,从指定范围内的数组(如:整数数组)中找出缺失的数字在各种场景下都非常有用,比如数据验证、确保完整性和识别数据集中存在的空缺。

在这个教程中,我们将学习如何使用多种方法从一个整数范围内([1-N])的数组中找到单个缺失的数字

2. 理解场景

设想我们有一个包含[1-9]范围内的整数的数组numbers

int[] numbers = new int[] { 1, 4, 5, 2, 7, 8, 6, 9 };

目标是在数组[1-9]范围内找到缺失的那个数字

为了概括问题陈述,我们可以计算数组的长度并设置上限N

int N = numbers.length + 1;

接下来,我们会了解不同的方法来找到给定数组中范围[1-N]内缺失的数字。

3. 使用算术和

首先,我们用算术和的方法来找numbers数组中的缺失数字。

首先,计算[1-N]范围内的等差数列预期总和以及数组的实际总和:

int expectedSum = (N * (N + 1)) / 2;
int actualSum = Arrays.stream(numbers).sum();

然后,从expectedSum中减去actualSum得到missingNumber

int missingNumber = expectedSum - actualSum;

最后,检查结果:

assertEquals(3, missingNumber);

结果正确!

4. 使用异或性质

另一种方法是利用异或操作(^)的两个有趣特性:

  • X^X = 0: 当一个数与自身异或,结果为0。
  • X^0 = X: 当一个数与0异或,结果还是原来的数。

首先,使用reduce函数对闭区间[1-9]内的所有整数值进行异或操作:

int xorValue = IntStream.rangeClosed(1, N).reduce(0, (a, b) -> a ^ b);

我们使用了0和(a, b) -> a ^ b作为reduce()操作的身份和累积器

接着,将异或操作扩展到numbers数组的整数值:

xorValue = Arrays.stream(numbers).reduce(xorValue, (x, y) -> x ^ y);

由于除了缺失的数字外,数组中的每个数字都会出现两次,所以xorValue将只包含数组[1-9]范围内numbers中的缺失数字。

最后,确认我们的方法给出正确结果:

assertEquals(3, xorValue);

太好了!我们做得对。

5. 使用排序

我们的输入数组numbers应包含范围[1-N]内连续的所有值,除了缺失的那个。因此,如果排序数组,会很容易发现缺失的数字,因为连续的数字之间会有空缺。

首先,对numbers数组进行排序:

Arrays.sort(numbers);

接着,遍历数组,检查每个值是否是其索引加1:

int missingNumber = -1;
for (int index = 0; index < numbers.length; index++) {
    if (numbers[index] != index + 1) {
        missingNumber = index + 1;
        break;
    }
}

当条件不满足时,意味着数组中缺少值index + 1。于是,设置missingNumber并提前结束循环。

最后,检查我们是否得到了期望的结果:

assertEquals(3, missingNumber);

结果看起来正确。然而,在这个方法中,我们修改了原始输入数组

6. 用布尔数组跟踪

在排序方法中,有两个主要缺点:

  • 排序的开销
  • 原始输入数组的修改

我们可以通过使用布尔数组来跟踪当前的数字来解决这些问题。

首先,定义一个大小为N的布尔数组present

boolean[] present = new boolean[N];

我们需要记住N被初始化为numbers.length + 1

接下来,遍历numbers数组,在present数组中标记每个数字的存在:

int missingNumber = -1;
Arrays.stream(numbers).forEach(number -> present[number - 1] = true);

然后,再次遍历present数组,查找未被标记的数字:

for (int index = 0; index < present.length; index++) {
    if (!present[index]) {
        missingNumber = index + 1;
        break;
    }
}

最后,验证我们的方法,检查missingNumber变量的值:

assertEquals(3, missingNumber);

完美!我们的方法有效。此外,我们使用了额外的N字节空间,因为每个布尔值在Java中占用1字节。

7. 使用位集优化空间复杂度

通过使用位集(Bitset),我们可以进一步优化空间复杂度。

BitSet bitSet = new BitSet(N);

这样初始化后,我们只需足够空间来表示N位。当N值很大时,这是一个相当可观的优化。

接下来,遍历numbers数组,在位集bitset中相应位置设置位:

for (int num : numbers) {
    bitSet.set(num);
}

现在,我们可以通过检查未设置的位来找到缺失的数字:

int missingNumber = bitSet.nextClearBit(1);

最后,确认我们得到了正确的missingNumber值:

assertEquals(3, missingNumber);

太棒了!看来我们成功了。

8. 总结

在这个教程中,我们学会了如何在数组中找到缺失的数字。我们探讨了多种方法来解决这个问题,例如算术和、异或操作、排序以及额外的数据结构,如位集和布尔数组。

如往常一样,本文的代码可以在GitHub上找到。