1. 概述

在这个教程中,我们将探讨合并两个数组(/java-concatenate-arrays)并随后消除重复内容的方法。这类似于一个并集操作:

  • array1 并集 array2

我们将考虑两个整数数组。例如,如果这两个数组是:

  • arr1 = {3, 2, 1, 4, 5, 6, 8, 7, 6, 9}
  • arr2 = {8, 9, 10, 11, 12, 13, 15, 14, 15, 14, 16, 17}

那么结果应该是:

  • mergedArray = {3, 2, 1, 4, 5, 6, 8, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17}

2. 基于基本数组操作的方法

通常,这个需求可以通过使用SetStream API来实现,这些将在后面的章节中解释。但这些库资源消耗较大。因此,我们将更多地关注传统方法,即在遍历数组时使用基本的数组操作。

2.1. 未排序数组的方法

首先,我们定义一个函数来合并数组,然后创建一个方法来去除重复项。另一种方法是先从数组中移除重复项,然后再合并它们。但在性能方面,这不会有太大影响。

让我们从合并数组的方法开始:

static int[] mergeArrays(int[] arr1, int[] arr2) {
    int[] mergedArrays = new int[arr1.length + arr2.length];
    System.arraycopy(arr1, 0, mergedArrays, 0, arr1.length);
    System.arraycopy(arr2, 0, mergedArrays, arr1.length, arr2.length);
    return mergedArrays;
}

在接下来的所有部分,我们将使用上述方法来合并数组。

现在,让我们实现从合并数组中移除重复项的方法:

static int[] removeDuplicate(int[] arr) {
    int[] withoutDuplicates = new int[arr.length];
    int i = 0;

    for (int element : arr) {
        if (!isElementPresent(withoutDuplicates, element)) {
            withoutDuplicates[i] = element;
            i++;
        }
    }
    int[] truncatedArray = new int[i];
    System.arraycopy(withoutDuplicates, 0, truncatedArray, 0, i);
    return truncatedArray;
}

static boolean isElementPresent(int[] arr, int element) {
    for (int el : arr) {
        if (el == element) {
            return true;
        }
    }
    return false;
}

在上述removeDuplicate()方法中,如果没有元素存在于withoutDuplicates数组中,withoutDuplicates数组将只填充合并数组的元素。

通过以上方法,我们可以定义mergeAndRemoveDuplicates()

public static int[] mergeAndRemoveDuplicates(int[] arr1, int[] arr2) {
    return removeDuplicate(mergeArrays(arr1, arr2));
}

让我们看看它的工作原理:

@Test
public void givenNoLibraryAndUnSortedArrays_whenArr1andArr2_thenMergeAndRemoveDuplicates() {
    int[] arr1 = {3, 2, 1, 4, 5, 6, 8, 7, 9};
    int[] arr2 = {8, 9, 10, 11, 12, 13, 15, 14, 15, 14, 16, 17};
    int[] expectedArr = {3, 2, 1, 4, 5, 6, 8, 7, 9, 10, 11, 12, 13, 15, 14, 16, 17};

    int[] mergedArr = MergeArraysAndRemoveDuplicate.mergeAndRemoveDuplicates(arr1, arr2);

    assertArrayEquals(expectedArr, mergedArr);
}

这证明了我们得到了预期的结果。有趣的是,这种方法还保留了数组元素的顺序。

由于移除重复项涉及到比较合并数组中的每个元素与所有其他元素,这种方法的时间复杂度接近O(n x n)。

2.2. 排序数组的方法

假设数组元素已经按顺序排列,并且第二个数组的第一个元素等于或大于第一个数组的最后一个元素。

在这种情况下,我们可以使用以下方法去除重复元素:

public static int[] removeDuplicateOnSortedArray(int[] arr) {
    int[] uniqueArray = new int[arr.length];
    uniqueArray[0] = arr[0];
    int uniqueCount = 1;

    for (int i = 1; i < arr.length; i++) {
        if (arr[i] != arr[i - 1]) {
            uniqueArray[uniqueCount] = arr[i];
            uniqueCount++;
        }
    }
    int[] truncatedArray = new int[uniqueCount];
    System.arraycopy(uniqueArray, 0, truncatedArray, 0, uniqueCount);
    return truncatedArray;
}

