1. 概述

在Java中,ArrayList 是常用的 List 实现之一。

本教程将探讨如何反转一个 ArrayList

2. 问题介绍

我们通过一个例子来理解这个问题。假设我们有一个 Integer 类型的 List

​List<Integer> aList = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7));

反转后,预期结果如下:

List<Integer> EXPECTED = new ArrayList<>(Arrays.asList(7, 6, 5, 4, 3, 2, 1));

所以,需求看起来相当直接。然而,问题可能有几种变体:

  • 在原地反转 List
  • 反转 List 并以新的 List 对象返回结果

本教程将涵盖这两种情况。

Java标准库提供了一个辅助方法来完成这项工作。我们将看到如何使用这个方法快速解决问题。

此外,考虑到一些人可能正在学习Java,我们将探讨两种有趣但高效的反转 List 的实现方式。

接下来,让我们动手实践。

3. 使用标准 Collections.reverse 方法

Java标准库提供了 [Collections.reverse](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Collections.html#reverse(java.util.List)) 方法,用于反转给定 List 中元素的顺序。

这个方便的方法实现了原地反转,它会直接改变接收的 List 的顺序。首先,我们创建一个单元测试方法来理解其工作原理:

List<Integer> aList = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7));
Collections.reverse(aList);
assertThat(aList).isEqualTo(EXPECTED);

当执行上述测试时,它会通过。如我们所见,我们将 aList 对象传递给了 reverse 方法,然后 aList 中元素的顺序就被反转了。

如果我们不想改变原始 List,并期望得到一个新的包含反转顺序元素的 List 对象,我们可以将一个新的 List 对象传递给 reverse 方法:

List<Integer> originalList = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7));
List<Integer> aNewList = new ArrayList<>(originalList);
Collections.reverse(aNewList);

assertThat(aNewList).isNotEqualTo(originalList).isEqualTo(EXPECTED);

这样,我们保持 originalList 不变,而 aNewList 中元素的顺序被反转。

从上面两个示例中,我们可以看出标准的 Collections.reverse 方法非常适合反转 List

然而,如果我们在学习Java,可能希望亲自练习实现一个“reverse”方法。接下来,我们将探索两种不错的实现:一种使用递归,另一种使用简单的循环。

4. 递归方式反转 List

首先,让我们使用递归技术实现自己的列表反转方法。先来看看实现:

public static <T> void reverseWithRecursion(List<T> list) {
    if (list.size() > 1) {
        T value = list.remove(0);
        reverseWithRecursion(list);
        list.add(value);
    }
}

如我们所见,上述实现看起来相当紧凑。现在,让我们理解它是如何工作的。

递归逻辑的停止条件是 list.size() <= 1。换句话说,如果 list 对象为空或仅包含一个元素,我们就停止递归

在每次递归调用中,我们执行“T value = list.remove(0)”,从列表中弹出第一个元素。它的工作原理如下:

recursion step 0: value = null, list = (1, 2, 3, ... 7)
   |_ recursion step 1: value = 1, list = (2, 3, 4,...7)
      |_ recursion step 2: value = 2, list = (3, 4, 5, 6, 7)
         |_ recursion step 3: value = 3, list = (4, 5, 6, 7)
            |_ ...
               |_ recursion step 6: value = 6, list = (7) 

当我们看到 list 对象仅包含一个元素(7)时,我们停止递归,并从底部开始执行 list.add(value)。也就是说,我们首先将 6 添加到列表的末尾,然后是 5,然后是 4,以此类推。最终,列表中元素的顺序原地反转。此外,这个方法的时间复杂度为线性

接下来,我们创建一个测试来验证我们的递归实现是否按预期工作:

List<Integer> aList = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7));
ReverseArrayList.reverseWithRecursion(aList);
assertThat(aList).isEqualTo(EXPECTED);

如果运行测试,它会通过。所以,我们的递归实现解决了问题。

5. 循环方式反转 List

我们刚刚使用递归反转了列表。另外,我们也可以使用迭代来解决这个问题。

首先,让我们看看实现:

public static <T> void reverseWithLoop(List<T> list) {
    for (int i = 0, j = list.size() - 1; i < j; i++) {
        list.add(i, list.remove(j));
    }
}

正如我们所见,迭代实现也很整洁。然而,我们只有一个 for 循环,循环体中只有一个语句。

接下来,让我们理解它是如何工作的。

我们定义了两个指针 ij,它们都指向给定列表。指针 j 永远指向列表的最后一个元素,而指针 i 从 0 递增到 j-1

在每次迭代步骤中,我们移除最后一个元素并将它添加到 i 位置,即 list.add(i, list.remove(j))。当 i 达到 j-1 时,循环结束,列表就被反转了:

Iteration step 0: i = j = null, list = (1, 2, 3,...7)
Iteration step 1: i = 0; j = 6 
                  |_ list.add(0, list.remove(6))
                  |_ list = (7, 1, 2, 3, 4, 5, 6)
Iteration step 2: i = 1; j = 6 
                  |_ list.add(1, list.remove(6))
                  |_ list = (7, 6, 1, 2, 3, 4, 5)
...
Iteration step 5: i = 4; j = 6 
                  |_ list.add(4, list.remove(6))
                  |_ list = (7, 6, 5, 4, 3, 1, 2)
Iteration step 6: i = 5; j = 6 
                  |_ list.add(5, list.remove(6))
                  |_ list = (7, 6, 5, 4, 3, 2, 1)

这种方法的时间复杂度也是线性的

最后,让我们测试我们的方法,看看它是否按预期工作:

List<Integer> aList = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7));
ReverseArrayList.reverseWithLoop(aList);
assertThat(aList).isEqualTo(EXPECTED);

当运行上述测试时,它会通过。

6. 总结

在这篇文章中,我们通过示例探讨了如何反转 ArrayList。标准的 Collections.reverse 方法非常方便地解决了这个问题。

然而,如果我们想创建自己的反转实现,我们已经学习了两种有效的原地反转方法。

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