目录

题目描述

237. 删除链表中的节点

image-20230305201112141

image-20230305201108966

题意分析

这道题的函数签名很不寻常:参数只有一个待删除的节点,既没有链表头,也没有返回值。调用方在函数返回后会拿原来的头指针去检查链表,期望看到那个节点的值已经从序列中消失,其余节点的相对顺序保持不变。

拿不到头节点意味着无法从前面走过来,而单向链表又不能回退,所以「找到前驱、改它的 next」这条常规路径被彻底堵死。这是本题唯一的难点,也是它被归为中等的原因。

题目补了一条关键保证:待删除的节点一定不是尾节点,也就是 node.next 必然存在。这个保证不是随口一提,而是解法能够成立的前提——正是因为后面还有节点,才有东西可以「搬过来」。此外还要意识到,判题只看链表里的值序列,并不检查节点对象的身份,这一点决定了可以用「改值」来伪装成「删节点」。

解法:复制后继节点并跳过

核心思路

问题关键: 参数只有待删除节点 node,没有头节点;单链表又无法向前找到 node 的前驱,所以不能执行常规的 prev.next = node.next

等价转换: 题目验证的是链表的值序列。先把后继节点的值复制到 node,再跳过后继节点,就能让原来 node 的值从序列中消失。

操作前局部序列为 X -> Y -> rest;复制后是 Y -> Y -> rest;跳过后继后变成 Y -> rest。因此值 X 被删除,后面的值及顺序保持不变。

适用边界: 题目保证 node 不是尾节点,这是解法成立的必要条件。对象身份上,被移出链表的是原后继节点,传入的 node 仍在链表中但值已改变;若业务依赖节点身份或存在外部节点引用,这种技巧就不等价。

解题步骤

  1. node.next.val 复制到 node.val
  2. node.next = node.next.next,跳过原后继节点。
  3. 无需返回头节点,修改会通过共享的节点对象对调用方生效。

口述示例: 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. 删除中间节点 简单 与本题同一技巧的另一份题面,可用来验证是否真的理解而非背诵