递归之美:合并两个有序链表的优雅解法

在算法刷题的旅程中,"合并两个有序链表" (LeetCode 21题)是一道经典的中等难度题目。它不仅考察了对链表结构的理解,还巧妙运用了递归思想,用极简的代码实现了复杂的功能。今天,我们就从问题分析、代码逻辑、递归过程、复杂度分析等方面,深入剖析这道题目的解法。

一、题目理解

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

举个例子:

  • 输入:l1 = [1,2,4], l2 = [1,3,4]

  • 输出:[1,1,2,3,4,4]

链表的结构是单向的,每个节点包含一个值(val)和一个指向下一节点的指针(next)。我们需要通过指针操作,将两个有序链表的节点"按序拼接"。

二、递归解法的核心思路

递归的本质是**"将大问题分解为小问题,直到小问题可以直接解决"**。对于合并两个有序链表,我们可以这样思考:

  1. 基准情况(递归终止条件):如果其中一个链表为空,直接返回另一个链表(因为空链表和任何链表合并,结果都是另一个链表)。

  2. 递归逻辑 :比较两个链表头节点的值,选择较小的那个作为"当前合并链表的头节点",然后递归地合并"当前头节点的下一个节点"和"另一个链表的头节点",并将结果赋值给当前头节点的next。

三、代码逐行解析(C++实现)

先给出完整的代码,再逐行讲解:

