1. 问题背景与需求分析
链表去重是算法面试中的经典问题,尤其出现在各大科技公司的技术面试中。LeetCode第82题要求我们处理一个已排序的链表,删除所有含有重复数字的节点,只保留原始链表中没有重复出现的数字。
这个问题看似简单,但实际处理时需要特别注意指针操作和边界条件。与基础的去重问题(如LeetCode第83题)不同,本题要求将所有重复节点完全移除,而不仅仅是去重保留一个。这种差异使得问题复杂度从O(n)提升到了需要考虑更多边界情况的程度。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 链表数据结构回顾
2.1 单链表的基本结构
在C++中,单链表通常定义为:
cpp复制struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(nullptr) {}
};
在Python中,对应的定义是:
python复制class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
2.2 链表的遍历与操作
链表的核心操作包括:
- 遍历:从头节点开始,通过next指针依次访问每个节点
- 插入:修改节点的next指针指向新节点
- 删除:修改前驱节点的next指针,跳过要删除的节点
这些操作都需要特别注意指针的指向关系,尤其是在处理边界条件时(如头节点、尾节点、空链表等)。
3. 解题思路与算法设计
3.1 双指针法
解决本题最有效的方法是使用双指针:
- 创建哑节点(dummy node)作为新链表的头,简化边界处理
- 使用pre指针记录当前确定不重复的最后一个节点
- 使用cur指针遍历链表,检测重复序列
3.2 具体步骤
- 初始化dummy节点,pre指向dummy,cur指向head
- 当cur不为空时循环:
a. 如果cur与下一个节点值相同:
i. 记录重复值
ii. 跳过所有等于该值的节点
b. 否则:
i. 将pre.next指向cur
ii. pre移动到cur
c. cur移动到下一个节点 - 最后处理pre的next指针
- 返回dummy.next作为结果
4. 代码实现与详细解析
4.1 C++实现
cpp复制class Solution {
public:
ListNode* deleteDuplicates(ListNode* head) {
if (!head || !head->next) return head;
ListNode* dummy = new ListNode(0);
dummy->next = head;
ListNode* pre = dummy;
ListNode* cur = head;
while (cur) {
if (cur->next && cur->val == cur->next->val) {
int val = cur->val;
while (cur && cur->val == val) {
ListNode* temp = cur;
cur = cur->next;
delete temp;
}
pre->next = cur;
} else {
pre = cur;
cur = cur->next;
}
}
return dummy->next;
}
};
4.2 Python实现
python复制class Solution:
def deleteDuplicates(self, head: ListNode) -> ListNode:
if not head or not head.next:
return head
dummy = ListNode(0)
dummy.next = head
pre = dummy
cur = head
while cur:
if cur.next and cur.val == cur.next.val:
val = cur.val
while cur and cur.val == val:
cur = cur.next
pre.next = cur
else:
pre = cur
cur = cur.next
return dummy.next
4.3 关键点解析
- 哑节点的使用:简化了头节点可能被删除的情况处理
- 内存管理:C++版本需要手动释放删除的节点内存
- 循环条件:内层循环跳过所有重复值节点
- 指针更新:只有在确认节点不重复时才移动pre指针
5. 复杂度分析与优化
5.1 时间复杂度
算法的时间复杂度是O(n),其中n是链表的长度。每个节点最多被访问两次(一次由外层循环,一次由内层循环),因此总体时间复杂度是线性的。
5.2 空间复杂度
空间复杂度是O(1),因为我们只使用了常数个额外指针变量,没有使用与输入规模相关的额外空间。
5.3 可能的优化
- 可以提前判断链表是否有序,但题目已说明输入是有序链表
- 对于极长的重复序列,可以优化内层循环的指针移动方式
- 在特定语言环境下,可以利用语言特性简化代码(如Python的垃圾回收机制)
6. 边界条件与测试用例
6.1 常见边界情况
- 空链表:应返回空
- 单节点链表:应返回原链表
- 所有节点都重复:应返回空
- 头节点重复:需要正确处理
- 尾节点重复:需要正确处理
- 连续多个不同重复序列:需要全部正确处理
6.2 测试用例示例
python复制# 测试用例1:1->2->3->3->4->4->5
# 预期结果:1->2->5
# 测试用例2:1->1->1->2->3
# 预期结果:2->3
# 测试用例3:1->1->2->2->3->3
# 预期结果:空
# 测试用例4:1->2->3->4->5
# 预期结果:1->2->3->4->5
7. 常见错误与调试技巧
7.1 常见错误类型
- 指针丢失:在删除节点时没有正确维护链表连接
- 内存泄漏:C++中删除节点后没有释放内存
- 边界处理不当:没有正确处理头节点或尾节点重复的情况
- 循环条件错误:导致无限循环或提前退出
7.2 调试建议
- 使用小规模测试用例手动模拟指针移动
- 打印中间状态检查指针位置和链表结构
- 对于C++,使用valgrind等工具检测内存泄漏
- 逐步验证每个边界条件
8. 扩展与变种问题
8.1 LeetCode相似题目
-
- 删除排序链表中的重复元素(保留一个重复元素)
-
- 移除链表元素(删除所有等于给定值的节点)
-
- 删除链表的倒数第N个节点
-
- 删除链表中的节点(只给定要删除的节点)
8.2 实际应用场景
- 数据库查询结果去重
- 日志处理中的重复条目过滤
- 数据清洗过程中的异常值处理
- 网络数据包处理中的重复检测
9. 面试技巧与注意事项
- 先明确问题要求,与面试官确认边界条件
- 画图辅助理解指针操作过程
- 先描述思路再写代码,展示思考过程
- 写完代码后主动检查边界条件
- 讨论时间空间复杂度及其优化可能性
10. 个人经验与心得
在实际面试中遇到这类问题时,我发现以下策略很有效:
- 一定要使用哑节点简化边界处理,这能避免很多头疼的特殊情况
- 在纸上画出链表和指针移动的示意图,比单纯在脑中想象更可靠
- 对于指针操作类问题,先理清各个指针的角色和移动条件再编码
- 测试时要特别注意连续重复和头尾重复的情况
- 在C++实现中,记得及时释放删除的节点内存,这是面试官常关注的细节
