- 示例工程
【免费下载链接】leetcode
🔥LeetCode solutions in any programming language | 多种编程语言实现 LeetCode、《剑指 Offer(第 2 版)》、《程序员面试金典(第 6 版)》题解
LeetCode 237「删除链表中的节点」是一道非常经典的链表面试题:它不给你链表的头节点head,只给你一个待删除的节点node,却要求你完成删除操作。本文将基于 doocs/leetcode 仓库中该题的中文题解与多语言实现,深入拆解"节点赋值(值拷贝 + 跳过后继)"这一 O(1) 解法背后的数据结构原理,并给出 Python、Java、C++、Go、TypeScript、JavaScript、C#、C 八种语言的完整可运行代码,帮助你彻底掌握这一"无前驱删除"技巧,同时应对同类变体题型。
题目回顾:只给你节点,不给你 head
题目描述非常"反常规":
- 有一个单链表的
head,我们想删除其中的一个节点node; - 但无法访问第一个节点
head,函数入参只有待删除的node本身; - 链表所有节点的值都是唯一的,且保证给定的
node不是链表中的最后一个节点。
同时,题目对"删除"给出了明确的语义界定:
- 给定节点的值不应该存在于链表中;
- 链表中的节点数应该减少 1;
node前面所有值的顺序不变;node后面所有值的顺序不变。
两个官方示例如下:
| 输入 | 输出 | 说明 |
|---|---|---|
head = [4,5,1,9],node = 5 | [4,1,9] | 删除值为 5 的第二个节点 |
head = [4,5,1,9],node = 1 | [4,5,9] | 删除值为 1 的第三个节点 |
约束条件(来自原题解文档):
- 链表中节点的数目范围是
[2, 1000]; -1000 <= Node.val <= 1000;- 链表中每个节点的值都是唯一的;
- 需要删除的节点
node是链表中的节点,且不是末尾节点。
在"自定义测试"中,判题系统会为你构建好整个链表并把node传入函数,输出则是调用函数之后的整个链表。
为什么这道题不能按常规套路删除
常规删除依赖"前驱节点"
在单向链表中,标准删除流程是:找到待删除节点的前驱节点pre,然后执行pre.next = pre.next.next,将前驱直接指向后继,从而把目标节点从链上摘除。
仓库中的 203. 移除链表元素 就是这种典型场景:题目给你head和val,解法维护一个哑节点dummy作为哨兵,用pre依次观察后继节点,命中目标值即执行pre.next = pre.next.next跳过它。这类解法的前提是能拿到头节点、能沿链遍历。
本题的"陷阱":没有前驱可用
而本题你只有node本身。单向链表的指针是单向的:node知道自己指向谁(node.next),却不知道"谁指向自己",因此无法修改前驱的next指针。如果沿用常规思路,这道题将无解。
这正是本题作为经典面试题的考察点:当"改前驱"这条路被堵死时,如何换一个角度完成等价的删除效果?
核心解法:节点赋值(值拷贝 + 跳过后继)
思路推导
原题解文档给出了简洁的思考过程:
只给定待删结点、没有前驱,无法改前驱的
next。可将后继的值抄到当前结点,再跳过后继,效果等同删除。
具体而言,只需要两步赋值:
node.val = node.next.val—— 把下一个节点的值拷贝进当前节点;node.next = node.next.next—— 让当前节点跳过后继节点,直接指向后继的后继。
执行完毕后,从调用者视角看:当前节点位置的值变成了原来后继的值,而原本后继的位置被"跳"出了链表,等价于"删除了传入的node"。节点数减少 1,前后节点的相对顺序均保持不变,完全满足题目语义。
以head = [4,5,1,9]、node = 5为例逐步推演:
- 初始:
4 -> 5 -> 1 -> 9,待删节点是值为 5 的第二个节点; - 第一步赋值:把后继(值为 1 的节点)的值抄给当前节点,节点变为
4 -> 1 -> 1 -> 9; - 第二步跳过:当前节点的
next指向后继的后继,链表变为4 -> 1 -> 9。
最终链表中不再存在值 5,节点数由 4 减为 3,前后顺序保持不变。
关于"删除"的内存语义
题目特别强调:"删除节点并不是指从内存中删除它。"因此本解法不需要调用free或delete释放内存。被跳过的那个节点虽然仍残留在堆上,但已经不再被链表引用:在 Python、Java、Go、C#、JavaScript/TypeScript 等带垃圾回收(GC)的运行时中,它会由 GC 自动回收;在 C/C++ 中,LeetCode 判题环境关注的是链表逻辑结构的变化,函数内无需手动释放内存。这也解释了为什么仓库中 Solution.c 的实现只做两步指针操作而不含free调用。
为什么题目约束"恰好"保证了该解法的正确性
- 保证
node不是尾节点:这是本解法成立的前提。若node是尾节点,则node.next为null,第一步取值就会空指针异常,更无从谈"跳过后继"; - 保证节点值唯一:第一步的"值拷贝"不会产生值冲突,确保删除后链表中确实不再存在该值,逻辑语义与"真正删除"一致;
- 节点数范围
[2, 1000]:保证链表中至少有两个节点,即任何待删节点都存在合法的后继。
多语言实现(完整可运行代码)
以下实现均来自仓库 0237.Delete Node in a Linked List 目录下的Solution.*源文件与题解文档,函数签名完全贴合 LeetCode 平台要求:不返回任何值,原地(in-place)修改链表。
Python3
# Definition for singly-linked list. # class ListNode: # def __init__(self, x): # self.val = x # self.next = None class Solution: def deleteNode(self, node): """ :type node: ListNode :rtype: void Do not return anything, modify node in-place instead. """ node.val = node.next.val node.next = node.next.nextJava
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */ class Solution { public void deleteNode(ListNode node) { node.val = node.next.val; node.next = node.next.next; } }C++
/** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode(int x) : val(x), next(NULL) {} * }; */ class Solution { public: void deleteNode(ListNode* node) { node->val = node->next->val; node->next = node->next->next; } };Go
/** * Definition for singly-linked list. * type ListNode struct { * Val int * Next *ListNode * } */ func deleteNode(node *ListNode) { node.Val = node.Next.Val node.Next = node.Next.Next }TypeScript
/** * Definition for singly-linked list. * class ListNode { * val: number * next: ListNode | null * constructor(val?: number, next?: ListNode | null) { * this.val = (val===undefined ? 0 : val) * this.next = (next===undefined ? null : next) * } * } */ /** Do not return anything, modify it in-place instead. */ function deleteNode(node: ListNode | null): void { node.val = node.next.val; node.next = node.next.next; }JavaScript
/** * Definition for singly-linked list. * function ListNode(val) { * this.val = val; * this.next = null; * } */ /** * @param {ListNode} node * @return {void} Do not return anything, modify node in-place instead. */ var deleteNode = function (node) { node.val = node.next.val; node.next = node.next.next; };C#
/** * Definition for singly-linked list. * public class ListNode { * public int val; * public ListNode next; * public ListNode(int x) { val = x; } * } */ public class Solution { public void DeleteNode(ListNode node) { node.val = node.next.val; node.next = node.next.next; } }C
/** * Definition for singly-linked list. * struct ListNode { * int val; * struct ListNode *next; * }; */ void deleteNode(struct ListNode* node) { node->val = node->next->val; node->next = node->next->next; }可以看到,无论哪种语言,核心逻辑都严格收敛为同一条语句模式:先拷贝后继值,再跳过后继节点。这也是仓库中 Solution.py、Solution.java 等全部 8 份源码文件与题解文档保持一致的原因。
边界条件与注意事项
node必须是链表中真实存在的节点:题目保证传入的node在链表中,因此无需做存在性校验;node绝不能是尾节点:node.next为null时,node.next.val会抛出空指针异常(Java/C++/C#/Go/TS)或AttributeError(Python);正是因为题目保证了这一点,代码才可以"裸奔"式直接访问node.next,无需判空;- 值唯一性是语义前提:若存在重复值,第一步"拷贝覆盖"可能让"删除后链表中不存在该值"这一要求无法严格满足;
- 不要试图获取
head:函数签名中根本没有head参数,不要在实现里尝试遍历链表定位前驱——那既不可行也不必要; - 注意指针的求值顺序:在 C/C++/C#/Java 中,
node.val = node.next.val与node.next = node.next.next两行顺序不可颠倒。必须先拷贝值,再改写next;若先改写next,则后继指针已丢失,第一步就无从拷贝。Python/Go/JS/TS 的实现同样遵循"先取值、再跳链"的顺序。
复杂度分析
- 时间复杂度:O(1)。整个删除过程只执行常数次赋值与指针操作,与链表长度无关,无需遍历;
- 空间复杂度:O(1)。只使用现有节点和指针,不申请任何额外数据结构。
这一"常数时间删除"是本题的核心亮点:常规的单链表删除需要 O(n) 遍历找前驱,而本题借助"值拷贝"这一技巧,在无法访问前驱的限制下依然做到了 O(1)。
同类题型拓展:面试题 02.03「删除中间节点」
同样的解题模式在仓库的《程序员面试金典》部分也有收录:面试题 02.03. 删除中间节点。该题给定单向链表a->b->c->d->e->f中的中间节点c,要求将其删除后链表变为a->b->d->e->f——入参同样只有待删节点本身,同样保证不是尾节点,解法与本题完全一致:
class Solution: def deleteNode(self, node): node.val = node.next.val node.next = node.next.next可见"无前驱删除"是链表题中的高频考察点。掌握了 LeetCode 237 的节点赋值法,遇到这类变体可以直接迁移复用。与之形成对比的是前面提到的 203. 移除链表元素:当你能拿到head时,应优先使用哑节点 + 前驱遍历的标准删除法;而当入参只有节点本身时,则应切换到"值拷贝 + 跳过后继"这一非常规但极其优雅的技巧。
小结
- LeetCode 237 的独特之处在于只给待删节点、不给头节点,导致常规的"找前驱、改指针"方案不可用;
- 解法核心是节点赋值:把后继的值拷贝到当前节点,再把当前节点的
next指向后继的后继,两步完成 O(1) 删除; - 该解法成立依赖两个关键前提:
node非尾节点与节点值唯一,二者均由题目约束保证; - 仓库中该题收录了 Solution.py、Solution.java、Solution.cpp、Solution.go、Solution.ts、Solution.js、Solution.cs、Solution.c 共 8 种语言的完整实现,并配中文与英文题解,可作为刷题与面试复习的可靠参考。
- 示例工程
【免费下载链接】leetcode
🔥LeetCode solutions in any programming language | 多种编程语言实现 LeetCode、《剑指 Offer(第 2 版)》、《程序员面试金典(第 6 版)》题解
相关推荐
不借助头结点删除链表节点:LeetCode 237「节点赋值」解法的原理与多语言实现
不借助头结点删除链表节点:LeetCode 237「节点赋值」解法的原理与多语言实现 本文围绕 LeetCode 237 题「删除链表中的节点」(Delete
示例工程InterviewGuide 精选力扣:237. 删除链表中的节点——不访问头节点也能 O(1) 删除的"替身攻击"解法精讲
InterviewGuide 精选力扣:237. 删除链表中的节点——不访问头节点也能 O 1 删除的"替身攻击"解法精讲 本篇来自 InterviewGuid
教程Czkawka 磁盘清理工具完整教程:一次扫描定位重复文件与空文件夹
Czkawka 磁盘清理工具完整教程:一次扫描定位重复文件与空文件夹 Czkawka 是一款用 Rust 编写的免费开源磁盘清理工具,能把你指定的目录一次性扫一
桌面应用CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考