目录
链表与其他数据结构的对比
与数组对比:
- 数组需要在删除元素时做大量数据移动和拷贝,时间和空间复杂度较高
- 链表只需要修改指针引用,时间和空间效率显著优于数组
与哈希表对比
- 哈希表需要额外空间存储键值对
- 插入和删除的时间复杂度也较链表大
- 不保证元素的顺序性
与二叉搜索树对比:
- 查找和删除节点时需要递归遍历树结构,编码实现复杂度高
- 需要大量指针引用,内存开销大
所以简而言之,链表主要优势是:
- 原生支持按顺序的数据结构
- 动态长度,插入和删除简单高效
- 指针引用让修改链表结构变简单
- 无需大量数据搬移,时间和空间复杂度低
题目描述
图片来源leetcode官网82题
提交代码
class Solution {
public:
ListNode* deleteDuplicates(ListNode* head) {
if (head == nullptr) {
return nullptr;
}
ListNode *dummy = new ListNode(0, head); // 虚拟头结点
ListNode *cur = dummy;
while (cur->next && cur->next->next ) {
if (cur->next->val == cur->next->next->val) { // 找到重复的元素
int val = cur->next->val;
cur->next = cur->next->next;
while (cur->next && cur->next->val == val) {
cur->next = cur->next->next;
}
}
else {
cur = cur->next; // 指向下一个元素
}
}
return dummy->next;
}
};
思路
当我第一次看到这个问题时,我脑海里首先浮现出以下几个关键点:
- 链表已排序
- 要删除重复节点
- 重复意味着值相同
- 要求不能打乱排序
那么我开始思考如何利用“链表已排序”这个条件来设计解法。排序意味着相同值的节点一定是相邻的。我可以遍历链表,一旦检测到当前节点和下一个节点的值相同,就是重复节点了。
然后我想到使用两个嵌套的while循环:
外层while循环用来遍历整个链表。内层while循环用来遍历连续的重复节点,就是当cur->next->val == cur->next->next->val
时,这就代表有重复,然后我可以修改链表链接跳过这些重复节点。
那么问题就转化为:
- 如何判断两个相邻节点是否是重复的? 通过值比较即可判断。
- 如何删除/跳过重复节点? 我可以直接修改 next 引用,将其指向重复序列的下一个非重复节点
为什么选择链表:
选择链表的结构来解决这个问题,主要是由于链表本身的特点及与题目的契合度最高。
首先,当我第一次看到这个问题时,脑海里浮现出“链表已排序”这个关键条件。排序意味着相同值的节点一定是相邻的。所以我可以利用这个条件来判断重复节点,这成为设计解法的关键起点。
然后,链表天然就是一个动态长度,有序的数据结构,正好符合了本题的输入输出要求。 链表通过指针/引用让节点连接在一起,可以非常容易地实现跳过某些节点的逻辑来删除重复元素,而又不影响链表的结构及元素顺序。
再者,如果使用其他数据结构比如数组,会需要大量元素的搬移操作,既增加实现难度,又降低效率。而链表只需要修改指针引用,简单高效完成删除操作。
最后,链表结构恰好契合了本题数据特征(排序、去重)的技术需求,可以写出非常简洁的解法。其它数据结构要么效率低,要么实现复杂,很难达到链表结构的简单与效率。
解决方案
1. 防御性编程:检查头结点是否为空,为空直接返回
2.初始化虚拟头结点: 首先,创建一个虚拟头结点(dummy),并将其指向原始链表的头结点。这个虚拟头结点的作用是简化边界情况的处理,同时也方便最终返回整理后的链表。
3.遍历链表: 使用指针 cur
遍历链表,检查当前节点 cur->next
和其后继节点 cur->next->next
是否存在。
4.检测重复元素: 如果当前节点和其后继节点的值相等,说明存在重复元素。在这种情况下,需要进行删除操作。
5.删除重复元素: 记录重复元素的值 val
,然后调整指针,将当前节点的下一个节点指向下下个节点,即 cur->next = cur->next->next
。接着,使用循环删除所有连续重复的节点,直到遇到不重复的节点为止。
6.移动指针: 如果当前节点和其后继节点的值不相等,说明没有重复元素,直接将指针 cur
移动到下一个节点,即 cur = cur->next
。
7.返回结果链表: 最终返回虚拟头结点的下一个节点,即去除重复元素后的已排序链表。