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

在算法刷题的旅程中,"合并两个有序链表" (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行)

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

递归终止条件(第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;
}
  • 比较头节点值 :判断l1l2的头节点哪个更小。

  • 选择较小的头节点

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

    • 如果l2->val < l1->val,则l2作为当前合并链表的头节点。此时,l2next需要指向"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)

    • 比较11l1->val <= l2->val成立。

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

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

    • 比较21l1->val > l2->val

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

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

    • 比较23l1->val <= l2->val成立。

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

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

    • 比较43l1->val > l2->val

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

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

    • 比较44l1->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),因为只需要几个指针变量。但递归法的代码更简洁,逻辑更清晰。)

六、总结

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

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

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

相关推荐
小毛驴8502 小时前
多线程同步打标记的几种实现方案
java·开发语言·python
bluebonnet272 小时前
【Python】一些PEP提案(五):注解的延迟求值
开发语言·python
橙露2 小时前
Python 操作 MongoDB:非关系型数据查询与分析
开发语言·python·mongodb
小魏小魏我们去那里呀2 小时前
Java2Flowchart:一款把 Java 方法一键转换成 Mermaid 流程图的 IntelliJ 插件
java·ide·intellij-idea
小江的记录本2 小时前
【RAG】RAG检索增强生成(核心架构、全流程、RAG优化方案、常见问题与解决方案)
java·前端·人工智能·后端·python·机器学习·架构
迷藏4942 小时前
**TiDB 在高并发场景下的性能优化实战:从慢查询到极致吞吐的跃迁**在现代分布式系统中,数据库不仅是数据存储的
java·数据库·python·性能优化·tidb
毅炼3 小时前
MySQL 常见问题总结(1)
java·大数据·数据库
Rust研习社3 小时前
深入 Rust 引用计数智能指针:Rc 与 Arc 从入门到实战
开发语言·后端·rust
CRMEB系统商城3 小时前
国内开源电商系统的格局与演变——一个务实的技术视角
java·大数据·开发语言·小程序·开源·php