
本文详解 LeetCode 第 83 题“删除排序链表中的重复元素”的常见逻辑错误与正确解法,重点剖析 head != head.next 的误用、空指针风险、结果指针更新时机等关键陷阱,并提供两种稳健实现方案。
本文详解 leetcode 第 83 题“删除排序链表中的重复元素”的常见逻辑错误与正确解法,重点剖析 `head != head.next` 的误用、空指针风险、结果指针更新时机等关键陷阱,并提供两种稳健实现方案。
在解决 LeetCode #83 —— “删除排序链表中的重复元素”时,一个典型误区是误将引用比较当作值比较使用。原始代码中 if (head != head.next) 这一条件始终为 true:因为 head 和 head.next 是两个不同的对象引用(除非链表成环,但本题输入为普通单链表),该判断完全无法检测数值是否重复,纯属逻辑失效。
更严重的是,原代码未做 head.next == null 的前置校验,直接访问 head.next.val 会导致 NullPointerException;同时,result = result.next 被置于循环末尾,导致无论是否跳过重复节点,结果链表都会无条件追加一个新节点——这正是输出 [1,1,2,3,3] 而非 [1,2,3] 的根本原因。
✅ 正确思路应分为两类实现方式:
方式一:双指针原地修改(推荐,空间 O(1))
利用链表已排序特性,仅需一次遍历,通过调整 next 指针跳过连续重复节点:
public ListNode deleteDuplicates(ListNode head) {
ListNode temp = head;
while (temp != null && temp.next != null) {
if (temp.val == temp.next.val) {
temp.next = temp.next.next; // 跳过重复节点
} else {
temp = temp.next; // 仅当值不同时才移动指针
}
}
return head;
}
⚠️ 注意:此方法不创建新节点,直接修改原链表结构,时间复杂度 O(n),空间复杂度 O(1),且天然规避空指针问题(因 while 条件已确保 temp.next != null 才进入循环体)。
方式二:虚拟头节点 + 显式构建(清晰易懂)
若倾向新建结果链表,必须严格控制 result 指针的移动时机:仅当确认当前节点为非重复首节点时,才将其接入结果链并推进 result:
public ListNode deleteDuplicates(ListNode head) {
ListNode dummy = new ListNode(-1);
ListNode result = dummy;
while (head != null) {
// 关键判断:当前节点是末尾,或与下一节点值不同 → 保留
if (head.next == null || head.val != head.next.val) {
result.next = head;
result = result.next; // ✅ 仅在此处推进 result
}
head = head.next; // ✅ head 始终推进(无论是否保留)
}
return dummy.next;
}
? 核心修正点:
- head.next == null 作为安全兜底,避免空指针;
- result 的移动被严格限制在 if 块内,杜绝无效追加;
- head = head.next 移至循环末尾,保证每次迭代都前进,防止死循环。
总结与避坑指南
- ❌ 禁止使用 head != head.next 判断重复——这是引用比较,毫无业务意义;
- ✅ 必须使用 head.val == head.next.val(并前置 head.next != null 校验);
- ✅ 结果指针(如 result)只在确定保留该节点时才更新;
- ✅ 输入链表为空(head == null)或单节点时,两种解法均能正确返回原链表;
- ? 本题依赖“链表已排序”前提,因此只需比较相邻节点,无需哈希表或额外存储。
掌握上述逻辑差异,不仅能通过 LeetCode #83,更能夯实链表操作中指针控制与边界处理的核心能力。











