一、题目是啥?一句话说清
给你一个已经排序的链表,删除所有重复数字的节点(包括本身),只留下不同的数字,并返回处理后的链表。
示例:
- 输入:1 → 1 → 2 → 3 → 3
- 输出:2(因为所有1和3都被删除,只留下2)
二、解题核心
使用虚拟头节点简化操作,遍历链表,对于每个节点,检查其后是否有重复节点,如果有则跳过整个重复序列。 这就像处理一排已经排序的队伍,如果发现连续多个人身高相同,就让所有这些人都离开队伍。
三、关键在哪里?(3个核心点)
想理解并解决这道题,必须抓住以下三个关键点:
1. 虚拟头节点(Dummy Node)的使用
- 是什么:在原始链表前添加一个不存储实际数据的节点。
- 为什么重要:因为头节点可能被删除(如示例中头节点1被删除),使用虚拟头节点可以避免单独处理头节点变化的情况。
2. 跳过整个重复序列
- 是什么:当发现重复时,不是只跳过下一个节点,而是找到重复序列的结束位置,然后一次性跳过所有重复节点。
- 为什么重要:因为题目要求删除所有重复数字的节点,而不是保留一个。
3. 指针操作的安全性
- 是什么:在遍历链表时,需要确保指针不为空,避免空指针异常。
- 为什么重要:因为可能操作到链表的末尾,需要谨慎检查指针的next属性。
四、看图理解流程(通俗理解版本)
让我们用链表 1 → 1 → 2 → 3 → 3 的例子来可视化过程:
-
初始化:
- 创建虚拟头节点
dummy,其 next 指向头节点 1。 - 设置当前指针
curr指向dummy。 - 初始状态:dummy → 1 → 1 → 2 → 3 → 3
- 创建虚拟头节点
-
第一轮检查:
curr指向 dummy,检查curr.next(1) 和curr.next.next(1) 的值是否相等。- 相等,说明有重复,需要删除所有值为1的节点。
- 找到第一个值不为1的节点(节点2)。
- 将
curr.next指向节点2(跳过所有1)。 - 链表状态:dummy → 2 → 3 → 3
curr仍然指向 dummy(因为可能新的开头还有重复)。
-
第二轮检查:
curr指向 dummy,检查curr.next(2) 和curr.next.next(3) 的值是否相等。- 不相等,说明没有重复,移动
curr到curr.next(指向2)。 - 链表状态:dummy → 2 → 3 → 3
-
第三轮检查:
curr指向2,检查curr.next(3) 和curr.next.next(3) 的值是否相等。- 相等,说明有重复,需要删除所有值为3的节点。
- 找到第一个值不为3的节点(null)。
- 将
curr.next指向 null(跳过所有3)。 - 链表状态:dummy → 2
-
结束:
- 返回
dummy.next,即节点2。
- 返回
五、C++ 代码实现(附详细注释)
#include <iostream>
using namespace std;
// 链表节点定义
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
class Solution {
public:
ListNode* deleteDuplicates(ListNode* head) {
// 处理空链表或单节点链表
if (head == nullptr || head->next == nullptr) {
return head;
}
// 创建虚拟头节点,简化操作
ListNode* dummy = new ListNode(0);
dummy->next = head;
ListNode* curr = dummy; // 当前指针,指向当前处理节点的前一个节点
while (curr->next != nullptr && curr->next->next != nullptr) {
// 检查当前节点的下一个节点和下下个节点是否值相等
if (curr->next->val == curr->next->next->val) {
int duplicateVal = curr->next->val; // 记录重复值
// 跳过所有值为duplicateVal的节点
while (curr->next != nullptr && curr->next->val == duplicateVal) {
ListNode* temp = curr->next;
curr->next = curr->next->next;
delete temp; // 释放内存(实际面试中可能不需要)
}
} else {
// 没有重复,移动到下一个节点
curr = curr->next;
}
}
ListNode* newHead = dummy->next;
delete dummy; // 释放虚拟头节点内存
return newHead;
}
};
// 辅助函数:打印链表
void printList(ListNode* head) {
while (head != nullptr) {
cout << head->val << " ";
head = head->next;
}
cout << endl;
}
// 测试代码
int main() {
// 创建示例链表:1->1->2->3->3
ListNode* head = new ListNode(1, new ListNode(1, new ListNode(2, new ListNode(3, new ListNode(3))));
Solution solution;
ListNode* result = solution.deleteDuplicates(head);
printList(result); // 输出:2
// 释放内存(实际面试中可能不需要完整释放)
return 0;
}
六、注意事项
- 内存管理:在C++中,如果删除了节点,最好释放其内存,但面试中通常更关注算法逻辑,可以不写delete。
- 指针判空:在访问
curr->next或curr->next->next之前,确保curr和curr->next不为nullptr,避免空指针异常。 - 循环条件:外部循环检查
curr->next和curr->next->next,内部循环检查curr->next。 - 特殊情况:链表为空或只有一个节点时,直接返回,不需要处理。
七、总结
理解此题的关键在于:
- 使用虚拟头节点:处理头节点可能被删除的情况。
- 跳过整个重复序列:当发现重复时,找到重复序列的结束位置,一次性跳过所有重复节点。
- 谨慎的指针操作:确保在操作指针时不会出现空指针异常。
掌握这三点,你就能高效解决删除排序链表中的重复元素 II 问题。这道题是链表操作的进阶题目,考察了对链表遍历和指针操作的深入理解。多练习几次,注意细节,就能熟练运用。