1. 问题背景与需求分析
链表去重是数据结构与算法中的经典问题,也是面试中的高频考点。LeetCode第83题"删除排序链表中的重复元素"要求我们给定一个已排序的链表,删除所有重复元素,使每个元素只出现一次。
这个问题看似简单,但涉及链表操作的多个核心概念:
- 链表节点的遍历与指针移动
- 节点间的连接关系维护
- 边界条件处理(如空链表、单节点链表等)
在实际工程中,类似的操作也经常出现。比如处理有序日志流时去除重复条目,或者在内存受限环境下处理有序数据集的去重问题。理解这个基础问题的解法,能为处理更复杂的链表问题打下坚实基础。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 链表基础与问题分析
2.1 链表数据结构回顾
链表是由节点组成的数据结构,每个节点包含:
- 数据域(存储元素值)
- 指针域(存储下一个节点的地址)
与数组不同,链表在内存中不是连续存储的,这使得插入和删除操作的时间复杂度为O(1),但随机访问需要O(n)时间。
python复制class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
2.2 问题具体分析
给定一个已排序的链表,我们需要:
- 遍历链表,比较当前节点与下一节点的值
- 当发现重复时,修改指针跳过重复节点
- 确保最终链表中每个值只出现一次
示例:
输入:1 → 1 → 2
输出:1 → 2
输入:1 → 1 → 2 → 3 → 3
输出:1 → 2 → 3
3. 解法实现与代码详解
3.1 迭代法实现
这是最直观的解法,使用单个指针遍历链表:
python复制def deleteDuplicates(head: ListNode) -> ListNode:
current = head
while current and current.next:
if current.val == current.next.val:
current.next = current.next.next
else:
current = current.next
return head
代码解析:
- 初始化current指针指向头节点
- 循环条件确保当前节点和下一节点都存在
- 值相同时,跳过下一节点(修改next指针)
- 值不同时,移动current指针
- 最终返回原头节点(头节点不会被删除)
时间复杂度:O(n),空间复杂度:O(1)
3.2 递归法实现
递归解法更简洁,但需要理解递归调用栈:
python复制def deleteDuplicates(head: ListNode) -> ListNode:
if not head or not head.next:
return head
head.next = deleteDuplicates(head.next)
return head.next if head.val == head.next.val else head
递归解析:
- 基线条件:空链表或单节点链表直接返回
- 递归处理后续节点
- 比较当前节点与处理后链表的头节点
- 值相同时跳过当前节点,不同时保留
注意:递归解法空间复杂度为O(n)(调用栈深度),在大链表情况下可能栈溢出
4. 边界条件与异常处理
实际编码中需要考虑的特殊情况:
- 空链表输入:直接返回None
- 单节点链表:无需处理,直接返回
- 全重复链表:如1→1→1,应返回1
- 末尾重复:如1→2→2,确保正确处理
测试用例示例:
python复制# 空链表
assert deleteDuplicates(None) is None
# 单节点
single_node = ListNode(1)
assert deleteDuplicates(single_node).val == 1
# 全重复
full_dup = ListNode(1, ListNode(1, ListNode(1)))
result = deleteDuplicates(full_dup)
assert result.val == 1 and result.next is None
5. 算法优化与变种问题
5.1 内存释放考虑
在C/C++等需要手动管理内存的语言中,我们需要注意释放被删除节点的内存:
cpp复制ListNode* deleteDuplicates(ListNode* head) {
ListNode* curr = head;
while (curr && curr->next) {
if (curr->val == curr->next->val) {
ListNode* temp = curr->next;
curr->next = temp->next;
delete temp; // 释放内存
} else {
curr = curr->next;
}
}
return head;
}
5.2 未排序链表去重
如果链表未排序,可以使用哈希表辅助:
python复制def deleteDuplicatesUnsorted(head: ListNode) -> ListNode:
seen = set()
dummy = ListNode(0)
dummy.next = head
current = dummy
while current.next:
if current.next.val in seen:
current.next = current.next.next
else:
seen.add(current.next.val)
current = current.next
return dummy.next
这种方法时间复杂度仍为O(n),但空间复杂度变为O(n)
6. 实际应用场景
链表去重算法在以下场景中有实际应用:
- 日志处理系统:去除连续的重复日志条目
- 数据库查询优化:合并相同的连续查询条件
- 网络数据包处理:去除重复的数据包序列
- 基因组数据分析:处理DNA序列中的重复片段
我在实际项目中曾用类似方法处理传感器数据。传感器会周期性上报数据,当数值未变化时会产生连续重复记录。使用这种算法可以有效减少存储空间和传输带宽。
7. 常见错误与调试技巧
新手常犯的错误包括:
- 指针移动过早:
python复制# 错误示例
while current:
if current.val == current.next.val: # 可能访问空指针
current.next = current.next.next
current = current.next # 移动过早
- 忽略头节点特殊情况:
python复制# 可能漏掉头节点重复的情况
if head.val == head.next.val:
head = head.next
调试建议:
- 使用小规模测试用例(0个、1个、2个相同、2个不同节点)
- 画图辅助理解指针变化
- 使用断点调试观察链表状态
8. 性能分析与比较
方法对比表:
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 迭代法 | O(n) | O(1) | 一般情况首选 |
| 递归法 | O(n) | O(n) | 小规模数据,代码简洁性要求高 |
| 哈希表法 | O(n) | O(n) | 未排序链表去重 |
在实际工程中,迭代法通常是首选,因为:
- 空间效率更高
- 没有递归栈溢出风险
- 代码可读性也不差
对于特别大的链表(百万节点以上),即使是O(n)算法也需要考虑:
- 内存局部性(链表遍历比数组慢)
- 并行化可能性(链表不易并行)
- 是否需要转为其他数据结构处理
9. 扩展练习与相关题目
为了巩固链表操作技能,建议尝试以下LeetCode题目:
-
- 删除排序链表中的重复元素 II(删除所有重复节点)
-
- 反转链表
-
- 环形链表(判断链表是否有环)
-
- 相交链表(找出两个链表的交点)
-
- 合并两个有序链表
特别是第82题,与本题目类似但要求更高:
- 需要删除所有重复元素的节点,而不仅是去重
- 需要使用哑节点(dummy node)技巧简化边界处理
- 需要维护前驱指针来跳过整个重复序列
我在面试候选人时,常会先问83题,再扩展到82题,观察候选人能否灵活调整思路。能处理好这种问题变种,说明对链表操作有深入理解。
