15、链表的中间节点:快慢指针法、计数法

链表的中间节点,这题我面试过不少人,自己也写过不下几十遍。说白了,就是给你一条单链表,让你找到正中间那个节点。如果节点总数是偶数,返回第二个中间节点——这是 LeetCode 上的标准定义。

嗯,咱们今天就把两种主流解法掰开揉碎讲清楚:快慢指针法计数法。这两种思路,我在实际项目中都用过,各有各的适用场景。

15.1 问题定义

先明确一下输入输出:

  • 输入:单链表的头指针 head
  • 输出:中间节点的指针
  • 约束:链表长度未知,只能单向遍历

举个例子:

1 → 2 → 3 → 4 → 5      // 奇数个节点,中间是 3
1 → 2 → 3 → 4 → 5 → 6  // 偶数个节点,中间是 4(第二个中间节点)

为什么会要求偶数时返回第二个?这是 LeetCode 的惯例,咱们就按这个来。如果你面试时遇到,最好跟面试官确认一下。

15.2 方法一:计数法(直观但低效)

计数法的思路很简单:先遍历一遍,数出链表总长度 n;再遍历第二遍,走到 n/2 的位置停下。

我刚开始学链表时,用的就是这招。虽然它要遍历两遍,但胜在逻辑清晰,不容易写错。

struct ListNode* middleNode_count(struct ListNode* head) {
    int count = 0;
    struct ListNode* cur = head;
    
    // 第一遍:数节点个数
    while (cur != NULL) {
        count++;
        cur = cur->next;
    }
    
    // 第二遍:走到中间位置
    int mid = count / 2;
    cur = head;
    for (int i = 0; i < mid; i++) {
        cur = cur->next;
    }
    
    return cur;
}

时间复杂度:O(n) + O(n/2) = O(n)
空间复杂度:O(1)

注意看,这里 mid = count / 2。如果 count = 5,mid = 2,走两步到 3(索引从0开始)。如果 count = 6,mid = 3,走三步到 4。完美符合「偶数时返回第二个中间节点」的要求。

小技巧:如果你需要同时知道中间节点和链表长度,计数法反而更合适。我在做日志缓冲区管理时,就经常需要这两个信息。

15.3 方法二:快慢指针法(优雅且高效)

快慢指针法,这才是面试官想看到的解法。思路是这样的:

  • 两个指针,slow 每次走一步,fast 每次走两步
  • 当 fast 走到末尾时,slow 正好在中间

你想想看,这就像两个人跑步,一个速度是另一个的两倍。当快的跑完全程,慢的刚好跑了一半。

struct ListNode* middleNode_fastSlow(struct ListNode* head) {
    struct ListNode* slow = head;
    struct ListNode* fast = head;
    
    while (fast != NULL && fast->next != NULL) {
        slow = slow->next;      // 慢指针走一步
        fast = fast->next->next; // 快指针走两步
    }
    
    return slow;
}

时间复杂度:O(n/2) = O(n),但实际遍历次数只有计数法的一半
空间复杂度:O(1)

这里有个细节要注意:while 条件里为什么要同时判断 fast 和 fast->next?

因为如果链表节点数是奇数,fast 会停在最后一个节点(fast->next == NULL)。如果节点数是偶数,fast 会停在 NULL。两种情况下,循环都能正确退出。

我曾经在写这段代码时,只写了 while (fast != NULL),结果偶数个节点时 fast 走到 NULL 后还试图访问 fast->next,直接段错误。嗯,这个坑我踩过,你们别踩。

15.4 两种方法对比

对比维度 计数法 快慢指针法
遍历次数 2次 1次
代码复杂度
额外信息 可同时获得链表长度
面试偏好 一般 推荐
适用场景 需要长度信息时 纯找中间节点

我个人习惯,如果只是单纯找中间节点,肯定用快慢指针。但如果后续逻辑还需要链表长度,计数法反而更省事——毕竟长度已经算出来了,不用再遍历一次。

15.5 核心逻辑流程图

下面这张图展示了快慢指针法的执行过程,我画的是奇数个节点的情况:

快慢指针法执行过程(奇数个节点:1→2→3→4→5) 初始状态: 1 2 3 4 5 NULL slow fast 第1步后: 1 2 3 4 5 NULL slow fast 第2步后(结束): 1 2 3 4 5 NULL slow(结果) fast(已到末尾)

从图中可以清楚看到:slow 最终停在节点 3,这就是我们要找的中间节点。fast 走了两步后到达 NULL,循环结束。

15.6 实际项目中的选择

我在嵌入式项目中,经常用快慢指针法来做链表的分割操作。比如一个任务队列,我想把它均匀分成两半,让两个处理器核分别处理。这时候找到中间节点,然后断开链表,就完成了分割。

但有一次,我需要同时知道队列的总长度来做负载均衡。这时候我就用了计数法——反正都要算长度,多遍历一次也无所谓。

所以我的建议是:

  • 面试时:优先写快慢指针法,面试官会眼前一亮
  • 实际项目中:根据需求选,不要为了炫技而炫技

扩展思考:快慢指针法还能用来检测链表是否有环。这个咱们后面章节会讲到,思路是一样的——如果有环,fast 永远追不上 slow,会陷入死循环。嗯,到时候再细说。

好了,链表的中间节点就讲到这里。两种方法,一个直观一个优雅,你掌握了吗?