LeetCode 热题 100-----27. 合并两个有序链表

一、题目完整描述

题目要求

给定两个非递减升序排列 的单链表 list1list2,请你将两个链表合并为一个全新的非递减升序链表。合并规则为:直接拼接两个链表的原有所有节点,不新建额外数据节点,最终返回合并后新链表的头节点。

题目示例

示例1:输入:l1 = [1,2,4], l2 = [1,3,4] 输出:[1,1,2,3,4,4]

示例2:输入:l1 = [], l2 = [] 输出:[]

示例3:输入:l1 = [], l2 = [0] 输出:[0]

题目约束

  1. 两个链表的节点数量范围:[0, 50](支持空链表)

  2. 节点数值范围:-100 ≤ Node.val ≤ 100

  3. 两个输入链表均为非递减有序(后一个节点值 ≥ 前一个节点值)

二、零基础必备前置知识

本题是链表入门核心题,所有知识点从零讲解,零基础可完全看懂,所有概念、特性、操作全部讲透。

1. 什么是链表?(对比数组理解)

1.1 数组的特点(铺垫对比)

我们平时接触的列表/数组,是连续内存存储 。比如数组 [1,2,4],三个数字在内存中是紧紧挨在一起的,可以通过下标 0、1、2 直接找到任意元素。

数组缺点:插入、删除元素时,需要移动大量元素,效率低;固定长度,灵活性差。

1.2 单链表的定义

链表是非连续内存存储 的线性数据结构。简单来说:链表由无数个「节点」串联而成,每个节点只认识自己的下一个节点

单链表是最基础的链表类型,只能从头向尾遍历,不能反向遍历。

2. 链表节点的完整结构

本题用到的所有链表节点,固定包含两个核心属性,C++和Python定义完全对应:

2.1 属性1:val(数值域)

存储当前节点的具体数值,就是题目中我们需要排序、合并的数字。

2.2 属性2:next(指针/引用域)

这是链表的灵魂!next 存储的是「下一个节点的地址/引用」,用来连接下一个节点。

特殊值:空值 (C++:nullptr,Python:None),代表当前节点是链表的最后一个节点,后面没有节点了。

2.3 节点可视化示例

节点(1) → 节点(2) → 节点(4) → 空

解读:数值为1的节点,next指向数值为2的节点;数值为2的节点,next指向数值为4的节点;数值为4的节点,next为空,是尾节点。

3. 关键基础概念

3.1 空链表

没有任何节点的链表,直接等于 nullptr/None,对应题目输入 [],是本题高频边界条件。

3.2 有序链表(非递减链表)

题目核心条件:输入的两个链表都是非递减有序 。含义:链表中后一个节点的 val 永远大于等于前一个节点的 val

示例:1→2→41→3→4 都是合法输入,无需我们排序,只需要合并有序链表。

3.3 链表的核心操作(本题全部用到)

① 链表遍历 :从链表头节点开始,通过 next 不断向后移动,直到遇到空值停止。

② 链表拼接 :修改节点的 next 指向,让当前节点连接另一个节点,实现链表合并。

③ 指针移动:用一个临时指针变量,记录当前操作的节点,遍历过程中不断后移。

4. 哨兵节点

定义 :哨兵节点(虚拟头节点)是一个无实际数值意义 的临时节点,不存储有效数据,专门用来解决链表头节点边界问题

为什么要用哨兵?

如果不使用哨兵,合并时需要单独判断「头节点是谁」(哪个链表的第一个节点更小),代码繁琐、边界易错。

使用哨兵后:所有节点统一通过拼接方式添加,无需单独处理头节点,代码逻辑统一、零边界bug。

使用规则:最终结果丢弃哨兵节点,返回哨兵节点的下一个节点(真正的链表头)。

5. 递归基础(进阶解法必备)

递归核心:把大问题拆解为相同逻辑的小问题,直到触发终止条件,再逐层返回结果

递归三要素:终止条件、递推逻辑、返回值。本题递归就是「合并两个链表」的子问题拆解。

三、解题思路总览(由易到难)

本题提供两种标准解法,难度从低到高排序,适配不同学习阶段:

