1 题目
给你单链表的头结点 head ,请你找出并返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
示例 1:

输入:head = [1,2,3,4,5]
输出:[3,4,5]
解释:链表只有一个中间结点,值为 3 。
示例 2:

输入:head = [1,2,3,4,5,6]
输出:[4,5,6]
解释:该链表有两个中间结点,值分别为 3 和 4 ,返回第二个结点。
提示:
- 链表的结点数范围是
[1, 100] 1 <= Node.val <= 100
2 代码实现
思考
快慢指针吧,一个指针走得快,每次都走两步,另一个指针走得慢,每次都走一步,如果快指针走到null了,就是走完了,那么慢指针也就走在中间了,而且在链表有两个中间节点的时候,也是符合的,因为快指针走过的结点数是慢指针的2倍。
写一下代码看看,之前好像是做过的,蛮简单的。
c++
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* middleNode(ListNode* head) {
ListNode* fast = head ;
ListNode* slow = head ;
while (fast && fast -> next ){
fast = fast -> next -> next ;
slow = slow -> next ;
}
return slow ;
}
};
js
javascript
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var middleNode = function(head) {
let fast = head ;
let slow = head ;
while(fast && fast.next){
fast = fast.next.next;
slow = slow.next ;
}
return slow ;
};
我发现这种循环体的约束条件一般有点像要用API的对象不能是空的,比如fast.next.next,第一个就算fast在调用next,然后就是fast.next在调用next,这两个对象不能是null,还有slow,slow是fast走过的路,这个就不用管了。
诶嘿嘿,上次做这个题目是10.12,现在是1.30,说明至少比以前不会写这个easy的好多了~~
3 题目
给你一个链表的头节点 head 。删除 链表的 中间节点 ,并返回修改后的链表的头节点 head 。
长度为 n 链表的中间节点是从头数起第 ⌊n / 2⌋ 个节点(下标从 0 开始),其中 ⌊x⌋ 表示小于或等于 x 的最大整数。
- 对于
n=1、2、3、4和5的情况,中间节点的下标分别是0、1、1、2和2。
示例 1:

输入:head = [1,3,4,7,1,2,6]
输出:[1,3,4,1,2,6]
解释:
上图表示给出的链表。节点的下标分别标注在每个节点的下方。
由于 n = 7 ,值为 7 的节点 3 是中间节点,用红色标注。
返回结果为移除节点后的新链表。
示例 2:

输入:head = [1,2,3,4]
输出:[1,2,4]
解释:
上图表示给出的链表。
对于 n = 4 ,值为 3 的节点 2 是中间节点,用红色标注。
示例 3:

输入:head = [2,1]
输出:[2]
解释:
上图表示给出的链表。
对于 n = 2 ,值为 1 的节点 1 是中间节点,用红色标注。
值为 2 的节点 0 是移除节点 1 后剩下的唯一一个节点。
提示:
- 链表中节点的数目在范围
[1, 105]内 1 <= Node.val <= 105
4 代码实现
思考
乍一看这个题目就升咖了从easy变medium,其实不就是前面快慢指针的操作多了一个删除而已。
自己写的错误百出的代码
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* deleteMiddle(ListNode* head) {
ListNode* dummy = new ListNode(0);
ListNode* fast = head;
ListNode* slow = head;
dummy -> next = head ;
ListNode* prev = dummy;
while(fast && fast -> next){
fast = fast -> next -> next ;
slow = slow -> next ;
prev = prev -> next ;
}
prev -> next = slow -> next ;
delete slow ;
ListNode* new_head = dummy -> next ;
delete dummy ;
return new_head;
}
};
代码错误核心原因
你的代码未处理链表只有 1 个节点的边界情况 ,导致逻辑异常:当链表仅有一个节点时,slow 会指向该唯一节点,执行 prev->next = slow->next 后虽能删除节点,但会因后续指针操作暴露问题(实际测试中会出现空指针访问 / 结果不符合预期)。
cpp 自行改正
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* deleteMiddle(ListNode* head) {
if(head == nullptr || head -> next == nullptr){
return nullptr;
}
ListNode* dummy = new ListNode(0);
ListNode* fast = head;
ListNode* slow = head;
dummy -> next = head ;
ListNode* prev = dummy;
while(fast && fast -> next){
fast = fast -> next -> next ;
slow = slow -> next ;
prev = prev -> next ;
}
prev -> next = slow -> next ;
delete slow ;
ListNode* new_head = dummy -> next ;
delete dummy ;
return new_head;
}
};
没处理base case。
js
javascript
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var deleteMiddle = function(head) {
if(head == null|| head.next == null){
return null;
}
const dummy = new ListNode(0);
let fast = head;
let slow = head;
dummy.next = head ;
let prev = dummy;
while(fast && fast.next){
fast = fast.next.next ;
slow = slow.next ;
prev = prev.next ;
}
prev.next = slow.next ;
const new_head = dummy.next ;
return new_head;
};
5 小结
快慢指针这里没什么难点,就是边界处理还有一些空指针的问题,注意些base case 或者走好测试用例就没问题,加油,在进步的,没想到这么几个月过去变成cpp自己能搓出来,js也可以自己写的程度了,ok!
复盘
1. 快慢指针法(链表中间节点通用解法)
- 适用场景:找链表中间节点、判断链表是否有环、找环的入口等;
- 核心规律:快指针步长 2,慢指针步长 1,快指针走到末尾时,慢指针必在中间;
- 循环条件:
while(fast && fast.next)(C++/JS 通用),彻底避免fast.next.next的空指针问题。
2. 链表删除操作的 2 个关键技巧
- 哨兵节点 dummy:统一处理 "头节点被删除" 和 "普通节点被删除" 的逻辑,无需单独判断头节点;
- 前驱指针 prev :删除链表节点必须找到其前一个节点,通过
prev.next = cur.next完成删除(直接操作当前节点无法跳过)。
3. 必处理的边界 Case(链表题通用)
- 空链表(
head == null); - 单节点链表(
head.next == null); - 双节点链表(删除后仅剩头节点)。
4. C++/JS 链表实现的核心差异
- 内存管理:C++ 需手动
delete释放动态创建的节点(避免内存泄漏),JS 由垃圾回收机制自动处理,无需手动释放; - 指针 / 属性:C++ 用
->访问节点属性(fast->next),JS 用.访问(fast.next); - 变量声明:C++ 用
ListNode*声明指针,JS 用let/const直接声明节点变量。
五、进步与亮点
- 思路连贯性:能从 "找中间节点" 自然延伸到 "删除中间节点",抓住快慢指针的核心,不盲目修改逻辑;
- 跨语言实现 :同时掌握 C++ 和 JS 的链表实现,能注意到两者的语法差异(如
->和.),非常难得; - 问题复盘能力:能快速定位错误原因(未处理 base case),并独立修正 C++ 和 JS 代码,这是算法学习的核心能力;
- 细节感知 :能注意到循环条件需避免空指针(
fast.next.next的调用对象不能为 null),细节把控到位。
六、小建议(锦上添花)
- 写代码前可先梳理测试用例:如单节点、双节点、奇数节点、偶数节点,提前预判边界问题;
- C++ 链表题中,养成随手释放内存的习惯(动态创建的节点、被删除的节点),这是面试中的加分项;
- 简化代码:如删除节点后,无需额外变量保存
dummy->next,直接返回即可(JS 版可直接return dummy.next)。