1 题目
给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。
示例 1:

输入:head = [1,1,2]
输出:[1,2]
示例 2:

输入:head = [1,1,2,3,3]
输出:[1,2,3]
提示:
- 链表中节点数目在范围
[0, 300]内 -100 <= Node.val <= 100- 题目数据保证链表已经按升序 排列
2 代码实现
没有难度,秒掉了,没有什么考察的难点。
cpp
/**
* 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 == nullptr){
return nullptr;
}
ListNode*dummy = new ListNode(0);
dummy -> next = head ;
ListNode* cur = head ;
while(cur -> next != nullptr){
if (cur -> next -> val == cur -> val){
cur -> next = cur -> next -> next ;
}else{
cur = cur -> next ;
}
}
head = dummy -> next;
delete dummy;
return head ;
}
};
3 题目
给你两个链表 list1 和 list2 ,它们包含的元素分别为 n 个和 m 个。
请你将 list1 中下标从 a 到 b 的全部节点都删除,并将list2 接在被删除节点的位置。
下图中蓝色边和节点展示了操作后的结果:

请你返回结果链表的头指针。
示例 1:

输入:list1 = [10,1,13,6,9,5], a = 3, b = 4, list2 = [1000000,1000001,1000002]
输出:[10,1,13,1000000,1000001,1000002,5]
解释:我们删除 list1 中下标为 3 和 4 的两个节点,并将 list2 接在该位置。上图中蓝色的边和节点为答案链表。
示例 2:

输入:list1 = [0,1,2,3,4,5,6], a = 2, b = 5, list2 = [1000000,1000001,1000002,1000003,1000004]
输出:[0,1,1000000,1000001,1000002,1000003,1000004,6]
解释:上图中蓝色的边和节点为答案链表。
提示:
3 <= list1.length <= 1041 <= a <= b < list1.length - 11 <= list2.length <= 104
4 代码实现
居然自己写出来了!!!今天两题都是自己手敲出来的!!!
cpp
/**
* 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* mergeInBetween(ListNode* list1, int a, int b, ListNode* list2) {
int counter = 0 ;
ListNode* dummy = new ListNode(0);
dummy -> next = list1;
ListNode* cur = list1;
while(cur -> next!= nullptr){
counter ++;
if(counter == a ){
ListNode* p = cur -> next;
for(int i = a ; i < b + 1 ;++i){
p = p -> next ;
}
cur -> next = list2;
while(list2 -> next!= nullptr){
list2 = list2 -> next;
}
list2 -> next = p ;
}else{
cur = cur -> next;
}
}
dummy -> next = list1;
delete dummy;
return list1;
}
};
但其实我觉得自己写的和史没区别,思路很简单粗暴:
1.list1遍历到a,同时有counter计数下标
2.遍历到a以后存储要连接的节点,这段尾巴连接list2的头
3.a一直往下遍历到b。list2的尾巴连接到list1的剩余部分
5 小结
可喜可贺,这次没有依赖ai,没有依靠题解。但至少自己写通过了!
但是,我知道自己写的只是通过的程度,时空复杂度没考虑,效率没考虑,哑节点也是按照经验放觉得直接操作要删除的list的head不安全,实际上的理解不是很深。
用ai老师来给我指出一点优化和值得反思的地方,尽管这对我来说是一次成功!
首先要恭喜你!能独立完成这两道链表题真的非常棒,这是实实在在的进步。接下来我会针对你写的代码,从优化思路、代码健壮性、空间 / 时间效率 以及核心知识点理解几个方面给你分析和建议。
一、83. 删除排序链表中的重复元素 - 代码分析与优化
你的代码亮点
- 思路正确:利用链表有序的特性,逐个比较当前节点和下一个节点的值
- 处理了空链表的边界情况
- 释放了 dummy 节点的内存,避免内存泄漏
可优化的点
- dummy 节点是多余的:这道题中链表头节点永远不会被删除(即使头节点重复,也是删除后面的重复节点),所以完全不需要 dummy 节点,直接操作原链表即可,能减少内存开销和代码复杂度。
- 代码简化:可以进一步精简逻辑,让代码更易读。
优化后的代码
cpp
/**
* 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 == nullptr) {
return nullptr;
}
// 直接使用原链表头节点,无需dummy
ListNode* cur = head;
while (cur->next != nullptr) {
if (cur->val == cur->next->val) {
// 保存要删除的节点,避免内存泄漏(可选,但养成好习惯)
ListNode* temp = cur->next;
cur->next = cur->next->next;
delete temp; // 释放重复节点的内存
} else {
cur = cur->next;
}
}
return head;
}
};
核心优化点解释
- 移除 dummy 节点:因为排序链表的重复节点都是在当前节点之后,头节点永远保留,所以 dummy 节点是多余的,移除后代码更简洁,也减少了内存分配 / 释放的开销。
- 内存泄漏修复:原代码中被跳过的重复节点没有被释放,虽然 LeetCode 评测不会检测内存泄漏,但实际开发中这是必须注意的问题。
二、1669. 合并两个链表 - 代码分析与优化
你的代码亮点
- 核心思路正确:找到 list1 的 a 位置前驱节点、b 位置后继节点,将 list2 插入中间
- 能独立完成链表的拼接逻辑,这对新手来说是很大的进步!
主要问题与优化点
- 时间效率问题 :你的代码中用
for循环从 a 遍历到 b+1,这会导致重复遍历;而且外层的while循环会遍历整个 list1,实际上我们只需要找到关键节点即可。 - 边界处理风险 :
cur->next!= nullptr的判断可能会导致如果 a 是最后一个节点时出现问题(虽然题目约束 a <= b < list1.length-1,不会触发,但代码鲁棒性不足)。 - list2 指针被修改:你直接移动了传入的 list2 指针,虽然不影响功能,但会改变原链表的结构,不是好的编程习惯。
- dummy 节点多余:和上一题一样,list1 的头节点不会被修改,无需 dummy 节点。
优化后的代码
cpp
/**
* 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* mergeInBetween(ListNode* list1, int a, int b, ListNode* list2) {
// 1. 找到list1中第a-1个节点(要插入list2的前驱节点)
ListNode* preA = list1;
for (int i = 0; i < a - 1; ++i) {
preA = preA->next;
}
// 2. 找到list1中第b+1个节点(要插入list2的后继节点)
ListNode* postB = preA;
for (int i = a - 1; i < b + 1; ++i) {
postB = postB->next;
}
// 3. 找到list2的尾节点(不修改原list2指针)
ListNode* list2Tail = list2;
while (list2Tail->next != nullptr) {
list2Tail = list2Tail->next;
}
// 4. 拼接链表
preA->next = list2; // 前驱节点连接list2头
list2Tail->next = postB; // list2尾连接后继节点
return list1;
}
};
优化点解释
- 时间复杂度优化 :
- 原代码:外层 while 遍历整个 list1(O (n)),内层 for 循环遍历 b-a+1 个节点(O (b-a)),总时间复杂度 O (n + b-a)
- 优化后:仅遍历到关键节点,总时间复杂度 O (a + (b-a+1) + m) = O (b + m)(m 是 list2 长度),实际执行效率更高。
- 代码可读性提升:将找前驱、找后继、找 list2 尾节点拆分成独立步骤,逻辑更清晰。
- 避免修改入参 :用
list2Tail遍历 list2,保留原list2指针不变,符合良好的编程规范。
三、关于 dummy 节点的理解
你提到对 dummy 节点理解不深,这里给你清晰的总结:
dummy 节点(哑节点 / 哨兵节点)的使用场景
-
必须使用的场景:
- 链表头节点可能被删除时(比如删除链表中所有值为 val 的节点,头节点也可能是 val)。
- 合并两个有序链表时,不知道哪个链表的头节点更小,需要 dummy 节点作为结果链表的起始。
- 反转链表时(尤其是需要处理边界情况时)。
-
不需要使用的场景:
- 链表头节点确定不会被修改 / 删除时(比如你做的这两道题)。
- 只需要在链表中间插入 / 删除节点时。
dummy 节点的核心作用
- 统一链表操作逻辑,避免单独处理头节点的边界情况。
- 保存链表的原始头节点(当头节点可能被修改时)。
总结
- 核心进步:你能独立完成链表题,掌握了链表遍历、节点拼接的核心逻辑,这是最关键的!
- 优化方向 :
- 减少不必要的 dummy 节点,降低代码复杂度和内存开销。
- 避免重复遍历,提升代码时间效率。
- 注意内存泄漏和入参修改的问题,养成良好的编程习惯。
- dummy 节点关键点:只有当头节点可能被修改 / 删除时才需要用,否则可以直接操作原链表。
继续保持这个势头!你已经能独立解决链表问题,接下来可以关注代码的效率和规范性,你的进步会更快。