解法一:迭代法(首选、最优解) - 逻辑直观、无递归、空间复杂度O(1),面试最推荐

解法二:递归法(进阶解法) - 代码极简、逻辑抽象、利用系统栈,适合进阶学习

四、解法一:迭代法(超详细讲解+完整可运行代码)

1. 核心解题原理

利用哨兵节点 + 双指针遍历 ,同时遍历两个有序链表,每次选取两个链表中当前值更小的节点,拼接到新链表尾部,直到其中一个链表遍历完毕,最后将剩余未遍历的链表直接拼接在新链表末尾。

2. 分步算法流程(大白话逐步骤)

步骤1:创建虚拟哨兵节点,作为新链表的临时起点;

步骤2:定义cur指针,指向哨兵节点,负责记录新链表的当前尾部位置

步骤3:循环遍历两个输入链表,只要两个链表都还有节点,就对比当前头节点数值;

步骤4:将数值更小的节点拼接在cur指针后面,同时该链表头指针后移、cur指针后移;

步骤5:当任意一个链表遍历为空,停止循环;

步骤6:将非空的剩余链表直接拼接在cur指针尾部(剩余链表本身有序,无需遍历);

步骤7:返回哨兵节点的下一个节点,即为合并后的完整有序链表。

3. C++ 完整可运行代码(逐行超详细注释+自测主函数)

该代码可直接复制运行,包含:节点定义、合并算法、自定义链表构建函数、链表打印函数、主函数测试(题目示例+自定义输入测试)

cpp 复制代码
// 引入标准输入输出头文件,用于自定义输入输出测试
#include <iostream>
#include <vector>
using namespace std;

// 【链表节点结构体定义】题目标准定义,零基础必懂
struct ListNode {
    // 节点存储的数值
    int val;
    // 指向下一个节点的指针,初始为空
    ListNode *next;

    // 无参构造:创建空节点,值为0,next为空
    ListNode() : val(0), next(nullptr) {}
    // 单参构造:创建指定数值的节点,next为空(尾节点)
    ListNode(int x) : val(x), next(nullptr) {}
    // 全参构造:创建指定数值、指定下一个节点的节点
    ListNode(int x, ListNode *next) : val(x), next(next) {}
};

// 【工具函数1:通过数组快速构建链表】方便自定义测试
ListNode* createList(vector<int> nums) {
    // 空数组直接返回空链表
    if (nums.empty()) return nullptr;
    // 创建哨兵节点,简化构建逻辑
    ListNode* dummy = new ListNode();
    // cur指针记录当前尾部节点
    ListNode* cur = dummy;
    // 遍历数组,逐个创建节点拼接
    for (int num : nums) {
        cur->next = new ListNode(num);
        cur = cur->next;
    }
    // 返回真实头节点
    return dummy->next;
}

// 【工具函数2:打印链表】可视化输出结果,方便查看测试效果
void printList(ListNode* head) {
    ListNode* cur = head;
    while (cur != nullptr) {
        cout << cur->val << " ";
        cur = cur->next;
    }
    cout << endl;
}

// 【核心算法:合并两个有序链表】迭代法
class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        // 1. 创建哨兵虚拟头节点,无实际数值,解决头节点边界问题
        ListNode* dummy = new ListNode(-1);
        // 2. cur指针:始终指向新链表的【最后一个节点】,用于拼接新节点
        ListNode* cur = dummy;

        // 3. 循环遍历:两个链表都不为空时,持续对比拼接
        while (list1 != nullptr && list2 != nullptr) {
            // 对比两个链表当前头节点的数值
            if (list1->val < list2->val) {
                // list1节点更小:拼接list1当前节点到新链表尾部
                cur->next = list1;
                // list1指针后移,准备下一次对比
                list1 = list1->next;
            } else {
                // list2节点更小或相等:拼接list2当前节点到新链表尾部
                cur->next = list2;
                // list2指针后移,准备下一次对比
                list2 = list2->next;
            }
            // 新链表尾部指针后移,等待拼接下一个节点
            cur = cur->next;
        }

        // 4. 处理剩余节点:一个链表遍历完,直接拼接另一个链表的剩余部分
        // 三目运算符:list1为空则接list2,否则接list1
        cur->next = (list1 == nullptr) ? list2 : list1;

        // 5. 返回新链表真实头节点(跳过哨兵节点)
        return dummy->next;
    }
};

