一、题目是啥?一句话说清
给你一个已经排序的链表,删除所有重复的元素,使得每个元素只出现一次,并返回处理后的链表。
示例:
- 输入:1 → 1 → 2 → 3 → 3
- 输出:1 → 2 → 3
二、解题核心
利用链表已排序的特性,遍历链表,比较当前节点与下一个节点的值,如果相同就跳过下一个节点,否则移动到下一个节点。 这就像处理一排已经按身高排序的队伍,如果发现相邻两个人身高相同,就让后一个人离开队伍。
三、关键在哪里?(3个核心点)
想理解并解决这道题,必须抓住以下三个关键点:
1. 利用已排序的特性
- 是什么:因为链表已经排序,所有重复元素一定是相邻的。
- 为什么重要:这样只需要一次遍历就能删除所有重复元素,不需要使用额外数据结构(如哈希表)来记录已出现的元素。
2. 指针操作
- 是什么:使用一个指针遍历链表,比较当前节点与下一个节点的值。
- 为什么重要:通过调整指针的指向来"跳过"重复节点,而不是实际删除节点(在C++中需要注意内存管理,但算法题通常更关注逻辑)。
3. 边界条件处理
- 是什么:处理空链表或单节点链表的情况。
- 为什么重要:这些特殊情况不需要任何操作,直接返回即可,避免程序出错。
四、看图理解流程(通俗理解版本)
让我们用链表 1 → 1 → 2 → 3 → 3 的例子来可视化过程:
-
初始化:
- 设置当前指针
curr指向头节点 1。 - 链表状态:1 → 1 → 2 → 3 → 3
- 设置当前指针
-
第一轮比较:
- 比较
curr.val(1) 和curr.next.val(1),两者相等。 - 跳过下一个节点:让
curr.next指向curr.next.next(即第二个1指向2)。 - 链表状态:1 → 2 → 3 → 3
curr仍然指向第一个1(因为可能还有重复)。
- 比较
-
第二轮比较:
- 再次比较
curr.val(1) 和curr.next.val(2),两者不相等。 - 移动
curr到下一个节点(指向2)。 - 链表状态:1 → 2 → 3 → 3
- 再次比较
-
第三轮比较:
- 比较
curr.val(2) 和curr.next.val(3),两者不相等。 - 移动
curr到下一个节点(指向3)。 - 链表状态:1 → 2 → 3 → 3
- 比较
-
第四轮比较:
- 比较
curr.val(3) 和curr.next.val(3),两者相等。 - 跳过下一个节点:让
curr.next指向curr.next.next(即null)。 - 链表状态:1 → 2 → 3
curr仍然指向3。
- 比较
-
结束:
curr.next为null,循环结束。- 返回头节点1。
五、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* curr = head; // 当前指针
// 遍历链表
while (curr != nullptr && curr->next != nullptr) {
if (curr->val == curr->next->val) {
// 发现重复,跳过下一个节点
ListNode* duplicate = curr->next;
curr->next = curr->next->next;
delete duplicate; // 释放内存(实际面试中可能不需要)
} else {
// 没有重复,移动到下一个节点
curr = curr->next;
}
}
return head;
}
};
// 辅助函数:打印链表
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); // 输出:1 2 3
// 释放内存(实际面试中可能不需要完整释放)
return 0;
}
六、注意事项
- 内存管理:在C++中,如果跳过了节点,最好释放其内存,但面试中通常更关注算法逻辑,可以不写delete。
- 指针判空:在访问
curr->next之前,确保curr不为nullptr,避免空指针异常。 - 循环条件:循环条件需要同时检查
curr和curr->next,因为我们需要比较当前节点和下一个节点。 - 特殊情况:链表为空或只有一个节点时,直接返回,不需要处理。
七、总结
理解此题的关键在于:
- 利用已排序特性:重复元素一定相邻,只需一次遍历。
- 指针操作:通过调整指针的指向来跳过重复节点。
- 边界处理:正确处理空链表和单节点链表的情况。
掌握这三点,你就能高效解决删除排序链表中的重复元素问题。这道题是链表操作的基础题目,考察了对链表遍历和指针操作的理解。多练习几次,注意细节,就能熟练运用。