LeetCode 237. 删除链表中的节点
题目描述


题意分析
这道题的函数签名很不寻常:参数只有一个待删除的节点,既没有链表头,也没有返回值。调用方在函数返回后会拿原来的头指针去检查链表,期望看到那个节点的值已经从序列中消失,其余节点的相对顺序保持不变。
拿不到头节点意味着无法从前面走过来,而单向链表又不能回退,所以「找到前驱、改它的
next」这条常规路径被彻底堵死。这是本题唯一的难点,也是它被归为中等的原因。题目补了一条关键保证:待删除的节点一定不是尾节点,也就是
node.next必然存在。这个保证不是随口一提,而是解法能够成立的前提——正是因为后面还有节点,才有东西可以「搬过来」。此外还要意识到,判题只看链表里的值序列,并不检查节点对象的身份,这一点决定了可以用「改值」来伪装成「删节点」。
解法:复制后继节点并跳过
核心思路
问题关键: 参数只有待删除节点
node,没有头节点;单链表又无法向前找到node的前驱,所以不能执行常规的prev.next = node.next。等价转换: 题目验证的是链表的值序列。先把后继节点的值复制到
node,再跳过后继节点,就能让原来node的值从序列中消失。操作前局部序列为
X -> Y -> rest;复制后是Y -> Y -> rest;跳过后继后变成Y -> rest。因此值X被删除,后面的值及顺序保持不变。适用边界: 题目保证
node不是尾节点,这是解法成立的必要条件。对象身份上,被移出链表的是原后继节点,传入的node仍在链表中但值已改变;若业务依赖节点身份或存在外部节点引用,这种技巧就不等价。
解题步骤
- 将
node.next.val复制到node.val。- 令
node.next = node.next.next,跳过原后继节点。- 无需返回头节点,修改会通过共享的节点对象对调用方生效。
口述示例:
4 -> 5 -> 1 -> 9删除值为 5 的节点。先复制后继,得到4 -> 1 -> 1 -> 9;再跳过第二个 1,得到4 -> 1 -> 9。边界反例: 若
node是尾节点,则没有后继值可复制,也没有已知前驱可改。在当前函数签名下无法完成删除,必须额外提供头节点、前驱或双向链表结构。
代码实现
class Solution {
public void deleteNode(ListNode node) {
node.val = node.next.val;
node.next = node.next.next;
}
}
func deleteNode(node *ListNode) {
node.Val = node.Next.Val
node.Next = node.Next.Next
}
复杂度分析
- 时间复杂度:$O(1)$,只有两次字段赋值。
- 空间复杂度:$O(1)$,没有创建新节点。
关键点总结
- 缺少前驱时,把“删除当前节点”转化为“当前节点顶替后继,再删除后继”。
- 先复制值、后修改指针,顺序不能颠倒。
- “不是尾节点”是必要前提,不是普通边界优化。
- 该解法保持值序列,不保持节点对象身份;面试时应主动说明这一语义差异。
易错点总结
- 只复制值不跳过后继:链表长度不变,并产生重复值。
- 只跳过后继不复制值:删除的是后继的值,不是目标值。
- 对尾节点使用该方法:访问空的
next会崩溃。- 误认为传入节点对象已从链表删除:真正脱链的是它原来的后继节点。
相似题目
| 题目 | 难度 | 考察点 |
|---|---|---|
| 82. 删除排序链表中的重复元素 II | 中等 | 重复值整段删光,头节点也可能被删,必须靠哑节点与前驱指针 |
| 83. 删除排序链表中的重复元素 | 简单 | 每段重复保留一个,只需单指针原地跳过,不涉及前驱 |
| 203. 移除链表元素 | 简单 | 按值批量删除,考的是哑节点如何统一处理头部删除 |
| 剑指 Offer 18. 删除链表的节点 | 简单 | 给了头节点和目标值,回到标准的「找前驱改 next」写法 |
| 面试题 02.01. 移除重复节点 | 简单 | 链表无序,需要哈希集合记录已出现的值,另有无额外空间的进阶 |
| 面试题 02.03. 删除中间节点 | 简单 | 与本题同一技巧的另一份题面,可用来验证是否真的理解而非背诵 |