LeetCode-Book 精讲:237. 删除链表中的节点——仅凭当前节点指针实现 O(1) 删除的「值替换」技法
【免费下载链接】LeetCode-Book《剑指 Offer》《图解算法数据结构》《Krahets 笔面试精选 88 题》Python, Java, C++ 解题代码项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Book
导读
本文围绕 LeetCode 237「删除链表中的节点」展开,这是 LeetCode-Book 仓库《Krahets 笔面试精选 88 题》中的一道高频链表基础题。题目最大的陷阱在于:函数只传入「待删除节点」本身,而不给链表头节点,导致常规的「通过前驱节点改指针」方案完全失效。读完本文,你将掌握**「复制后继节点值 + 删除后继节点」**这一 O(1) 时间的核心技法,理解其适用前提与边界限制,并对照本仓库 Python / Java / C++ 三语实现与 C++ 测试用例,彻底吃透这道经典题。
题目本质:只给「节点」,不给「链表」
LeetCode 237 的题目约束决定了它与常规删除操作截然不同:
- 链表是单向链表,每个节点只有指向后继的
next指针; - 函数签名只接收待删除节点
node本身,不提供头节点head,也不返回任何值; - 题目保证传入的节点不是链表尾节点。
常规删除需要「前驱指针」,而单向链表无法从当前节点回溯到前驱——这正是本题的难点与考察点。
常规删除为什么行不通
设前驱节点为pre、当前节点为cur、后继节点为cur.next,常规删除的本质是执行一行指针操作:
pre.next = cur.next即将当前节点从链表中「摘除」。但本题只有node的引用,无法访问node的前驱pre,因此这一常规方法被直接封死。这也是为什么该题被选入精选 88 题——它考察的是对「节点引用」与「链表结构」两者关系的理解深度。
核心思路:「值替换」——复制后继,删除后继
无法动前驱指针,那就换一个思路:不删除传入的node本身,而是让node变成后继节点的“替身”,再把后继节点删掉。具体两步:
- 复制后继节点值:把
node.next的值赋给node,使node在值上“变成”后继节点; - 逻辑删除后继:执行
node.next = node.next.next,把原来的后继节点从链表中摘除。
从最终链表结构看,node这个位置上的值已被替换,后继被跳过,效果等同于「删除了传入节点」。
示例推演:4 → 5 → 1 → 9 中删除节点 5
以文档中的示例链表4 → 5 → 1 → 9、待删除节点为5为例,逐步推演:
| 步骤 | 操作 | 链表状态 |
|---|---|---|
| 初始 | — | 4 →5→ 1 → 9 |
| 第一步 | node.val = node.next.val(5 变成 1) | 4 →1→ 1 → 9 |
| 第二步 | node.next = node.next.next(跳过第二个 1) | 4 →1→ 9 |
最终链表变为4 → 1 → 9,数值序列与直接删除节点 5 完全一致。注意:这里被真正“摘除”的是原值 1 所在的后继节点,而传入的node节点对象本身仍留在链表中,只是它的值被覆盖了。删除的是“值”,而不是“指针所指向的节点对象”——这是理解本题的关键。
三语代码实现与注释解析
文档给出了 Python / Java / C++ 三种语言的解法,并在后三个 Tab 中附带了逐行注释版本。以下完整继承并给出带注释的版本:
class Solution: def deleteNode(self, node): # 1. 复制 node.next 的值到 node(“值替换”) node.val = node.next.val # 2. 从链表中删除 node.next(跳过原后继节点) node.next = node.next.nextclass Solution { public void deleteNode(ListNode node) { // 1. 复制 node.next 到 node node.val = node.next.val; // 2. 从链表中删除 node.next node.next = node.next.next; } }class Solution { public: void deleteNode(ListNode* node) { // 1. 复制 node.next 到 node node->val = node->next->val; // 2. 从链表中删除 node.next node->next = node->next->next; } };三种语言逻辑完全一致,差异仅在于语法:C++ 通过->访问指针成员,Java / Python 通过.访问对象成员。该解法在 LeetCode-Book 仓库中均有对应源码文件:
- Python:lc_237_delete_node_in_a_linked_list_s1.py 与带注释的 lc_237_delete_node_in_a_linked_list_s2.py;
- C++:lc_237_delete_node_in_a_linked_list_s1.cpp 与带注释的 lc_237_delete_node_in_a_linked_list_s2.cpp;
- Java:lc_237_delete_node_in_linked_list_s1.java 与 lc_237_delete_node_in_linked_list_s2.java。
仓库源码佐证:工具函数与 C++ 测试用例
仓库不仅提供解法,还给出了可运行的验证环境,可从源码层面印证上述推理。
链表节点定义与工具函数
Python 侧的工具函数集中在 linked_list.py,核心定义如下:
ListNode:单链表节点类,含val与next两个属性;list_to_linked_list(arr):由数组构造链表;linked_list_to_list(head):将链表序列化为数组,便于打印验证;get_list_node(head, val):按值定位节点——这正是本题“只拿到节点、拿不到前驱”场景的模拟入口。
C++ 测试用例:完整验证删除效果
C++ 源码 lc_237_delete_node_in_a_linked_list_s1.cpp 中给出了可运行测试,与文档示例完全对应:
int main() { // ======= Test Case ======= ListNode* head = vectorToLinkedList({4, 5, 1, 9}); ListNode* node = getListNode(head, 5); // ====== Driver Code ====== Solution* slt = new Solution(); slt->deleteNode(node); PrintUtil::printLinkedList(head); return 0; }执行流程:用数组{4, 5, 1, 9}构造链表 → 按值5定位到待删除节点 → 调用deleteNode(node)→ 打印整个链表。预期输出为4 -> 1 -> 9,与文档推演结果一致,可作为该算法的自动化验证手段。可见仓库源码、文档与题目三者互为印证:删除后传入的node节点仍留在链表中,但链表的数值序列已等价于删除了该节点。
复杂度分析
- 时间复杂度 O(1):无论链表多长,都只执行两次指针/赋值操作,不涉及遍历,使用常数时间;
- 空间复杂度 O(1):仅使用常数大小的额外空间,未借助任何辅助容器。
这也是该题“最优解”的复杂度上界——因为题目只给节点引用,任何需要定位前驱的方案都至少是 O(n),而值替换法恰好绕开了这一瓶颈。
边界条件与易错点
- 尾节点不可删除:若
node是链表尾节点,则node.next为空,代码会因空指针访问崩溃。本题题目约束已保证传入节点不是尾节点,因此可直接使用;但在改写为通用函数时必须显式处理尾节点(如返回None或报错)。 - 删除的是“值”而非“对象”:函数执行后,原
node节点对象仍在内存中,只是值被覆盖。调用方若持有其他指向原后继节点的引用,会观察到该节点已“脱离”链表。 - C++ 内存管理:C++ 版本执行
node->next = node->next->next后,被摘除的节点在 LeetCode 评测环境中由平台统一回收;若在本地自行管理内存,需注意避免内存泄漏,考虑手动释放被跳过的后继节点(并同步更新node->next指向)。
小结
「删除链表中的节点」表面是 O(1) 的简单题,实则考察三点:对单链表“只能单向访问”结构的认知、对“删除操作本质是改指针”的抽象能力,以及临场绕开约束的变通思维。值替换法(复制后继值 + 删除后继)是这道题的标准最优解:时间 O(1)、空间 O(1),且三行代码即可实现。掌握这一技法后,建议结合仓库内 LeetCode-Book 精选题解文档 中其他链表题目(如 206. 反转链表、876. 链表的中间结点)继续巩固链表指针操作的基本功。
【免费下载链接】LeetCode-Book《剑指 Offer》《图解算法数据结构》《Krahets 笔面试精选 88 题》Python, Java, C++ 解题代码项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Book
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考