Leetcode 33

1 题目

21. 合并两个有序链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

示例 1:

复制代码
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]

示例 2:

复制代码
输入:l1 = [], l2 = []
输出:[]

示例 3:

复制代码
输入:l1 = [], l2 = [0]
输出:[0]

提示:

  • 两个链表的节点数目范围是 [0, 50]
  • -100 <= Node.val <= 100
  • l1l2 均按 非递减顺序 排列

2 代码实现(c)

cpp 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
typedef struct ListNode ListNode;
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
    ListNode * head = (ListNode*)malloc(sizeof(ListNode));
    ListNode * p= head;
    while(list1 != NULL && list2 != NULL){
        if(list1 -> val > list2 -> val){
            p -> next = list2;
            list2 = list2 -> next;
        }else{
            p -> next = list1;
            list1 = list1 -> next ;
        }
        p = p -> next;
    }
    if(list1 != NULL){
       p -> next = list1;
    } else{
        p -> next = list2;
    }
    ListNode *result = head -> next;
   return result;
    }

不会用哑节点,遇到了困难。

cpp 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
typedef struct ListNode ListNode;
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
    ListNode * head = (ListNode*)malloc(sizeof(ListNode));
    ListNode * p= head;
    while(list1 != NULL && list2 != NULL){
        if(list1 -> val > list2 -> val){
            p -> next = list2;
            list2 = list2 -> next;
        }else{
            p -> next = list1;
            list1 = list1 -> next ;
        }
    }
    if(list1 == NULL){
        while(list2 != NULL){
            p-> next = list2;
            list2 = list2 -> next;
        }
    }

    if(list2 == NULL){
        while(list1 != NULL){
            p-> next = list1;
            list1 = list1 -> next;
        }
    }
    return head;
}

你的代码思路是对的,但在使用哑节点(dummy node)时存在一个小问题,导致返回结果不正确。我们来分析并修正:

问题所在:未正确处理哑节点的「头指针偏移」

你创建了哑节点 head 作为临时头节点(这是正确的做法),但最终直接返回了 head,而实际上 head 是我们手动创建的「空节点」,真正的合并后链表的头节点应该是 head->next

此外,你的代码中拼接剩余节点的逻辑可以简化,不需要用 while 循环(因为链表本身是连续的,直接把剩余链表的头接过来即可)。

修正后的代码(正确使用哑节点):

cpp 复制代码
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
    // 创建哑节点(临时头节点),避免处理空链表的特殊情况
    ListNode* dummy = (ListNode*)malloc(sizeof(ListNode));
    ListNode* p = dummy; // p 作为当前节点的指针,用于拼接链表

    // 当两个链表都不为空时,比较节点值并拼接
    while (list1 != NULL && list2 != NULL) {
        if (list1->val <= list2->val) {
            p->next = list1;   // 拼接 list1 的当前节点
            list1 = list1->next; // list1 指针后移
        } else {
            p->next = list2;   // 拼接 list2 的当前节点
            list2 = list2->next; // list2 指针后移
        }
        p = p->next; // 当前指针后移,准备拼接下一个节点
    }

    // 拼接剩余的节点(如果有)
    // 当一个链表为空时,直接把另一个链表的剩余部分接过来
    if (list1 != NULL) {
        p->next = list1;
    } else {
        p->next = list2;
    }

    // 哑节点的 next 才是合并后链表的真正头节点
    ListNode* result = dummy->next;
    free(dummy); // 释放哑节点的内存(可选,刷题时可省略)
    return result;
}

哑节点的作用详解:

  1. 统一边界处理 :当 list1list2 为空时(比如 list1 = NULLlist2 = [1,2,3]),不需要单独判断「如何设置头节点」,因为哑节点 dummy 已经提供了一个临时的头,直接拼接即可。

  2. 简化指针操作 :不需要纠结「第一个节点该选谁」,无论两个链表是否为空,都可以通过 p->next 来拼接节点,最后返回 dummy->next 即可得到正确的头节点。

