📌 题目链接:21. 合并两个有序链表 - 力扣(LeetCode) 🔍 难度:简单 | 🏷️ 标签:链表、递归、迭代、双指针
⏱️ 目标时间复杂度:O(n + m)
💾 空间复杂度:O(1)(迭代) / O(n + m)(递归)
✅ 题目分析
📌 题目名称:21. 合并两个有序链表 - 力扣(LeetCode)
📌 类型:链表、排序、双指针
📌 要求:将两个已排序的链表合并为一个新的升序链表,不能修改原始链表结构。
🎯 输入输出说明:
- 输入:两个升序链表
l1和l2 - 输出:一个新的升序链表,由
l1和l2的所有节点拼接而成 - 特殊情况:
- 其中一个为空 → 返回另一个
- 两者都为空 → 返回空
🧩 示例解析:
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
过程:
比较 1 vs 1 → 取第一个 1
比较 2 vs 1 → 取 1
比较 2 vs 3 → 取 2
...
最终合并结果:[1,1,2,3,4,4]
🔍 核心算法及代码讲解
本题的核心在于如何高效地比较两个链表的节点值,并将较小者依次接入结果链表。由于链表是线性结构,无法随机访问,因此我们采用两种经典方法:
✅ 方法一:递归(Recursive)
🤔 思想本质:
“分而治之”思想的体现 —— 每次选择当前两个链表头节点中更小的那个,将其加入结果,并递归处理剩余部分。
🧠 递归定义:
merge(l1, l2) =
if l1 == nullptr: return l2
if l2 == nullptr: return l1
if l1->val < l2->val:
l1->next = merge(l1->next, l2)
return l1
else:
l2->next = merge(l1, l2->next)
return l2
🧪 优点:
- 代码简洁,逻辑清晰
- 符合人类思维模式:“先选最小的,再看剩下的”
❌ 缺点:
- 递归深度达到
n+m,栈空间消耗大 - 在大量数据时可能触发栈溢出(Stack Overflow)
- 不适合对内存敏感的场景(如嵌入式系统)
✅ 方法二:迭代(Iterative)✅ 推荐!
🤔 思想本质:
使用“哨兵节点”+“双指针”技术,通过循环逐步构建结果链表,避免递归开销。
🧠 关键设计点:
| 设计要素 | 作用 |
|---|---|
preHead 哨兵节点 | 方便统一处理头节点,无需判断是否为空 |
prev 指针 | 记录上一次插入位置,便于连接下一个节点 |
while(l1 && l2) | 循环直到其中一个链表遍历完 |
prev->next = l1/l2 | 将较小节点挂载到结果链表末尾 |
🧪 优点:
- 时间复杂度相同,但空间复杂度仅为 O(1)
- 更稳定,无栈溢出风险
- 是面试中最推荐的写法!
🧩 解题思路(分步详解)
🚀 步骤 1:初始化哨兵节点
ListNode* preHead = new ListNode(-1);
ListNode* prev = preHead;
preHead是虚拟头节点,其值无关紧要(设为 -1 即可)prev指向当前结果链表的最后一个节点,初始指向preHead
✅ 这样做是为了避免单独处理第一个节点的情况!
🚀 步骤 2:双指针遍历比较
while (l1 != nullptr && l2 != nullptr) {
if (l1->val < l2->val) {
prev->next = l1;
l1 = l1->next;
} else {
prev->next = l2;
l2 = l2->next;
}
prev = prev->next;
}
- 比较两个链表当前节点的值
- 将较小的节点接入结果链表
- 对应指针后移一位
prev也后移,准备下一轮连接
🔄 注意:每次只移动一个链表的指针,另一个不动!
🚀 步骤 3:处理剩余节点
prev->next = l1 == nullptr ? l2 : l1;
- 当其中一个链表遍历完后,另一个链表剩下的节点一定全部大于已合并部分
- 因此可以直接把剩余链表整体接到结果链表末尾
💡 这是关键优化!避免重复比较!
📊 算法分析
| 方法 | 时间复杂度 | 空间复杂度 | 是否推荐 | 适用场景 |
|---|---|---|---|---|
| 递归 | O(n + m) | O(n + m) | ⚠️ 不推荐 | 教学演示、小规模数据 |
| 迭代 | O(n + m) | O(1) | ✅ 强烈推荐 | 面试、生产环境、大规模数据 |
🔍 为什么迭代更好?
- 面试官看重“空间效率”和“稳定性”
- 大公司(如 Google、Meta)倾向于考察迭代能力
- 递归虽优雅,但容易被质疑“为什么不考虑栈溢出?”
- 实际工程中几乎不用递归来处理链表
🧱 代码实现(完整版)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
// 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* mergeTwoLists(ListNode* l1, ListNode* l2) {
if (l1 == nullptr) {
return l2;
} else if (l2 == nullptr) {
return l1;
} else if (l1->val < l2->val) {
l1->next = mergeTwoLists(l1->next, l2);
return l1;
} else {
l2->next = mergeTwoLists(l1, l2->next);
return l2;
}
}
// 方法二:迭代实现(推荐)
ListNode* mergeTwoListsIterative(ListNode* l1, ListNode* l2) {
ListNode* preHead = new ListNode(-1); // 哨兵节点
ListNode* prev = preHead; // 当前结果链表的最后一个节点
// 双指针遍历两个链表
while (l1 != nullptr && l2 != nullptr) {
if (l1->val < l2->val) {
prev->next = l1; // 把 l1 的节点接到结果链表
l1 = l1->next; // l1 后移
} else {
prev->next = l2; // 把 l2 的节点接到结果链表
l2 = l2->next; // l2 后移
}
prev = prev->next; // prev 后移
}
// 将未遍历完的链表直接接上
prev->next = l1 == nullptr ? l2 : l1;
return preHead->next; // 返回真实头节点
}
};
// 测试函数
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
// 构造测试用例:l1 = [1,2,4], l2 = [1,3,4]
ListNode* l1 = new ListNode(1);
l1->next = new ListNode(2);
l1->next->next = new ListNode(4);
ListNode* l2 = new ListNode(1);
l2->next = new ListNode(3);
l2->next->next = new ListNode(4);
Solution sol;
ListNode* result = sol.mergeTwoListsIterative(l1, l2);
// 打印结果
cout << "合并后的链表:";
while (result != nullptr) {
cout << result->val << " ";
result = result->next;
}
cout << endl;
return 0;
}
💡 面试加分技巧 & 常见追问
❓ Q1:为什么用哨兵节点?
A:避免对第一个节点进行特殊判断,简化代码逻辑。例如若没有哨兵,需要额外判断
head == nullptr,增加分支。
❓ Q2:能否原地合并?会破坏原链表吗?
A:可以,但要注意是否允许修改原链表。通常题目要求“新建链表”,所以建议新建。如果允许原地修改,则只需调整指针即可。
❓ Q3:有没有办法减少内存分配?
A:可以用
dummy节点代替new ListNode(-1),但实际影响不大。关键是避免递归栈开销。
❓ Q4:如果链表是降序呢?
A:只需将
<改为>即可,逻辑不变。
❓ Q5:如何扩展到 k 个有序链表?
A:可用优先队列(堆)维护每个链表的头节点,每次取出最小值。这是后续题目「23. 合并 K 个升序链表」的核心思想。
🧠 总结:本题核心价值
| 维度 | 内容 |
|---|---|
| 🧩 数据结构 | 链表的基本操作:创建、遍历、连接 |
| 🧠 算法思想 | 双指针、递归 vs 迭代、分治思想 |
| 🛠️ 工程实践 | 哨兵节点、边界处理、空间优化 |
| 🎯 面试重点 | 代码健壮性、空间复杂度控制、边界条件处理 |
✅ 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📣 下一期预告:LeetCode 热题 100 第28题 —— 2.两数相加(中等)
🔹 题目:给定两个非空链表,代表两个非负整数,数字以逆序存储在链表中(个位在前),求它们的和并返回一个新的链表。
🔹 核心思路:模拟竖式加法,使用进位变量
carry,逐位相加并处理进位。🔹 考点:链表操作、数学模拟、边界处理(如进位导致多一位)、原地构造新链表。
🔹 难度:中等,是链表类问题的经典入门题,常考于字节跳动、腾讯、阿里等大厂面试。
💡 提示:注意处理最后一位的进位!不要漏掉!