如何找到单链表的中间节点?
简化版
用快慢指针一次遍历搞定:慢指针一次走 1 步、快指针一次走 2 步。快指针走到末尾时,慢指针正好在中间。时间 O(n)、空间 O(1),只遍历一遍,不用先数长度再走一半。
详细版
ListNode middleNode(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next; // 走 1 步
fast = fast.next.next; // 走 2 步
}
return slow;
}
快指针速度是慢指针的两倍,当快指针到终点时,慢指针刚好走了一半,停在中间。
偶数个节点时有两个「中间」(比如 4 个节点的第 2、3 个),返回哪个取决于循环条件:
- 条件
fast != null && fast.next != null:偶数时返回靠后的中间节点(第 n/2+1 个)。 - 条件
fast.next != null && fast.next.next != null:偶数时返回靠前的中间节点。
面试要问清楚要哪个,或主动说明你的写法返回的是哪一个。
完整版教学
一、为什么快慢指针能定位中点
设链表长 n。慢走 1、快走 2,两者步数始终是 2:1。快指针走完全程 n 步,慢指针只走了 n/2 步——正好中点。这比「先遍历一遍数出长度 n,再从头走 n/2 步」少走一趟,一次遍历就够。
二、奇偶情况分别验证
- 奇数(如
1 2 3 4 5):中点唯一是3。快指针停在最后一个节点5(fast.next == null),慢指针在3。✅ - 偶数(如
1 2 3 4):两个中间是2和3。用fast != null && fast.next != null时,快指针会走到null,慢指针停在3(靠后那个)。想要2(靠前),把循环条件改成看fast.next和fast.next.next。
三、和「判环」共用一套模板
快慢指针是链表里的万能模板:判环、找中点、找倒数第 k 个、回文链表判断,都是它的变体。区别只是快慢指针的速度差和终止条件。所以把这套「一次遍历定位」的思路吃透,一大类链表题都能套。
四、典型用途
- 归并排序链表:先用快慢指针找中点拆两半,再递归归并。
- 判断回文链表:找到中点,反转后半段,再和前半段逐个比对。
这两道进阶题都以「找中点」为第一步,所以它不只是道基础题,更是很多题的地基。
五、返回前中还是后中
| 链表长度 | 模板 | 循环条件 | 返回位置 |
|---|---|---|---|
奇数 1→2→3→4→5 | 常规模板 | fast != null && fast.next != null | 3 |
偶数 1→2→3→4 | 后中点 | fast != null && fast.next != null | 3 |
偶数 1→2→3→4 | 前中点 | fast.next != null && fast.next.next != null | 2 |
用数字走一遍偶数长度 1→2→3→4:如果 slow=1, fast=1,每轮 slow 走 1 步、fast 走 2 步;第一轮后 slow=2, fast=3,第二轮后 slow=3, fast=null,所以返回后中点 3。这不是错,只是模板选择决定的结果。
面试写找中点前最好主动说一句:我这个循环条件在偶数长度时返回后中点;如果要前中点,终止条件要改。
六、常见误区与追问
- 误区:偶数长度链表只有一个中点。 偶数长度天然有前中点和后中点,题目如果没说明,要主动说明你的返回策略。
- 误区:快慢指针一定比两次遍历复杂。 代码不长,但能一趟完成,尤其适合作为回文链表、链表排序的子步骤。
- 误区:循环条件随便写都一样。
fast != null && fast.next != null和fast.next != null && fast.next.next != null在偶数长度时结果不同。 - 追问:为什么快到尾时慢在中间? 快速指针速度是慢指针两倍,快走完整条链时慢只走了一半。
- 追问:空链表怎么办? 若题目可能传空链表,应先判断
head == null;很多 LeetCode 中点题会保证非空。 - 追问:找中点在归并排序中为什么重要? 它能把链表尽量均分成两半,保证递归深度接近
log n。
七、加强记忆
找中点用快慢指针「快 2 慢 1,快到头、慢到中」,一次遍历、O(1) 空间。偶数个节点时返回前中还是后中,由循环条件决定,写之前先问清楚要哪个。