// 【主函数:所有测试用例执行入口,支持自定义输入】
int main() {
    Solution sol;

    // 测试用例1:题目示例1 l1=[1,2,4], l2=[1,3,4]
    cout << "测试用例1结果:";
    ListNode* l1 = createList({1,2,4});
    ListNode* l2 = createList({1,3,4});
    printList(sol.mergeTwoLists(l1, l2));

    // 测试用例2:题目示例2 两个空链表
    cout << "测试用例2结果:";
    ListNode* l3 = createList({});
    ListNode* l4 = createList({});
    printList(sol.mergeTwoLists(l3, l4));

    // 测试用例3:题目示例3 一空一非空
    cout << "测试用例3结果:";
    ListNode* l5 = createList({});
    ListNode* l6 = createList({0});
    printList(sol.mergeTwoLists(l5, l6));

    // 自定义测试用例:负数、多节点测试
    cout << "自定义测试用例结果:";
    ListNode* l7 = createList({-5,0,2});
    ListNode* l8 = createList({-3,1,4,6});
    printList(sol.mergeTwoLists(l7, l8));

    return 0;
}

4. Python 完整可运行代码(逐行超详细注释+自测主函数)

完全适配Python3,可直接运行,包含工具函数、核心算法、多组测试用例、自定义输入

python 复制代码
# 导入类型注解工具,规范代码格式
from typing import Optional, List

# 【链表节点类定义】题目标准定义,零基础必懂
class ListNode:
    # 节点初始化方法:默认值0,默认无下一个节点
    def __init__(self, val=0, next=None):
        self.val = val   # 节点数值
        self.next = next # 下一个节点引用

# 【工具函数1:数组转链表】自定义测试专用
def create_list(nums: List[int]) -> Optional[ListNode]:
    # 空数组返回空链表
    if not nums:
        return None
    # 创建虚拟头节点
    dummy = ListNode()
    cur = dummy
    # 遍历数组批量创建节点
    for num in nums:
        cur.next = ListNode(num)
        cur = cur.next
    return dummy.next

# 【工具函数2:打印链表】可视化输出结果
def print_list(head: Optional[ListNode]) -> None:
    res = []
    cur = head
    while cur:
        res.append(str(cur.val))
        cur = cur.next
    print(" ".join(res))

# 【核心算法:迭代法合并有序链表】
class Solution:
    def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
        # 1. 创建哨兵虚拟节点,统一拼接逻辑,规避头节点边界问题
        dummy = ListNode(-1)
        # 2. cur指针:记录新链表的当前尾部位置,初始指向哨兵
        cur = dummy

        # 3. 双链表同时遍历,均非空时循环对比
        while list1 and list2:
            # 选取数值更小的节点拼接
            if list1.val < list2.val:
                # 拼接list1当前节点
                cur.next = list1
                # list1指针后移
                list1 = list1.next
            else:
                # 拼接list2当前节点
                cur.next = list2
                # list2指针后移
                list2 = list2.next
            # 新链表尾部后移,等待下一次拼接
            cur = cur.next

        # 4. 拼接剩余未遍历的节点(有序直接拼接,无需循环)
        cur.next = list1 if list1 else list2

        # 5. 返回真实合并后的链表头节点
        return dummy.next

# 【主函数:批量测试所有用例+自定义测试】
if __name__ == "__main__":
    sol = Solution()

    # 题目示例1测试
    print("测试用例1结果:", end="")
    l1 = create_list([1,2,4])
    l2 = create_list([1,3,4])
    print_list(sol.mergeTwoLists(l1, l2))

    # 题目示例2测试
    print("测试用例2结果:", end="")
    l3 = create_list([])
    l4 = create_list([])
    print_list(sol.mergeTwoLists(l3, l4))

    # 题目示例3测试
    print("测试用例3结果:", end="")
    l5 = create_list([])
    l6 = create_list([0])
    print_list(sol.mergeTwoLists(l5, l6))

    # 自定义复杂测试用例(含负数、递增节点)
    print("自定义测试用例结果:", end="")
    l7 = create_list([-1,2,5,7])
    l8 = create_list([0,3,6,8])
    print_list(sol.mergeTwoLists(l7, l8))

