1. 概述

本文将深入探讨如何在 Java 中高效地查找链表的中间元素。

我们会先分析常见场景和难点,然后逐一介绍几种实用的解决方案,包括踩坑点和优化思路。对于有经验的开发者来说,这属于基础但高频的面试与实际问题,掌握多种解法能让你在不同场景下游刃有余。

2. 维护链表大小信息

最简单粗暴的方式是:在插入元素时就维护链表的 size。一旦我们知道总长度,中间位置自然就明确了——直接计算索引即可,问题瞬间降维。

以 Java 自带的 LinkedList 为例:

public static Optional<String> findMiddleElementLinkedList(
  LinkedList<String> linkedList) {
    if (linkedList == null || linkedList.isEmpty()) {
        return Optional.empty();
    }

    return Optional.of(linkedList.get(
      (linkedList.size() - 1) / 2));
}

⚠️ 但要注意:LinkedList.get(index) 并不是 O(1) 操作!它内部仍需遍历节点。我们来看 JDK 源码中的 node(index) 方法:

Node<E> node(int index) {
    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++) {
            x = x.next;
        }
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--) {
            x = x.prev;
        }
        return x;
    }
}

✅ 优点:逻辑清晰,代码简洁
❌ 缺点:频繁随机访问时性能差,尤其在单向链表中无法利用双向指针优化

所以这种“取巧”方式仅适用于你能控制数据结构设计的场景。如果只能从 head 开始遍历?那就得看下面的硬核解法了。

3. 仅知道头节点时查找中间元素

更常见的现实是:你只拿到一个 head 节点,链表长度未知。这时候就不能依赖 size 了,必须通过遍历来解决。

我们先定义一个简单的单向链表节点类:

public static class Node {

    private Node next;
    private String data;

    public Node(String data) {
        this.data = data;
    }

    public Node next() {
        return next;
    }

    public boolean hasNext() {
        return next != null;
    }

    public void setNext(Node next) {
        this.next = next;
    }

    public String data() {
        return data;
    }

    public String toString() {
        return this.data;
    }
}

为了方便测试,我们写个辅助方法生成指定长度的链表:

private static Node createNodesList(int n) {
    Node head = new Node("1");
    Node current = head;

    for (int i = 2; i <= n; i++) {
        Node newNode = new Node(String.valueOf(i));
        current.setNext(newNode);
        current = newNode;
    }

    return head;
}

比如 createNodesList(5) 会生成:1 → 2 → 3 → 4 → 5

3.1. 先遍历求长度,再找中间节点

最直观的思路:

  1. 第一次遍历:统计总长度 size
  2. 第二次遍历:从头走 (size - 1)/2 步,即为中间节点

实现如下:

public static Optional<String> findMiddleElementFromHead(Node head) {
    if (head == null) {
        return Optional.empty();
    }

    // 第一次遍历:计算长度
    Node current = head;
    int size = 1;
    while (current.hasNext()) {
        current = current.next();
        size++;
    }

    // 第二次遍历:走到中间
    current = head;
    for (int i = 0; i < (size - 1) / 2; i++) {
        current = current.next();
    }

    return Optional.of(current.data());
}

✅ 实现简单,容易理解
❌ 时间复杂度 O(n),但需要两次遍历 —— 性能不够优雅

面试中如果你只答出这一种,基本会被要求优化。

3.2. 一次遍历:快慢指针(推荐)

这才是真正“高级感”拉满的解法——快慢指针(Slow-Fast Pointer)技巧

核心思想:

  • 定义两个指针:slowfast
  • fast 每次走 2 步,slow 每次走 1 步
  • fast 到达末尾时,slow 正好在中间

🎯 图解过程(以 5 个节点为例):

初始:
slow → 1
fast → 1

第1轮:
       slow → 2
              fast → 3

第2轮:
            slow → 3
                     fast → 5 (next=null,停止)

此时 slow 指向 3,正是中间元素

代码实现:

