一、题目完整描述
题目要求
给定两个非递减升序排列 的单链表 list1 和 list2,请你将两个链表合并为一个全新的非递减升序链表。合并规则为:直接拼接两个链表的原有所有节点,不新建额外数据节点,最终返回合并后新链表的头节点。
题目示例
示例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 -
两个输入链表均为非递减有序(后一个节点值 ≥ 前一个节点值)
二、零基础必备前置知识
本题是链表入门核心题,所有知识点从零讲解,零基础可完全看懂,所有概念、特性、操作全部讲透。
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→4、1→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 分别为两个输入链表的节点总数,两种解法均需要遍历所有节点,时间效率一致
七、高频易错点总结(避坑必看)
-
忘记处理空链表:未判断链表为空会导致空指针报错,本题所有边界均已覆盖
-
不使用哨兵节点:手动判断头节点,极易出现头节点丢失、拼接错乱问题
-
循环结束忘记拼接剩余节点:两个链表长度不同,剩余有序节点必须直接拼接,否则数据丢失
-
递归无终止条件:会造成无限递归、程序崩溃,必须优先判断空链表终止