链表去重是算法面试中的经典问题,尤其当链表已经排序时,我们可以利用有序性设计出高效解法。这道题的核心在于理解链表指针操作的精妙之处。
链表去重的基本思路很简单:遍历链表,遇到重复元素就跳过。但实际操作中,指针移动的时机和条件判断的细节往往成为面试官的考察重点。我们先来看一个直观的例子:
假设输入链表是 1 -> 1 -> 2 -> 3 -> 3,经过处理后应该变成 1 -> 2 -> 3。注意这里我们只需要保留每个值的一个实例,而不是完全删除所有重复元素(这与另一道变种题不同)。
最直接的解法是使用单指针遍历链表:
javascript复制function deleteDuplicates(head) {
let current = head;
while (current !== null && current.next !== null) {
if (current.val === current.next.val) {
current.next = current.next.next;
} else {
current = current.next;
}
}
return head;
}
这段代码看似简单,但有几个关键点需要注意:
current.next !== null 检查链表问题最容易出错的就是边界条件处理。让我们仔细分析几种特殊情况:
1->1->1,需要正确处理连续重复提示:在面试中,主动提及这些边界条件的处理能展示你的全面思考能力。
链表操作的核心在于理解指针的指向。当我们发现重复时:
code复制current -> node1 -> node2 -> ...
val=a val=a
执行 current.next = current.next.next 后:
code复制current -> node2 -> ...
val=a val=a
注意此时 current 仍然指向 node1,只是 node1 的 next 现在指向了 node2。这种"原地不动"的策略是处理连续重复的关键。
我们只遍历链表一次,时间复杂度是 O(n),其中 n 是链表长度。这是最优的,因为至少需要检查每个节点一次。
只使用了常数级别的额外空间(current 指针),空间复杂度是 O(1)。这也是题目要求的。
如果链表未排序,我们需要使用哈希表来记录已出现的值:
javascript复制function deleteDuplicatesUnsorted(head) {
const seen = new Set();
let prev = null;
let current = head;
while (current !== null) {
if (seen.has(current.val)) {
prev.next = current.next;
} else {
seen.add(current.val);
prev = current;
}
current = current.next;
}
return head;
}
这种解法:
相比之下,原题的排序链表解法更高效。
javascript复制// 错误示范
if (current.val === current.next.val) {
current.next = current.next.next;
current = current.next; // 错误!可能跳过检查
}
javascript复制// 危险!
while (current.next !== null) {
// 如果 current 为 null 会报错
}
javascript复制// 忘记检查 head 是否为 null
let current = head;
while (current.next !== null) { // 如果 head 为 null 会报错
// ...
}
使用简单的测试用例逐步验证:
[] (空链表)[1] (单节点)[1,1,1] (全重复)[1,2,3] (无重复)[1,1,2,3,3] (混合情况)画图辅助理解指针变化:
使用 console.log 打印中间状态:
javascript复制while (...) {
console.log('Current:', current.val);
if (...) {
console.log('Found duplicate, skipping');
}
}
在 JavaScript 中实现链表操作需要注意:
javascript复制function ListNode(val, next) {
this.val = (val===undefined ? 0 : val);
this.next = (next===undefined ? null : next);
}
javascript复制// 使用 === 而不是 ==
if (current.val === current.next.val)
虽然算法逻辑相同,但不同语言的实现细节有差异:
Python:
python复制def deleteDuplicates(self, head):
current = head
while current and current.next:
if current.val == current.next.val:
current.next = current.next.next
else:
current = current.next
return head
Java:
java复制public ListNode deleteDuplicates(ListNode head) {
ListNode current = head;
while (current != null && current.next != null) {
if (current.val == current.next.val) {
current.next = current.next.next;
} else {
current = current.next;
}
}
return head;
}
当面试官提出这个问题时,建议采取以下步骤:
面试官可能会追问:
如果要完全删除所有重复元素(不保留任何副本)怎么办?
如果链表很大但重复很少,如何优化?
如何测试这个函数?
链表去重算法在实际中有多种应用:
Remove Duplicates from Sorted List II:
Remove Duplicates from Unsorted List:
Remove Nth Node From End of List:
虽然算法复杂度已经是 O(n),但在实际工程中还可以考虑:
javascript复制/**
* 删除排序链表中的重复元素
* @param {ListNode} head 链表头节点
* @return {ListNode} 去重后的链表头
*/
function deleteDuplicates(head) {
// 实现代码
}
为了深入理解链表操作,推荐以下资源:
链表去重问题看似简单,但包含了链表操作的核心思想。通过这道题,我总结了几个重要经验:
在实际面试中,我建议先口头解释思路,获得面试官确认后再写代码。写完代码后,主动用几个测试用例验证,展示你的严谨性。