Leetcode 99 删除排序链表中的重复元素 | 合并两个链表

1 题目

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
  • 题目数据保证链表已经按升序 排列

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 题目

1669. 合并两个链表

给你两个链表 list1list2 ,它们包含的元素分别为 n 个和 m 个。

请你将 list1 中下标从 ab 的全部节点都删除,并将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 <= 104
  • 1 <= a <= b < list1.length - 1
  • 1 <= 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 节点的内存,避免内存泄漏

可优化的点

  1. dummy 节点是多余的:这道题中链表头节点永远不会被删除(即使头节点重复,也是删除后面的重复节点),所以完全不需要 dummy 节点,直接操作原链表即可,能减少内存开销和代码复杂度。
  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;
        }
        
        // 直接使用原链表头节点,无需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 插入中间
  • 能独立完成链表的拼接逻辑,这对新手来说是很大的进步!

主要问题与优化点

  1. 时间效率问题 :你的代码中用for循环从 a 遍历到 b+1,这会导致重复遍历;而且外层的while循环会遍历整个 list1,实际上我们只需要找到关键节点即可。
  2. 边界处理风险cur->next!= nullptr的判断可能会导致如果 a 是最后一个节点时出现问题(虽然题目约束 a <= b < list1.length-1,不会触发,但代码鲁棒性不足)。
  3. list2 指针被修改:你直接移动了传入的 list2 指针,虽然不影响功能,但会改变原链表的结构,不是好的编程习惯。
  4. 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;
    }
};

优化点解释

  1. 时间复杂度优化
    • 原代码:外层 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 长度),实际执行效率更高。
  2. 代码可读性提升:将找前驱、找后继、找 list2 尾节点拆分成独立步骤,逻辑更清晰。
  3. 避免修改入参 :用list2Tail遍历 list2,保留原list2指针不变,符合良好的编程规范。

三、关于 dummy 节点的理解

你提到对 dummy 节点理解不深,这里给你清晰的总结:

dummy 节点(哑节点 / 哨兵节点)的使用场景

  1. 必须使用的场景

    • 链表头节点可能被删除时(比如删除链表中所有值为 val 的节点,头节点也可能是 val)。
    • 合并两个有序链表时,不知道哪个链表的头节点更小,需要 dummy 节点作为结果链表的起始。
    • 反转链表时(尤其是需要处理边界情况时)。
  2. 不需要使用的场景

    • 链表头节点确定不会被修改 / 删除时(比如你做的这两道题)。
    • 只需要在链表中间插入 / 删除节点时。

dummy 节点的核心作用

  • 统一链表操作逻辑,避免单独处理头节点的边界情况。
  • 保存链表的原始头节点(当头节点可能被修改时)。

总结

  1. 核心进步:你能独立完成链表题,掌握了链表遍历、节点拼接的核心逻辑,这是最关键的!
  2. 优化方向
    • 减少不必要的 dummy 节点,降低代码复杂度和内存开销。
    • 避免重复遍历,提升代码时间效率。
    • 注意内存泄漏和入参修改的问题,养成良好的编程习惯。
  3. dummy 节点关键点:只有当头节点可能被修改 / 删除时才需要用,否则可以直接操作原链表。

继续保持这个势头!你已经能独立解决链表问题,接下来可以关注代码的效率和规范性,你的进步会更快。

相关推荐
创作者mateo17 小时前
PyTorch 入门笔记配套【完整练习代码】
人工智能·pytorch·笔记
YangYang9YangYan17 小时前
中专大数据技术专业学习数据分析的价值分析
大数据·学习·数据分析
米汤爱学习17 小时前
stable-diffusion-webui【笔记】
笔记·stable diffusion
testpassportcn18 小时前
Fortinet FCSS_SDW_AR-7.4 認證介紹|Fortinet Secure SD-WAN 高級路由專家考試
网络·学习·改行学it
光影少年18 小时前
vite为什么速度快?
前端·学习
王老师青少年编程18 小时前
信奥赛C++提高组csp-s之欧拉回路
c++·算法·csp·欧拉回路·信奥赛·csp-s·提高组
墨有66618 小时前
数学分析栈的出栈顺序:从算法判断到数学本质(卡特兰数初探)
c++·算法·数学建模
zhutoutoutousan18 小时前
氛围数学学习:用游戏化思维征服抽象数学
学习·算法·游戏
爱的瀚海天空18 小时前
claude code使用学习记录--安装环境和程序
学习