复制代码
/**
 * 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) {
        // 基准情况1:l1为空,返回l2(剩余l2已有序)
        if (l1 == nullptr) return l2;
        // 基准情况2:l2为空,返回l1(剩余l1已有序)
        if (l2 == nullptr) return l1;
        
        // 比较两个链表的头节点值,选择较小的作为当前合并链表的头
        if (l1->val <= l2->val) {
            // l1的头节点更小,所以l1的next需要合并"l1的下一个节点"和"l2"
            l1->next = mergeTwoLists(l1->next, l2);
            // 返回l1作为当前合并链表的头
            return l1;
        } else {
            // l2的头节点更小,所以l2的next需要合并"l1"和"l2的下一个节点"
            l2->next = mergeTwoLists(l1, l2->next);
            // 返回l2作为当前合并链表的头
            return l2;
        }
    }
};

1. 链表节点结构定义(第2-9行)

复制代码
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) {}
};

这是C++中链表的节点定义,包含:

  • val:节点存储的整数值。

  • next:指向下一个节点的指针。

  • 三个构造函数:支持无参初始化、单值初始化、值和指针同时初始化,方便创建链表节点。

2. 主函数mergeTwoLists(第11-27行)

函数接收两个链表的头指针l1和l2,返回合并后的链表头指针。

递归终止条件(第13-14行)
复制代码
if (l1 == nullptr) return l2;
if (l2 == nullptr) return l1;
  • 如果l1为空,说明l1已经没有节点了,直接返回l2(此时l2的剩余部分已经是升序的)。

  • 如果l2为空,同理返回l1。

    这是递归的"出口",确保递归不会无限进行。

递归逻辑(第16-25行)
复制代码
if (l1->val <= l2->val) {
    l1->next = mergeTwoLists(l1->next, l2);
    return l1;
} else {
    l2->next = mergeTwoLists(l1, l2->next);
    return l2;
}
  • 比较头节点值 :判断l1和l2的头节点哪个更小。

  • 选择较小的头节点:

    • 如果l1->val <= l2->val,则l1作为当前合并链表的头节点。此时,l1的next需要指向"l1的下一个节点"和l2合并后的结果(递归调用mergeTwoLists(l1->next, l2))。

    • 如果l2->val < l1->val,则l2作为当前合并链表的头节点。此时,l2的next需要指向"l1"和"l2的下一个节点"合并后的结果(递归调用mergeTwoLists(l1, l2->next))。

  • 返回当前头节点 :无论选择l1还是l2,都将其作为当前合并链表的头返回,供上一层递归使用。

四、递归过程可视化(以示例1为例)

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

我们用栈帧的方式模拟递归过程:

  1. 初始调用:mergeTwoLists(l1=1→2→4, l2=1→3→4)

    • 比较1和1,l1->val <= l2->val成立。

    • 执行l1->next = mergeTwoLists(l1->next=2→4, l2=1→3→4),进入下一层递归。

  2. 第二层调用:mergeTwoLists(l1=2→4, l2=1→3→4)

    • 比较2和1,l1->val > l2->val。

    • 执行l2->next = mergeTwoLists(l1=2→4, l2->next=3→4),进入下一层递归。

  3. 第三层调用:mergeTwoLists(l1=2→4, l2=3→4)

    • 比较2和3,l1->val <= l2->val成立。

    • 执行l1->next = mergeTwoLists(l1->next=4, l2=3→4),进入下一层递归。

  4. 第四层调用:mergeTwoLists(l1=4, l2=3→4)

    • 比较4和3,l1->val > l2->val。

    • 执行l2->next = mergeTwoLists(l1=4, l2->next=4),进入下一层递归。

  5. 第五层调用:mergeTwoLists(l1=4, l2=4)

    • 比较4和4,l1->val <= l2->val成立。

    • 执行l1->next = mergeTwoLists(l1->next=nullptr, l2=4),进入下一层递归。

  6. 第六层调用:mergeTwoLists(l1=nullptr, l2=4)

    • 触发基准条件,返回l2=4。
  7. 回到第五层:l1->next = 4(第六层的返回值),返回l1=4。

  8. 回到第四层:l2->next = 4(第五层的返回值),返回l2=3→4。

  9. 回到第三层:l1->next = 3→4(第四层的返回值),返回l1=2→3→4。

  10. 回到第二层:l2->next = 2→3→4(第三层的返回值),返回l2=1→2→3→4。

  11. 回到第一层:l1->next = 1→2→3→4(第二层的返回值),返回l1=1→1→2→3→4→4。

最终,合并后的链表为1→1→2→3→4→4,与示例输出一致。

五、复杂度分析

时间复杂度:O(n+m)

其中 n和 m分别是两个链表的长度。每个节点都会被递归访问一次,因此总的时间复杂度是两个链表长度之和。

空间复杂度:O(n+m)(递归栈的空间)

递归的深度最多为 n+m(当两个链表长度分别为 n和 m时,最坏情况下需要递归 n+m层)。因此,递归栈的空间复杂度为 O(n+m)。

(如果使用迭代法,空间复杂度可以优化到 O(1),因为只需要几个指针变量。但递归法的代码更简洁,逻辑更清晰。)

六、总结

合并两个有序链表的递归解法,核心在于**"分解问题"**:每次选择较小的头节点,然后递归处理剩余部分。这种思路不仅代码简洁,还能很好地体现递归"自顶向下、逐步细化"的思想。

在实际刷题或面试中,递归法适合快速写出逻辑清晰的代码;如果需要优化空间,也可以尝试迭代法(用指针逐个拼接节点)。但无论哪种方法,理解"如何比较两个节点并选择下一个节点"的逻辑是关键。

希望这篇博客能帮你彻底掌握这道经典题目的递归解法!如果有疑问,欢迎在评论区交流~ 🚀

相关推荐
令狐少侠20114 小时前
centos7 下,使用docker 部署IOT 调试平台,ThingsBoard 并完美启动
java·spring boot·iot
小灰灰搞电子4 小时前
Rust+Slint 实现一个终端软件源码分享(半成品)
开发语言·rust
大鹏说大话4 小时前
C++ STL 容器怎么选?vector、list、map、unordered_map 对比
开发语言·c++·list
JAVA面经实录9174 小时前
Java高级后端 · 全套面试通关手册(线上故障排查)
java·jvm·面试
IT_Octopus4 小时前
【零基础入门 LLM 开发 · Day 11】:ChatOpenAI vs init_chat_model——一行换供应商
java·前端·javascript
宇卿.4 小时前
C#语句:
开发语言·c#
ComPDFKit4 小时前
用 ComPDF DocSlight 提取 PDF 表格:三种样本实测与 Python 接入
开发语言·python·pdf·pdf表格提取
Wang's Blog4 小时前
Java 中间件之 RabbitMQ 快速入门: MQ 常见技术选型对比
java·中间件·java-rabbitmq
蜗牛互联网4 小时前
Java Agent 工具调用的 allowlist、参数校验与调用预算
java·开发语言·人工智能·后端·oracle
大文说跨境4 小时前
多账号环境隔离方案技术选型:指纹浏览器、VPS 与云手机的三种架构对比
java·开发语言·前端