5. 逐行运行流程模拟(以示例1为例:l1=[1,2,4], l2=[1,3,4])

初始状态:dummy(-1) → cur=dummy,list1=1,list2=1

第1轮循环:list1.val(1) == list2.val(1) → 拼接list2节点,list2后移为3,cur后移,新链表:-1→1

第2轮循环:list1.val(1) < list2.val(3) → 拼接list1节点,list1后移为2,cur后移,新链表:-1→1→1

第3轮循环:list1.val(2) < list2.val(3) → 拼接list1节点,list1后移为4,cur后移,新链表:-1→1→1→2

第4轮循环:list1.val(4) > list2.val(3) → 拼接list2节点,list2后移为4,cur后移,新链表:-1→1→1→2→3

第5轮循环:list1.val(4) == list2.val(4) → 拼接list2节点,list2后移为空,循环结束

收尾:list2为空,拼接剩余list1(4),最终链表:1→1→2→3→4→4

五、解法二:递归法(进阶讲解+完整可运行代码)

1. 递归核心原理

递归核心思想:分治思想,将「合并两个长链表」拆解为「合并两个子链表」,层层递归,直到触发终止条件,再逐层回溯拼接结果。

2. 递归三要素

终止条件:任意一个链表为空,直接返回另一个链表(无需合并,剩余链表直接作为结果)

递推逻辑:对比两个链表头节点,保留数值更小的节点,让该节点的next指向「剩余两个链表的合并结果」

返回值:每一层递归返回当前层最小的节点,最终拼接成完整链表

3. C++ 完整可运行递归代码(带注释+测试主函数)

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

// 链表节点定义
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) {}
};

// 数组构建链表工具函数
ListNode* createList(vector<int> nums) {
    if (nums.empty()) return nullptr;
    ListNode* dummy = new ListNode();
    ListNode* cur = dummy;
    for (int num : nums) {
        cur->next = new ListNode(num);
        cur = cur->next;
    }
    return dummy->next;
}

// 链表打印工具函数
void printList(ListNode* head) {
    ListNode* cur = head;
    while (cur != nullptr) {
        cout << cur->val << " ";
        cur = cur->next;
    }
    cout << endl;
}

// 递归解法核心算法
class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        // 递归终止条件1:list1为空,直接返回list2剩余节点
        if (list1 == nullptr) return list2;
        // 递归终止条件2:list2为空,直接返回list1剩余节点
        if (list2 == nullptr) return list1;

        // 对比两个链表头节点,选取更小的节点递归拼接
        if (list1->val < list2->val) {
            // list1节点更小:当前节点的next = 剩余list1和完整list2的合并结果
            list1->next = mergeTwoLists(list1->next, list2);
            // 返回当前最小节点
            return list1;
        } else {
            // list2节点更小或相等:当前节点的next = 完整list1和剩余list2的合并结果
            list2->next = mergeTwoLists(list1, list2->next);
            // 返回当前最小节点
            return list2;
        }
    }
};

// 测试主函数
int main() {
    Solution sol;
    cout << "示例1结果:";
    printList(sol.mergeTwoLists(createList({1,2,4}), createList({1,3,4})));
    cout << "示例2结果:";
    printList(sol.mergeTwoLists(createList({}), createList({})));
    cout << "示例3结果:";
    printList(sol.mergeTwoLists(createList({}), createList({0})));
    cout << "自定义测试结果:";
    printList(sol.mergeTwoLists(createList({-2,1,3}), createList({-1,2,4})));
    return 0;
}

4. Python 完整可运行递归代码(带注释+测试主函数)

python 复制代码
from typing import Optional, List

# 链表节点定义
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

# 数组转链表工具函数
def create_list(nums: List[int]) -> Optional[ListNode]:
    if not nums:
        return None
    dummy = ListNode()
    cur = dummy
    for num in nums:
        cur.next = ListNode(num)
        cur = cur.next
    return dummy.next

