Appearance
删除排序链表的重复元素:一趟相邻比较
本文属于「408 算法拓展训练」——考纲内、真题代码题尚未考过的经典题。这道题和 2015·41 链表按绝对值去重看着像同一类题,其实性质完全不同——2015·41 的链表是任意顺序,靠值域标记数组换时间;本题的链表已排序,重复值必然相邻,靠有序性一趟解决,用不上标记数组。它是经典训练题,不是真题,也不押题。
题目
给定一个按值升序排列的单链表 head,删除所有重复出现的结点,使得每个值只保留一个,返回处理后的链表头。例如 1 → 1 → 2 → 3 → 3 → 3 → 4 处理后为 1 → 2 → 3 → 4。链表结点定义如下:
c
typedef struct node {
int val;
struct node *next;
} NODE;这道题没有「尽可能高效」的要求。按拓展训练导语的分类,它属于「直观即满分」型——直白写法就是最优解,力气花在把边界写准上。
直观解法:一趟相邻比较
已排序是这道题最重要的前提:值相同的结点在链表里必然挨在一起,不会被其他值隔开。这意味着判断「当前结点是不是重复」,只需要看它和紧挨着的下一个结点是否同值,完全不需要跟前面所有出现过的值比对。
c
NODE *deleteDuplicates(NODE *head) {
NODE *cur = head;
while (cur != NULL && cur->next != NULL) {
if (cur->val == cur->next->val) {
NODE *dup = cur->next;
cur->next = cur->next->next; // 跳过重复结点
free(dup);
// cur 不前进,继续跟新的 cur->next 比较,处理连续三个及以上重复的情况
} else {
cur = cur->next; // 值不同才前进
}
}
return head;
}拿 1 → 1 → 2 → 3 → 3 → 3 → 4 走一遍:cur 指向第一个 1,cur->next 也是 1,删掉后一个 1,cur 不动;此时 cur->next 变成 2,值不同,cur 前进到 2;cur->next 是 3,值不同,cur 前进到第一个 3;cur->next 是第二个 3,删掉,cur 不动;cur->next 变成第三个 3,仍相同,再删一次,cur 不动;此时 cur->next 变成 4,值不同,cur 前进到 4;cur->next 为 NULL,循环结束。结果 1 → 2 → 3 → 4 ✓——连续三个重复值被正确压缩成一个。
复杂度:cur 最多把整条链表走一遍(每次不是删除就是前进,两种操作合计不超过 n 次),时间
为什么它就是满分解
用总纲的「找浪费三问」审一遍,三问全部答「没有」:
- 有没有重复扫描? 没有。每个结点只在 cur 或 cur->next 的位置被检查一次,删除操作直接摘链,不需要回头重新扫描前面已经处理过的部分。
- 有没有重算已知的东西? 没有。判断「当前值是否重复」只看相邻的下一个结点,不需要维护任何「之前出现过哪些值」的额外记录。
- 有没有放着性质不用? 没有。题目给的「已排序」性质,正好被「重复值必然相邻」这个推论直接利用,一趟相邻比较就够了。
更根本的一条:删除链表里的重复元素,至少要把每个结点看一遍才能确认它是否重复——读一遍链表就是
和 2015·41 的区别,别混着用
这道题最容易和真题 2015·41搞混,两者表面都是「链表去重」,但依赖的性质完全不同:
- 本题:链表已经升序排列,重复值必然相邻,一趟相邻比较(cur 和 cur->next)就能删干净,不需要任何辅助空间。
- 2015·41:链表是任意顺序(结点顺序不保证有序),要删的是「绝对值相等」的结点,且相等的两个结点在链表里可能隔得很远。这种情况下相邻比较毫无意义,必须借助值域标记数组(利用 |data| ≤ n 这个值域约束)记住「这个绝对值是否已经出现过」,这才是空间换时间的用武之地。
一句话区分:相邻比较靠「有序性」,标记数组靠「值域约束」,用错了性质,本题套标记数组是杀鸡用牛刀(多开
真正的难点在这里
分不在优化上,在把这两处边界写准:
- 删除后 cur 不能前进。这是全题最容易写错的一步:删掉
cur->next之后,新的cur->next可能和cur仍然相同(连续三个及以上重复值),必须让 cur 停在原地、继续比较新的cur->next,直到不再相同才前进。如果每次判断后都无条件前进,连续重复值只会删掉一部分。 - 循环条件
cur != NULL && cur->next != NULL:既要防止空链表时访问cur->val越界,也要防止 cur 走到最后一个结点时访问cur->next->val越界。
一趟比较,看一遍
用 1 → 1 → 2 → 3 → 3 → 3 → 4 演示——连续三个 3 被逐个摘除、压缩成一个:
演示为 JavaScript 版本,逻辑与上文 C 代码一一对应。
别画蛇添足
这道题结构简单,过度设计的空间不大,但有一个常见的「想复杂了」:
- 想套用 2015·41 的值域标记数组。已排序链表根本用不上「记住出现过哪些值」这一套——相邻比较已经是
时间 空间,标记数组不但多开销空间,还得先假设一个值域范围,纯属多此一举。
下一步
- 看单链表基础操作的交互演示:单链表可视化
- 回到方法论源头:「暴力解到最优解」真题专题总纲
- 对比另一种去重的性质依赖:2015·41 链表按绝对值去重