Leetcode 109 链表的中间结点 | 删除链表的中间节点

1 题目

876. 链表的中间结点

给你单链表的头结点 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 题目

2095. 删除链表的中间节点

给你一个链表的头节点 head删除 链表的 中间节点 ,并返回修改后的链表的头节点 head

长度为 n 链表的中间节点是从头数起第 ⌊n / 2⌋ 个节点(下标从 0 开始),其中 ⌊x⌋ 表示小于或等于 x 的最大整数。

  • 对于 n = 12345 的情况,中间节点的下标分别是 01122

示例 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直接声明节点变量。

五、进步与亮点

  1. 思路连贯性:能从 "找中间节点" 自然延伸到 "删除中间节点",抓住快慢指针的核心,不盲目修改逻辑;
  2. 跨语言实现 :同时掌握 C++ 和 JS 的链表实现,能注意到两者的语法差异(如->.),非常难得;
  3. 问题复盘能力:能快速定位错误原因(未处理 base case),并独立修正 C++ 和 JS 代码,这是算法学习的核心能力;
  4. 细节感知 :能注意到循环条件需避免空指针(fast.next.next的调用对象不能为 null),细节把控到位。

六、小建议(锦上添花)

  1. 写代码前可先梳理测试用例:如单节点、双节点、奇数节点、偶数节点,提前预判边界问题;
  2. C++ 链表题中,养成随手释放内存的习惯(动态创建的节点、被删除的节点),这是面试中的加分项;
  3. 简化代码:如删除节点后,无需额外变量保存dummy->next,直接返回即可(JS 版可直接return dummy.next)。
相关推荐
只是懒得想了4 小时前
C++实现密码破解工具:从MD5暴力破解到现代哈希安全实践
c++·算法·安全·哈希算法
码农水水5 小时前
得物Java面试被问:消息队列的死信队列和重试机制
java·开发语言·jvm·数据结构·机器学习·面试·职场和发展
m0_736919105 小时前
模板编译期图算法
开发语言·c++·算法
dyyx1115 小时前
基于C++的操作系统开发
开发语言·c++·算法
AutumnorLiuu5 小时前
C++并发编程学习(一)——线程基础
开发语言·c++·学习
m0_736919105 小时前
C++安全编程指南
开发语言·c++·算法
CS创新实验室5 小时前
关于 Moltbot 的学习总结笔记
笔记·学习·clawdbot·molbot
蜡笔小马5 小时前
11.空间索引的艺术:Boost.Geometry R树实战解析
算法·r-tree
-Try hard-5 小时前
数据结构:链表常见的操作方法!!
数据结构·算法·链表·vim
2301_790300965 小时前
C++符号混淆技术
开发语言·c++·算法