目录
[一、LeetCode 83 移除排序链表中的重复元素(保留一个)](#一、LeetCode 83 移除排序链表中的重复元素(保留一个))
[深度思路(盒子 - 标签 - 纸条模型)](#深度思路(盒子 - 标签 - 纸条模型))
[易踩坑点 & 底层原理](#易踩坑点 & 底层原理)
[二、LeetCode 237 删除链表中的节点(无法访问头节点)](#二、LeetCode 237 删除链表中的节点(无法访问头节点))
[深度思路(盒子 - 标签 - 纸条模型)](#深度思路(盒子 - 标签 - 纸条模型))
[易踩坑点 & 底层原理](#易踩坑点 & 底层原理)
[三、LeetCode 82 删除排序链表中的重复元素 II(全删)](#三、LeetCode 82 删除排序链表中的重复元素 II(全删))
[深度思路(盒子 - 标签 - 纸条模型)](#深度思路(盒子 - 标签 - 纸条模型))
[易踩坑点 & 底层原理](#易踩坑点 & 底层原理)
一、LeetCode 83 移除排序链表中的重复元素(保留一个)
题目核心
已排序链表中,重复元素仅保留一个(如 1→1→1→2→3→3 → 1→2→3)。
核心难点拆解
- 为什么不能直接比较
cur和cur.next? 若让cur初始指向head,比较cur.val == cur.next.val,会导致:- 空指针风险:当链表只剩最后一个节点时,
cur.next为null; - 锚点丢失:
cur移动后无法回溯,连续重复节点删不干净(如1→1→1会剩最后一个 1,但中间的重复删不彻底)。
- 空指针风险:当链表只剩最后一个节点时,
dummy哑节点的底层价值 :并非仅为 "避免删头节点",而是提供一个永远非空的 "根锚点" ,让cur可以稳定锚定 "已保留的最后一个不重复节点的前驱",避免遍历过程中链表断裂。
深度思路(盒子 - 标签 - 纸条模型)
| 模型元素 | 角色与逻辑 |
|---|---|
| 盒子 | 链表节点实体(如 1、1、2),排序特性保证 "重复节点必连续",无需跨区间检查; |
| 标签 | - dummy:贴在虚拟盒子(val=0)上,永久锚点,不移动;- cur:贴在 "已保留最后一个不重复节点的前驱盒子"(初始贴 dummy),仅在无重复时后移; |
| 纸条 | 删除的本质是修改 cur.next(纸条指向),跳过重复盒子;连续重复时 cur 不移动,持续修改纸条直到无重复。 |
代码实现
java
class Solution {
public ListNode deleteDuplicates(ListNode head) {
// 关键点1:空链表防御(基础边界,所有链表题必加)
if (head == null) return null;
// 关键点2:dummy哑节点------根锚点,避免头节点重复时的边界问题
ListNode dummy = new ListNode(0, head);
ListNode cur = dummy; // cur锚定"已保留节点的前驱",核心标签
// 关键点3:循环条件双层防御------避免cur.next或cur.next.next为null时的空指针
while (cur.next != null && cur.next.next != null) {
if (cur.next.val == cur.next.next.val) {
// 难点1:连续重复时,cur不移动,仅修改纸条跳过重复节点
// 例:1→1→1,第一次跳过第二个1,cur仍在dummy,第二次跳过第三个1
cur.next = cur.next.next;
} else {
// 无重复时,cur才后移------保证cur始终锚定"有效前驱"
cur = cur.next;
}
}
// 关键点4:返回dummy.next而非head------head可能已被跳过(如链表全重复时)
return dummy.next;
}
}
易踩坑点 & 底层原理
- 坑 1:循环条件只写
cur.next != null→ 会访问cur.next.next导致空指针; - 坑 2:连续重复时移动
cur→ 如1→1→1,cur 移到第一个 1 后,后续重复节点无法被跳过; - 原理:排序链表的重复是 "连续的",因此只需 "原地跳过",无需额外存储 / 回溯。
二、LeetCode 237 删除链表中的节点(无法访问头节点)
题目核心
仅给定待删除节点 node(非尾节点),无链表头节点访问权限,要求删除该节点(如 4→5→1→9 删 5 → 4→1→9)。
核心难点拆解
- 为什么不能直接删除
node节点? 链表的节点删除本质是 "修改前驱节点的next",但本题无表头,无法找到node的前驱节点;且链表节点是 "引用类型",直接置空node仅会让当前标签失效,链表结构未变。 - "偷梁换柱" 的底层逻辑 :链表的 "节点价值" 在于
val和next,而非节点本身的内存地址 ------ 因此可以复用node的内存空间,替换其内容为下一个节点的内容,再删除下一个节点,等价于 "逻辑删除node"。 - 为什么题目限定 "非尾节点"? 若
node是尾节点,node.next为null,无法复制内容,此方法失效(尾节点删除必须依赖前驱节点)。
深度思路(盒子 - 标签 - 纸条模型)
| 模型元素 | 角色与逻辑 |
|---|---|
| 盒子 | 待删除盒子(如 5)、下一个盒子(如 1);复用待删除盒子的 "物理空间",替换其 "内容"; |
| 标签 | 仅能访问 node 标签(贴在待删除盒子上),无其他锚点标签; |
| 纸条 | 先复制下一个盒子的 val 到当前盒子,再修改当前盒子的纸条(node.next),跳过下一个盒子; |
代码实现
java
class Solution {
public void deleteNode(ListNode node) {
// 关键点1:复制下一个盒子的内容到当前盒子------核心逻辑,偷梁换柱
// 例:node是5,node.next是1 → node.val = 1,此时链表变为4→1→1→9
node.val = node.next.val;
// 关键点2:修改纸条,跳过下一个盒子------删除"被复制的下一个盒子"
// 例:node.next = 1.next = 9,最终链表4→1→9,等价于删除了原5节点
node.next = node.next.next;
}
}
易踩坑点 & 底层原理
- 坑 1:试图直接
node = null→ 仅让当前标签失效,链表结构无变化(4→5→1→9 仍存在); - 坑 2:忽略 "非尾节点" 限制 → 若 node 是尾节点,
node.next.val会空指针; - 原理:链表的 "节点标识" 是逻辑上的(val+next),而非物理上的(内存地址),这是 "偷梁换柱" 能成立的核心。
三、LeetCode 82 删除排序链表中的重复元素 II(全删)
题目核心
已排序链表中,所有重复出现的元素全部删除 ,仅保留无重复的元素(如 1→2→3→3→4→4→5 → 1→2→5;1→1→1→2→3 → 2→3)。
核心难点拆解
- 与 83 题的核心差异:83 题是 "保留一个重复元素",只需 "逐个跳过";82 题是 "全删重复元素",需 "定位重复区间的首尾,批量跳过整个区间"。
- 为什么需要
temp标签遍历重复区间? 若仅用cur单次比较cur.next和cur.next.next,无法处理 "超过 2 个的连续重复"(如1→1→1),必须用temp走到重复区间的最后一个节点,才能精准跳过整个区间。 - 循环条件的多层防御 :
cur.next != null && cur.next.next != null是 "基础防御",temp != null && temp.next != null是 "区间遍历防御",缺一不可 ------ 否则会在重复区间末尾访问null.next导致空指针。
深度思路(盒子 - 标签 - 纸条模型)
| 模型元素 | 角色与逻辑 |
|---|---|
| 盒子 | 重复区间内的所有盒子需 "批量跳过",无重复的盒子需保留; |
| 标签 | - dummy:永久根锚点;- cur:贴在 "待判断区间的前驱盒子"(初始贴 dummy),仅在无重复时后移;- temp:遍历重复区间的临时标签,找到区间末尾; |
| 纸条 | cur.next 直接指向重复区间的下一个盒子,批量删除整个区间的盒子; |
代码实现
java
class Solution {
public ListNode deleteDuplicates(ListNode head) {
// 关键点1:双层边界防御------空链表/单节点链表直接返回
if (head == null || head.next == null) return head;
// 关键点2:dummy哑节点------避免头节点全重复时的边界问题(如1→1→2)
ListNode dummy = new ListNode(0, head);
ListNode cur = dummy; // cur锚定"待判断区间的前驱"
while (cur.next != null && cur.next.next != null) {
// 发现重复区间的起点
if (cur.next.val == cur.next.next.val) {
// 关键点3:temp标签遍历重复区间,找到区间最后一个节点
ListNode temp = cur.next;
// 区间遍历防御:temp != null 避免temp.next空指针
while (temp != null && temp.next != null && temp.val == temp.next.val) {
temp = temp.next; // 走到重复区间的最后一个节点
}
// 关键点4:批量跳过整个重复区间------核心逻辑
// 例:1→1→1→2,temp走到第三个1,cur.next = 2,直接跳过所有1
cur.next = temp.next;
} else {
// 无重复时,cur才后移------保证cur始终锚定"有效前驱"
cur = cur.next;
}
}
return dummy.next;
}
}
易踩坑点 & 底层原理
- 坑 1:
temp循环条件漏写temp != null→ 重复区间末尾temp.next为 null,temp.val空指针; - 坑 2:找到重复区间后直接
cur.next = cur.next.next→ 仅跳过一个重复节点,无法处理多重复(如 1→1→1 仍会剩一个 1); - 原理:排序链表的重复区间是 "连续的",因此只需一次遍历找到区间末尾,即可批量删除,时间复杂度仍为 O (n)。
四、跨题深度对比:链表删除的底层逻辑
| 维度 | 83 题(重复留一) | 237 题(指定节点删除) | 82 题(重复全删) |
|---|---|---|---|
| 核心策略 | 逐个跳过重复节点 | 偷梁换柱(内容替换) | 批量跳过重复区间 |
| 锚点依赖 | 依赖 dummy 做根锚点 | 无锚点(仅目标节点) | 依赖 dummy 做根锚点 |
| 空指针防御 | 双层循环条件 | 无(题目限定非尾节点) | 双层循环 + 区间遍历防御 |
| 核心难点 | 连续重复时 cur 不移动 | 理解 "节点逻辑删除" | 定位重复区间的首尾 |
| 底层原理 | 排序链表的连续性 | 链表节点的引用特性 | 排序链表的区间连续性 |
通用核心原则(所有链表删除题的底层逻辑)
- 锚点优先 :只要涉及 "删除头节点" 或 "前驱节点不可知",优先用
dummy哑节点做根锚点,避免链表断裂; - 标签不碰原始锚点 :始终用
cur/temp等临时标签移动,dummy/head等原始锚点仅做初始定位; - 删除本质是改纸条 :所有删除操作均不销毁节点(物理删除),而是修改
next指向(逻辑删除); - 边界防御前置:所有链表题先处理 "空链表 / 单节点链表",避免后续循环的空指针。
进阶思考(深度延伸)
- 若链表未排序,83/82 题该如何修改?→ 需要用哈希表记录已出现的 val,遍历链表时跳过重复值(时间 O (n),空间 O (n));
- 237 题若允许删除尾节点,该如何处理?→ 必须遍历链表找到尾节点的前驱(时间 O (n)),或改用双向链表(空间换时间)。