关键修正点:

  • 原代码中 p 指针在拼接节点后没有后移(p = p->next),导致后续拼接会覆盖之前的节点,修正后通过 p = p->next 保证指针始终指向当前链表的末尾。
  • 原代码用 while 循环拼接剩余节点是多余的,因为链表是连续的,直接 p->next = list1(或 list2)即可把剩余所有节点接过来。
  • 最终返回 dummy->next 而不是 dummy,因为 dummy 是我们手动创建的临时节点,本身不存储有效数据。

测试用例验证:

  • 输入 list1 = [1,2,4]list2 = [1,3,4] → 输出 [1,1,2,3,4,4](正确)。
  • 输入 list1 = []list2 = [] → 输出 [](正确)。
  • 输入 list1 = []list2 = [0] → 输出 [0](正确)。

这样修改后,哑节点的作用就体现出来了,代码更简洁且边界情况处理更优雅。

【为什么剩余拼接可以直接这么写?】

cpp 复制代码
 // 当一个链表为空时,直接把另一个链表的剩余部分接过来
    if (list1 != NULL) {
        p->next = list1;
    } else {
        p->next = list2;
    }

这么做的核心原因是链表的连续性 ------ 链表节点通过next指针串联成一个整体,只要拿到剩余链表的「头节点」,将其赋值给当前链表末尾节点的next,就能把整个剩余链表完整接过来,无需逐个节点循环拼接。

为什么可以直接这么写?

我们可以通过一个具体例子理解:假设 list1 = [1,2,4]list2 = [1,3,4]

  1. 循环比较拼接阶段结束后,list1 会指向 4(已拼接完 1,2),list2 会指向 NULL(已拼接完 1,3,4)。
  2. 此时判断 list1 != NULL,执行 p->next = list1
  3. 由于 list1 指向的 4 节点,其 next 本身就指向 NULL(原链表的末尾),所以赋值后,新链表的末尾会自动接上 4,无需额外处理后续节点。

简单来说,剩余链表本身就是一个完整的、连接好的结构 ,我们只需要 "搭一根线"(即赋值p->next),就能把它整体接入,不需要逐个 "搬运" 节点。

和原代码中 while 循环的对比

你的原代码用while循环逐个拼接剩余节点,虽然结果正确,但完全没必要,属于 "多做了无用功"。

两种方式的对比:

实现方式 核心逻辑 时间复杂度 效率
直接赋值p->next 利用链表连续性,一次赋值接入整个剩余链表 O(1) 高效
while 循环拼接 逐个遍历剩余节点,重复赋值p->next O (k)(k 为剩余节点数) 冗余

特殊场景验证

这种写法能覆盖所有剩余节点的场景,包括:

  1. list1 剩余,list2 为空:直接接 list1 剩余部分。
  2. list2 剩余,list1 为空:直接接 list2 剩余部分。
  3. 两者都为空:p->next 赋值为 NULL,最终返回的链表也为空,符合预期。

所以放心用这种写法,它是合并有序链表问题中处理剩余节点的最优解。

相关推荐
你们补药再卷啦18 分钟前
人工智能算法概览
人工智能·算法
cnxy18824 分钟前
围棋对弈Python程序开发完整指南:步骤3 - 气(Liberties)的计算算法设计
python·算法·深度优先
AndrewHZ34 分钟前
【图像处理基石】什么是光栅化?
图像处理·人工智能·算法·计算机视觉·3d·图形渲染·光栅化
小白菜又菜41 分钟前
Leetcode 944. Delete Columns to Make Sorted
算法·leetcode
我找到地球的支点啦1 小时前
Matlab系列(006) 一利用matlab保存txt文件和读取txt文件
开发语言·算法·matlab
Dev7z1 小时前
基于Matlab实现GRACE卫星重力数据的全球水储量变化估算与分析
人工智能·算法·matlab
爱喝热水的呀哈喽2 小时前
11题目汇总
算法
三斗米2 小时前
Transformer入门:一文读懂《Attention Is All You Need》
算法·架构
Swift社区3 小时前
LeetCode 458 - 可怜的小猪
算法·leetcode·职场和发展
AI科技星3 小时前
宇宙的像素:真空中一点如何编码无限星光
数据结构·人工智能·算法·机器学习·重构