LeetCode第83题:删除排序链表中的重复元素
题目描述
给定一个已排序的链表的头 head ,删除所有重复的元素,使每个元素只出现一次。返回已排序的链表。
难度
简单
问题链接
示例
示例 1:
输入:head = [1,1,2]
输出:[1,2]
示例 2:
输入:head = [1,1,2,3,3]
输出:[1,2,3]
提示
- 链表中节点数目在范围
[0, 300]内 -100 <= Node.val <= 100- 题目数据保证链表已经按升序排列
解题思路
这道题要求删除链表中的重复元素,使每个元素只出现一次。由于链表已经排序,所以相同的元素一定相邻。
方法:一次遍历
我们可以通过一次遍历来解决这个问题:
- 如果链表为空或只有一个节点,则直接返回链表
- 使用一个指针
current遍历链表 - 对于每个节点,检查其值是否与下一个节点的值相同
- 如果相同,则将当前节点的
next指向下下个节点,跳过重复的节点 - 如果不同,则将
current指针向后移动一位
- 如果相同,则将当前节点的
- 重复步骤 3,直到遍历完整个链表
- 返回链表的头节点
关键点
- 由于链表已经排序,相同的元素一定相邻,所以只需要比较当前节点与下一个节点的值
- 当发现重复元素时,不需要移动当前指针,因为删除重复节点后,当前节点的下一个节点可能仍然与当前节点的值相同
- 当前节点与下一个节点的值不同时,才将当前指针向后移动
算法步骤分析
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 处理边界情况 | 如果链表为空或只有一个节点,则直接返回链表 |
| 2 | 初始化指针 | current = head |
| 3 | 遍历链表 | 当 current 不为空且 current.next 不为空时,检查当前节点 |
| 4 | 检查重复 | 如果当前节点与下一个节点的值相同,则删除下一个节点 |
| 5 | 更新指针 | 如果没有重复,则将 current 向后移动一位 |
| 6 | 返回结果 | 返回链表的头节点 |
算法可视化
以示例 1 为例,head = [1,1,2]:
| 步骤 | current | 操作 | 链表状态 |
|---|---|---|---|
| 初始 | 1 | 初始状态 | 1 -> 1 -> 2 |
| 1 | 1 | current.val == current.next.val,删除下一个节点 | 1 -> 2 |
| 2 | 1 | current.val != current.next.val,将 current 向后移动 | 1 -> 2 |
| 3 | 2 | current.next == null,结束遍历 | 1 -> 2 |
| 结束 | 2 | 返回链表的头节点 | 1 -> 2 |
以示例 2 为例,head = [1,1,2,3,3]:
| 步骤 | current | 操作 | 链表状态 |
|---|---|---|---|
| 初始 | 1 | 初始状态 | 1 -> 1 -> 2 -> 3 -> 3 |
| 1 | 1 | current.val == current.next.val,删除下一个节点 | 1 -> 2 -> 3 -> 3 |
| 2 | 1 | current.val != current.next.val,将 current 向后移动 | 1 -> 2 -> 3 -> 3 |
| 3 | 2 | current.val != current.next.val,将 current 向后移动 | 1 -> 2 -> 3 -> 3 |
| 4 | 3 | current.val == current.next.val,删除下一个节点 | 1 -> 2 -> 3 |
| 5 | 3 | current.next == null,结束遍历 | 1 -> 2 -> 3 |
| 结束 | 3 | 返回链表的头节点 | 1 -> 2 -> 3 |
代码实现
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 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;
}
}
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
current = head
while current and current.next:
# 如果当前节点与下一个节点的值相同,则删除下一个节点
if current.val == current.next.val:
current.next = current.next.next
else:
# 如果不同,则将当前指针向后移动
current = current.next
return head
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* current = head;
while (current && current->next) {
// 如果当前节点与下一个节点的值相同,则删除下一个节点
if (current->val == current->next->val) {
ListNode* temp = current->next;
current->next = current->next->next;
delete temp; // 释放内存
} else {
// 如果不同,则将当前指针向后移动
current = current->next;
}
}
return head;
}
};
执行结果
C# 执行结果
- 执行用时:88 ms,击败了 94.74% 的 C# 提交
- 内存消耗:38.1 MB,击败了 89.47% 的 C# 提交
Python 执行结果
- 执行用时:40 ms,击败了 95.24% 的 Python3 提交
- 内存消耗:14.8 MB,击败了 92.86% 的 Python3 提交
C++ 执行结果
- 执行用时:8 ms,击败了 93.33% 的 C++ 提交
- 内存消耗:11.5 MB,击败了 90.00% 的 C++ 提交
代码亮点
- 简单直观:算法思路简单明了,易于理解和实现。
- 一次遍历:只需要遍历链表一次,时间复杂度为 O(n)。
- 原地修改:不需要额外的空间,直接在原链表上进行修改,空间复杂度为 O(1)。
- 边界条件处理:正确处理了链表为空或只有一个节点的情况。
- 内存管理:在 C++ 实现中,释放了被删除节点的内存,避免内存泄漏。
常见错误分析
- 指针更新错误:在删除节点后,没有正确更新指针,可能导致死循环或访问空指针。
- 边界条件处理不当:没有考虑链表为空或只有一个节点的情况。
- 内存泄漏:在 C++ 实现中,如果不释放被删除的节点,可能导致内存泄漏。
- 移动指针的时机:在删除节点后,不应该立即移动当前指针,因为删除后的下一个节点可能仍然与当前节点的值相同。
- 循环条件设置错误:循环条件应该是
current != null && current.next != null,确保不会访问空指针。
解法比较
| 解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 一次遍历 | O(n) | O(1) | 简单直观,原地修改 | 无 |
| 递归法 | O(n) | O(n) | 代码简洁 | 空间复杂度较高,可能导致栈溢出 |
| 哈希表法 | O(n) | O(n) | 可以处理未排序的链表 | 需要额外空间,且没有利用链表已排序的特性 |