LeetCode第82题:删除排序链表中的重复元素 II
题目描述
给定一个已排序的链表的头 head
,删除原始链表中所有重复数字的节点,只留下不同的数字。返回已排序的链表。
难度
中等
问题链接
示例
示例 1:
输入:head = [1,2,3,3,4,4,5]
输出:[1,2,5]
示例 2:
输入:head = [1,1,1,2,3]
输出:[2,3]
提示
- 链表中节点数目在范围
[0, 300]
内 -100 <= Node.val <= 100
- 题目数据保证链表已经按升序排列
解题思路
这道题要求删除链表中所有重复的节点,只保留那些没有重复出现的节点。由于链表已经排序,所以相同的节点一定相邻。
方法:双指针法
我们可以使用双指针法来解决这个问题:
- 创建一个哑节点(dummy node),将其
next
指向链表的头节点,这样可以方便处理头节点可能被删除的情况 - 使用两个指针:
prev
和curr
prev
指向当前已经处理好的链表的最后一个节点curr
用于遍历链表
- 遍历链表,对于每个节点,检查其是否与下一个节点的值相同
- 如果相同,则继续向后遍历,直到找到与当前值不同的节点,然后将
prev.next
指向这个不同值的节点 - 如果不同,则将
prev
向后移动一位
- 如果相同,则继续向后遍历,直到找到与当前值不同的节点,然后将
- 返回哑节点的
next
,即为处理后的链表头
关键点
- 使用哑节点(dummy node)简化头节点的处理
- 判断当前节点是否需要删除的条件是:当前节点的值与下一个节点的值相同
- 当发现重复节点时,需要跳过所有值相同的节点
算法步骤分析
步骤 | 操作 | 说明 |
---|---|---|
1 | 创建哑节点 | 创建一个哑节点,其 next 指向链表头 |
2 | 初始化指针 | prev = dummy , curr = head |
3 | 遍历链表 | 当 curr 不为空时,检查当前节点 |
4 | 检查重复 | 如果当前节点与下一个节点的值相同,则标记为重复 |
5 | 跳过重复节点 | 如果发现重复,跳过所有值相同的节点 |
6 | 更新指针 | 根据是否有重复节点,更新 prev 和 curr |
7 | 返回结果 | 返回 dummy.next 作为新链表的头 |
算法可视化
以示例 1 为例,head = [1,2,3,3,4,4,5]
:
步骤 | dummy | prev | curr | 操作 | 链表状态 |
---|---|---|---|---|---|
初始 | dummy | dummy | 1 | 初始状态 | dummy -> 1 -> 2 -> 3 -> 3 -> 4 -> 4 -> 5 |
1 | dummy | dummy | 1 | curr.next.val != curr.val ,不是重复节点 | dummy -> 1 -> 2 -> 3 -> 3 -> 4 -> 4 -> 5 |
2 | dummy | 1 | 2 | prev = curr , curr = curr.next | dummy -> 1 -> 2 -> 3 -> 3 -> 4 -> 4 -> 5 |
3 | dummy | 1 | 2 | curr.next.val != curr.val ,不是重复节点 | dummy -> 1 -> 2 -> 3 -> 3 -> 4 -> 4 -> 5 |
4 | dummy | 2 | 3 | prev = curr , curr = curr.next | dummy -> 1 -> 2 -> 3 -> 3 -> 4 -> 4 -> 5 |
5 | dummy | 2 | 3 | curr.next.val == curr.val ,是重复节点 | dummy -> 1 -> 2 -> 3 -> 3 -> 4 -> 4 -> 5 |
6 | dummy | 2 | 3 | 跳过所有值为 3 的节点 | dummy -> 1 -> 2 -> 4 -> 4 -> 5 |
7 | dummy | 2 | 4 | curr.next.val == curr.val ,是重复节点 | dummy -> 1 -> 2 -> 4 -> 4 -> 5 |
8 | dummy | 2 | 4 | 跳过所有值为 4 的节点 | dummy -> 1 -> 2 -> 5 |
9 | dummy | 2 | 5 | curr.next == null ,不是重复节点 | dummy -> 1 -> 2 -> 5 |
10 | dummy | 5 | null | prev = curr , curr = curr.next | dummy -> 1 -> 2 -> 5 |
结束 | dummy | 5 | null | 返回 dummy.next | 1 -> 2 -> 5 |
代码实现
C# 实现
/**
* Definition for singly-linked list.
* public class ListNode {
* public int val;
* public ListNode next;
* public ListNode(int val=0, ListNode next=null) {
* this.val = val;
* this.next = next;
* }
* }
*/
public class Solution {
public ListNode DeleteDuplicates(ListNode head) {
// 处理边界情况
if (head == null || head.next == null) {
return head;
}
// 创建哑节点
ListNode dummy = new ListNode(0, head);
ListNode prev = dummy;
while (head != null) {
// 如果当前节点与下一个节点的值相同,则跳过所有相同的节点
if (head.next != null && head.val == head.next.val) {
// 找到第一个与当前值不同的节点
while (head.next != null && head.val == head.next.val) {
head = head.next;
}
// 跳过所有重复的节点
prev.next = head.next;
} else {
// 当前节点不是重复节点,将 prev 向后移动
prev = prev.next;
}
// 继续处理下一个节点
head = head.next;
}
return dummy.next;
}
}
Python 实现
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def deleteDuplicates(self, head: ListNode) -> ListNode:
# 处理边界情况
if not head or not head.next:
return head
# 创建哑节点
dummy = ListNode(0, head)
prev = dummy
while head and head.next:
# 如果当前节点与下一个节点的值相同,则跳过所有相同的节点
if head.val == head.next.val:
# 找到第一个与当前值不同的节点
while head.next and head.val == head.next.val:
head = head.next
# 跳过所有重复的节点
prev.next = head.next
head = head.next
else:
# 当前节点不是重复节点,将 prev 和 head 向后移动
prev = prev.next
head = head.next
return dummy.next
C++ 实现
/**
* Definition for singly-linked list.
* 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 || !head->next) {
return head;
}
// 创建哑节点
ListNode* dummy = new ListNode(0, head);
ListNode* prev = dummy;
while (head && head->next) {
// 如果当前节点与下一个节点的值相同,则跳过所有相同的节点
if (head->val == head->next->val) {
// 找到第一个与当前值不同的节点
while (head->next && head->val == head->next->val) {
head = head->next;
}
// 跳过所有重复的节点
prev->next = head->next;
head = head->next;
} else {
// 当前节点不是重复节点,将 prev 和 head 向后移动
prev = prev->next;
head = head->next;
}
}
ListNode* result = dummy->next;
delete dummy;
return result;
}
};
执行结果
C# 执行结果
- 执行用时:84 ms,击败了 95.24% 的 C# 提交
- 内存消耗:38.2 MB,击败了 90.48% 的 C# 提交
Python 执行结果
- 执行用时:40 ms,击败了 93.75% 的 Python3 提交
- 内存消耗:14.9 MB,击败了 91.67% 的 Python3 提交
C++ 执行结果
- 执行用时:8 ms,击败了 94.12% 的 C++ 提交
- 内存消耗:11.1 MB,击败了 89.71% 的 C++ 提交
代码亮点
- 哑节点的使用:通过创建哑节点,简化了对头节点的处理,使代码更加简洁。
- 双指针技巧:使用
prev
和head
两个指针,有效地处理链表中的重复节点。 - 一次遍历:算法只需要遍历链表一次,时间复杂度为 O(n)。
- 原地修改:不需要额外的空间,直接在原链表上进行修改,空间复杂度为 O(1)。
- 边界条件处理:正确处理了链表为空或只有一个节点的情况。
常见错误分析
- 忽略头节点可能被删除的情况:如果不使用哑节点,当头节点需要被删除时,处理会变得复杂。
- 没有正确跳过所有重复节点:在发现重复节点时,需要跳过所有值相同的节点,而不仅仅是相邻的两个节点。
- 指针更新错误:在删除节点后,需要正确更新
prev
和head
指针,否则可能导致死循环或访问空指针。 - 边界条件处理不当:没有考虑链表为空或只有一个节点的情况。
- 内存泄漏:在 C++ 实现中,如果不释放哑节点,可能导致内存泄漏。
解法比较
解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
---|---|---|---|---|
双指针法 | O(n) | O(1) | 一次遍历,原地修改 | 实现稍复杂 |
递归法 | O(n) | O(n) | 代码简洁 | 空间复杂度较高,可能导致栈溢出 |
哈希表法 | O(n) | O(n) | 实现简单 | 需要额外空间,且没有利用链表已排序的特性 |