链表题在算法面试里有个奇怪的地位:说不难吧,很多人一写就崩;说难吧,核心操作无非就是遍历、插入、删除那几下。LeetCode上跟“中间结点”相关的两道题——定位链表的中间节点和删除链表的中间节点——恰好把“找节点”和“删节点”这两个链表基本动作串在了同一条线上。相关热搜词长期霸榜不是没道理的,因为这两道题是很多链表综合题的底座,面试官也喜欢拿它们当热身题,看看你对指针操作和边界条件敏不敏感。这篇就把它们合在一起拆透:从暴力解法推到快慢指针,再到删除中间节点时必须解决的“前驱节点”问题,最后给出可运行的完整代码和一套边界测试用例。适合刚刷链表题的新手,也适合准备面试前快速回顾的人。
顺带说一句,标题里写“Leetcode 109”的话,搜出来的其实是另一道题,官方题库中“链表的中间结点”编号是876,“删除链表的中间节点”编号是2095。题号记岔很正常,关键是把这两道题背后的套路吃透,编号不影响你掌握知识点。
1. 从两道“同源题”说起:定位和删除之间的关系
1.1 两道题到底在问什么
先明确题目要求,这是所有链表题的第一步。
LeetCode 876“链表的中间结点”:给你单链表的头节点 head,找出并返回链表的中间节点。如果有两个中间节点,则返回第二个中间节点。这个“两个就返回第二个”的约定很关键,后面快慢指针的循环条件就是照着它设计的。
LeetCode 2095“删除链表的中间节点”:给你链表的头节点 head,删除链表的中间节点,然后返回修改后的链表的头节点。同样,如果有两个中间节点,删除第二个。节点数至少为1。
这两道题放在一起看,逻辑关系非常直接:2095的第一步就是876——先找到中间节点;第二步是标准的链表删除。但这里有个坑:如果你真的先把“中间节点”单独找出来,然后想原地删掉它,会发现没有前驱节点信息,删不了。所以删除中间节点的题,真正考验的不是“找中间节点”,而是“找中间节点的前驱节点”。
1.2 为什么这两道题适合一起练
我个人的体会是,链表题可以按“操作链路”来分组复习。876和2095正好构成一条完整链路:定位节点 -> 修改前驱指针 -> 释放节点内存。把这条链路走一遍,后面再做“删除链表倒数第N个节点”“两两交换链表中的节点”这类题,思路会顺很多。
另外,这两道题能把一个很重要的思维习惯练出来:链表操作之前,先搞清楚你到底需要哪个节点的信息。删除节点需要的是前驱,翻转链表需要的是后继,找中间节点需要的是一个能稳定停在中间的游标。把这句话刻在脑子里,比背十道题解都管用。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 快慢指针的推导过程与临界条件较真
2.1 朴素解法:两遍遍历的慢在哪
先看最直观的解法。第一遍遍历数出链表长度 n,第二遍从头走 n / 2 步(整数除法,向下取整),停下的节点就是中间节点。C++ 写出来大概这样:
cpp复制ListNode* middleNode(ListNode* head) {
int n = 0;
ListNode* cur = head;
while (cur) {
++n;
cur = cur->next;
}
cur = head;
for (int i = 0; i < n / 2; ++i) {
cur = cur->next;
}
return cur;
}
这个解法的好处是极其直观,不容易错,时间复杂度 O(n),空间复杂度 O(1)。坏处是链表被遍历了两次。单从算法复杂度看,两次遍历和一次遍历都是 O(n),量级没区别,但那一次额外的遍历在一些大厂面试官眼里就是不够优雅。他们想听的是快慢指针。
2.2 快慢指针为什么能一次遍历找到中点
快慢指针的思路很朴素:让两个指针都从链表头出发,慢指针 slow 每次走一步,快指针 fast 每次走两步。因为 fast 的速度是 slow 的两倍,所以当 fast 到达链表末尾时,slow 刚好走了一半,停下的位置就是中间节点。
文字描述可能不够直观,画个 ASCII 图。假设链表是 1 -> 2 -> 3 -> 4 -> 5:
code复制1 -> 2 -> 3 -> 4 -> 5
s
f
1 -> 2 -> 3 -> 4 -> 5
s f
1 -> 2 -> 3 -> 4 -> 5
s f(此时 f->next 为空,退出)
slow 停在节点 3,正好是中间节点。
再看偶数长度的情况,链表是 1 -> 2 -> 3 -> 4 -> 5 -> 6:
code复制1 -> 2 -> 3 -> 4 -> 5 -> 6
s
f
1 -> 2 -> 3 -> 4 -> 5 -> 6
s f
1 -> 2 -> 3 -> 4 -> 5 -> 6
s f
1 -> 2 -> 3 -> 4 -> 5 -> 6