# 链表打印工具函数
def print_list(head: Optional[ListNode]) -> None:
    res = []
    cur = head
    while cur:
        res.append(str(cur.val))
        cur = cur.next
    print(" ".join(res))

# 递归核心算法
class Solution:
    def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
        # 递归终止条件:空链表直接返回另一个
        if not list1:
            return list2
        if not list2:
            return list1
        
        # 递归递推逻辑:选小节点,递归合并剩余部分
        if list1.val < list2.val:
            # 当前list1节点保留,后续节点由剩余链表合并生成
            list1.next = self.mergeTwoLists(list1.next, list2)
            return list1
        else:
            # 当前list2节点保留,后续节点由剩余链表合并生成
            list2.next = self.mergeTwoLists(list1, list2.next)
            return list2

# 批量测试
if __name__ == "__main__":
    sol = Solution()
    print("示例1结果:", end="")
    print_list(sol.mergeTwoLists(create_list([1,2,4]), create_list([1,3,4])))
    print("示例2结果:", end="")
    print_list(sol.mergeTwoLists(create_list([]), create_list([])))
    print("示例3结果:", end="")
    print_list(sol.mergeTwoLists(create_list([]), create_list([0])))
    print("自定义测试结果:", end="")
    print_list(sol.mergeTwoLists(create_list([-3,0,5]), create_list([-2,2,4])))

5. 递归逐层调用流程(示例1)

初始调用:merge(1→2→4, 1→3→4)

层1:1=1,保留右侧1,调用merge(1→2→4, 3→4)

层2:1<3,保留左侧1,调用merge(2→4, 3→4)

层3:2<3,保留左侧2,调用merge(4, 3→4)

层4:4>3,保留右侧3,调用merge(4,4)

层5:4=4,保留右侧4,调用merge(4, null)

层6:触发终止条件,返回4

逐层回溯拼接,最终生成完整有序链表

六、两种解法复杂度深度分析

解法 时间复杂度 空间复杂度 优缺点总结
迭代法 O(n+m) O(1) 最优解,仅用常数级指针空间,逻辑直观,无栈溢出风险,面试首选
递归法 O(n+m) O(n+m) 代码简洁,逻辑抽象,占用递归栈空间,节点过多可能栈溢出,适合学习思想

注:n、m 分别为两个输入链表的节点总数,两种解法均需要遍历所有节点,时间效率一致

七、高频易错点总结(避坑必看)

  1. 忘记处理空链表:未判断链表为空会导致空指针报错,本题所有边界均已覆盖

  2. 不使用哨兵节点:手动判断头节点,极易出现头节点丢失、拼接错乱问题

  3. 循环结束忘记拼接剩余节点:两个链表长度不同,剩余有序节点必须直接拼接,否则数据丢失

  4. 递归无终止条件:会造成无限递归、程序崩溃,必须优先判断空链表终止

相关推荐
玖釉-1 小时前
Slang 和 HLSL 的区别与用法详解:现代图形渲染中的两种 Shader 编程语言
c++·算法·图形渲染
t-think1 小时前
深入了解指针(3)
c语言·算法
GIOTTO情1 小时前
Infoseek 危机公关自动化闭环系统,实现 PR 运维工程化
人工智能·算法·机器学习
hef2881 小时前
利用C 图形界面展示MATLAB算法的高效混合编程实践
windows·算法·matlab
sali-tec1 小时前
C# 基于OpenCv的视觉工作流-章76-轮廓-段距
图像处理·人工智能·opencv·算法·计算机视觉
水木流年追梦1 小时前
大模型入门-RL基础
开发语言·python·算法·leetcode·正则表达式
城事漫游Molly1 小时前
AI 快速生成标准化问卷分析报告:从 SUS 到 UMUX-LITE,如何把“分数”写成“结论”
人工智能·算法·机器学习·论文笔记·科研统计·问卷设计
人道领域1 小时前
【LeetCode刷题日记】617.合并二叉树(空间换安全,还是原地省内存)
java·数据结构·算法·leetcode
独自破碎E1 小时前
机器人Java后端算法笔试题解析
java·windows·算法