public static Optional<String> findMiddleElementFromHead1PassIteratively(Node head) {
    if (head == null) {
        return Optional.empty();
    }

    Node slowPointer = head;
    Node fastPointer = head;

    // 条件:fast 还能再走两步
    while (fastPointer.hasNext() && fastPointer.next().hasNext()) {
        fastPointer = fastPointer.next().next();
        slowPointer = slowPointer.next();
    }

    return Optional.ofNullable(slowPointer.data());
}

✅ 仅需一次遍历,O(n) 时间,O(1) 空间
✅ 面试高频考点,必须掌握
⚠️ 注意边界判断:空链表、单节点、偶数长度

测试用例验证奇偶情况:

@Test
void whenFindingMiddleFromHead1PassIteratively_thenMiddleFound() {
    assertEquals("3", MiddleElementLookup
      .findMiddleElementFromHead1PassIteratively(createNodesList(5)).get());
    assertEquals("2", MiddleElementLookup
      .findMiddleElementFromHead1PassIteratively(createNodesList(4)).get());
}

对于偶数长度(如 4),返回第 2 个元素(即前半部分的尾部),符合 (n-1)/2 的下取整逻辑。

3.3. 一次遍历:递归解法(拓展思路)

除了迭代,也可以用递归实现“一次遍历”。关键在于利用递归的回溯过程来计数。

思路:

  1. 一路递归到底(类似 DFS)
  2. 回溯时开始计数,当计数到达 size/2 时,当前节点就是中间节点

由于 Java 没有指针引用传递,我们需要一个辅助类来保存状态:

private static class MiddleAuxRecursion {
    Node middle;
    int length = 0;
}

递归主体逻辑:

private static void findMiddleRecursively(Node node, MiddleAuxRecursion middleAux) {
    if (node == null) {
        // 到达尾部,开始回溯,此时 length 是总长度
        middleAux.length = middleAux.length / 2;
        return;
    }
    
    middleAux.length++;  // 向下递归时累加长度
    findMiddleRecursively(node.next(), middleAux);

    // 回溯阶段:length 表示“距离尾部还有多少步”
    if (middleAux.length == 0) {
        middleAux.middle = node; // 正好是中间节点
    }
    
    middleAux.length--; // 回溯时递减
}

封装调用入口:

public static Optional<String> findMiddleElementFromHead1PassRecursively(Node head) {
    if (head == null) {
        return Optional.empty();
    }

    MiddleAuxRecursion middleAux = new MiddleAuxRecursion();
    findMiddleRecursively(head, middleAux);
    return Optional.of(middleAux.middle.data());
}

测试验证:

@Test
void whenFindingMiddleFromHead1PassRecursively_thenMiddleFound() {
    assertEquals("3", MiddleElementLookup
      .findMiddleElementFromHead1PassRecursively(createNodesList(5)).get());
    assertEquals("2", MiddleElementLookup
      .findMiddleElementFromHead1PassRecursively(createNodesList(4)).get());
}

✅ 思路巧妙,体现递归对称美
❌ 使用了额外的栈空间,链表过长可能栈溢出
💡 适合展示算法思维,生产环境建议用快慢指针

4. 总结

方法 时间复杂度 空间复杂度 是否推荐
维护 size O(1) 取值 O(1) ✅ 特定场景可用
两次遍历 O(n) O(1) ⚠️ 可行但不优雅
快慢指针(迭代) O(n) O(1) ✅✅✅ 强烈推荐
递归回溯 O(n) O(n) 栈空间 ✅ 拓展思路

📌 核心结论

  • 日常开发中,优先使用 快慢指针 解法,简单高效无副作用
  • 面试中如果被问到,先说两次遍历,再主动优化到快慢指针,展现思维过程
  • 递归解法虽酷,但要考虑栈安全,别为了炫技埋雷

所有示例代码已托管至 GitHub:https://github.com/baeldung/java-algorithms


原始标题:Find the Middle Element of a Linked List in Java | Baeldung