【LeetCode Hot100 刷题日记(27/100)】21合并两个有序链表——递归与迭代双解法详解 🔗

72 阅读7分钟

📌 题目链接:21. 合并两个有序链表 - 力扣(LeetCode) 🔍 难度:简单 | 🏷️ 标签:链表、递归、迭代、双指针
⏱️ 目标时间复杂度:O(n + m)
💾 空间复杂度:O(1)(迭代) / O(n + m)(递归)


✅ 题目分析

📌 题目名称:21. 合并两个有序链表 - 力扣(LeetCode)

📌 类型:链表、排序、双指针

📌 要求:将两个已排序的链表合并为一个新的升序链表,不能修改原始链表结构。

🎯 输入输出说明:

  • 输入:两个升序链表 l1l2
  • 输出:一个新的升序链表,由 l1l2 的所有节点拼接而成
  • 特殊情况:
    • 其中一个为空 → 返回另一个
    • 两者都为空 → 返回空

🧩 示例解析:

输入: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,逐位相加并处理进位。

🔹 考点:链表操作、数学模拟、边界处理(如进位导致多一位)、原地构造新链表。

🔹 难度:中等,是链表类问题的经典入门题,常考于字节跳动、腾讯、阿里等大厂面试。

💡 提示:注意处理最后一位的进位!不要漏掉!