这个方法更有效,因为它只比较相邻元素。如果不相等,就将它们添加到uniqueArray数组中。相比之下,先前的isElementPresent()方法会将数组元素与数组中的所有其他元素进行比较。

让我们看看removeDuplicateOnSortedArray()的执行情况:

@Test
public void givenNoLibraryAndSortedArrays_whenArr1andArr2_thenMergeAndRemoveDuplicates() {
    int[] arr1 = {1, 2, 3, 4, 5, 5, 6, 7, 7, 8};
    int[] arr2 = {8, 9, 10, 11, 12, 13, 14, 15, 15, 16, 17};
    int[] expectedArr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17};

    int[] mergedArr = MergeArraysAndRemoveDuplicate.mergeAndRemoveDuplicatesOnSortedArray(arr1, arr2);

    assertArrayEquals(expectedArr, mergedArr);
}

在最坏的情况下,这种方法的时间复杂度为O(n)。

3. 使用Set合并数组

Set不允许重复元素,因此这个特性有助于去除重复项。让我们看看mergeAndRemoveDuplicatesUsingSet()方法:

public static int[] mergeAndRemoveDuplicatesUsingSet(int[] arr1, int[] arr2) {
    int[] mergedArr = mergeArrays(arr1, arr2);
    Set<Integer> uniqueInts = new HashSet<>();

    for (int el : mergedArr) {
        uniqueInts.add(el);
    }

    return getArrayFromSet(uniqueInts);
}

当将数组元素添加到Set uniqueInts时,如果元素已经在其中,就忽略它们。最后,getArrayFromSet()Set转换为数组。

让我们看看这个方法的行为:

@Test
public void givenSet_whenArr1andArr2_thenMergeAndRemoveDuplicates() {
    int[] arr1 = {3, 2, 1, 4, 5, 6, 8, 7, 9};
    int[] arr2 = {8, 9, 10, 11, 12, 13, 15, 14, 15, 14, 16, 17};
    int[] expectedArr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17};

    int[] mergedArr = MergeArraysAndRemoveDuplicate.mergeAndRemoveDuplicatesUsingSet(arr1, arr2);

    assertArrayEquals(expectedArr, mergedArr);
}

显然,数组不再保持元素的原始顺序。

使用这种方法的执行时间取决于数组元素的数量。因此,其时间复杂度为O(n)。

4. 使用Stream合并数组

强大的Stream API提供了合并和从数组中去除重复项的简洁且声明式的方式。让我们查看mergeAndRemoveDuplicatesUsingStream()方法的详细内容:

public static int[] mergeAndRemoveDuplicatesUsingStream(int[] arr1, int[] arr2) {
    Stream<Integer> s1 = Arrays.stream(arr1).boxed();
    Stream<Integer> s2 = Arrays.stream(arr2).boxed();
    return Stream.concat(s1, s2)
      .distinct()
      .mapToInt(Integer::intValue)
      .toArray();
}

首先,将数组单独转换为Stream,然后将数组中的int元素包装为Integer。接着,Stream管道将合并数组并从中删除重复项。

让我们看看它是如何工作的:

@Test
public void givenStream_whenArr1andArr2_thenMergeAndRemoveDuplicates() {
    int[] arr1 = {3, 2, 1, 4, 5, 6, 8, 7, 9};
    int[] arr2 = {8, 9, 10, 11, 12, 13, 15, 14, 15, 14, 16, 17};
    int[] expectedArr = {3, 2, 1, 4, 5, 6, 8, 7, 9, 10, 11, 12, 13, 15, 14, 16, 17};

    int[] mergedArr = MergeArraysAndRemoveDuplicate.mergeAndRemoveDuplicatesUsingStream(arr1, arr2);

    assertArrayEquals(expectedArr, mergedArr);
}

显然,流方法保留了数组中元素的顺序。

决定时间复杂性的主要因素是Stream中的distinct()方法,其时间复杂度为O(n)。总的来说,可以说这种方法的时间复杂度为O(n)。

5. 结论

在这篇教程中,我们探讨了合并两个数组并移除重复项的不同方法。最简洁的方法使用了Stream API。Set不保留插入元素的顺序,但它非常有效地去除了重复项。其他两种方法保留了数组中元素的顺序。

值得注意的是,StreamSet资源消耗较大,因此在可能的情况下应尽量避免使用它们。

如往常一样,代码示例可在GitHub上找到