Leetcode21.合并两个有序链表 双指针+递归 【hot100算法个人笔记】【java写法】

算法刷题打卡 | 今天刷到了 LeetCode 21. 合并两个有序链表,这道题和昨天的反转链表一样,都是链表的经典入门题,做完之后发现思路和归并排序里的合并步骤几乎一模一样,把迭代和递归两种写法都理清楚了,做个笔记记录一下,防止之后回头就忘。

题目回顾

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

示例

示例 1:

Plain 复制代码
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]

示例 2:

Plain 复制代码
输入:l1 = [], l2 = []
输出:[]

示例 3:

Plain 复制代码
输入:l1 = [], l2 = [0]
输出:[0]

节点定义

题目中的单链表节点定义如下:

java 复制代码
/**
 * Definition for singly-linked list.
 */
public class ListNode {
    int val;
    ListNode next;
    ListNode() {}
    ListNode(int val) { this.val = val; }
    ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}

解法一:迭代(双指针)法

这道题的推荐解法,空间复杂度可以做到 O (1),也是生产环境中最常用的写法。

核心思路

我们用两个指针分别遍历两个有序链表,每次比较两个指针指向的节点的值,把更小的那个节点接到我们的结果链表后面,然后移动对应的指针,直到其中一个链表遍历完,最后把剩下的节点直接接到结果链表的末尾就可以了。

这里有个链表题的神器:虚拟头节点(dummy node),用它可以完美解决头节点不好处理的问题,不用单独判断哪个链表的头节点更小,所有节点的处理逻辑完全统一。

步骤拆解

我自己整理的操作要点,每一步都很清晰:

  1. 创建虚拟头节点 :先初始化一个值为 0 的伪节点 dum,然后用游标指针 cur 指向它,最后我们要返回的是 dum.next,也就是真正的结果链表的头节点。

  2. 循环遍历 :循环的结束条件是两个原链表都遍历完,也就是 list1 != null && list2 != null,只要还有节点没处理,就继续。

  3. 比较选择更小的节点

  • 如果 list1.val < list2.val,说明当前 list1 的节点更小,把它接到结果链表的后面:cur.next = list1

  • 然后把 list1 的指针向后移动一位:list1 = list1.next

  • 否则就处理 list2 的节点,同理,把节点接过去,然后移动 list2 的指针

  • 处理完之后,把结果链表的游标指针 cur 也向后移动一位,准备接下一个节点

  1. 处理剩余节点:循环结束后,肯定有一个链表已经遍历完了,另一个还有剩余节点,直接把剩下的整个链表接到 cur 的后面就可以了,因为剩下的节点本身就是有序的。

  2. 返回结果 :最后返回 dum.next,也就是虚拟头节点的下一个节点,就是我们合并后的新链表的头节点。

代码实现

java 复制代码
class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        ListNode dum = new ListNode(0);
        ListNode cur = dum;
        while(list1 != null && list2 != null){
            if(list1.val < list2.val){
                cur.next = list1;
                list1 = list1.next;
            }else{
                cur.next = list2;
                list2 = list2.next;
            }
            cur = cur.next;
        }
        cur.next = list1 == null ? list2 : list1;
        return dum.next;
    }
}

踩坑感悟

一开始做这道题的时候,我没用 dummy 节点,自己去判断哪个链表的头节点更小,作为结果的头节点,结果处理空链表的时候出了一堆问题,比如两个都是空的,或者其中一个是空的,要写一堆 if 判断,代码又乱又长。后来才知道用 dummy 节点,一下子就把所有情况都统一了,不管头节点是谁,不管有没有空链表,代码逻辑完全不用变,太香了!这应该是链表题里最实用的小技巧了。

复杂度分析

  • 时间复杂度:O (n + m),n 和 m 是两个链表的长度,我们需要遍历完两个链表的所有节点,每个节点处理一次。

  • 空间复杂度:O (1),我们只是复用了原来的节点,只用到了几个指针变量,常数级的额外空间。

解法二:递归法

个人笔记:递归法不推荐在生产环境使用,代码虽然简洁,但是有栈溢出的风险。

核心思路

递归的思路其实就是分治:每次我们选两个链表头节点里更小的那个,然后这个节点的 next,就是剩下的两个链表合并后的结果,这样一层层递归下去,直到其中一个链表为空,就直接返回另一个剩下的链表。

步骤拆解

我整理的递归要点:

  1. 先写递归终止条件 :如果 list1 == null,说明 list1 已经遍历完了,直接返回剩下的 list2 就可以了;反之如果 list2 == null,就返回剩下的 list1,因为剩下的节点本身就是有序的,直接接上去就行。

  2. 递归处理剩余部分:比较两个链表的头节点的值,更小的那个节点,它的 next 指针,就指向剩下的两个链表递归合并后的结果,然后返回这个更小的节点,作为当前层的结果。

代码实现

java 复制代码
class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        if(list1==null){
            return list2;
        }
        else if(list2==null){
            return list1;
        }
        else if(list1.val<list2.val){
            list1.next=mergeTwoLists(list1.next,list2);
            return list1;
        }else{
            list2.next=mergeTwoLists(list1,list2.next);
            return list2;
        }
    }
}

复杂度分析

  • 时间复杂度:O (n + m),同样要处理所有的节点,递归 n+m 次。

  • 空间复杂度:O (n + m),递归调用的栈深度最多是 n+m,因为最坏情况下,我们要递归到其中一个链表的最后一个节点,所以会占用这么多的栈空间,这也是为什么不推荐在生产环境用的原因,链表长了很容易栈溢出。

刷题笔记与感悟

做完这道题,最大的收获就是搞懂了虚拟头节点的用法,之前做链表题总头疼头节点的特殊处理,现在发现只要加个 dummy 节点,所有节点的处理逻辑都能统一,代码一下子就简洁了,再也不用写一堆边界判断了。

而且这道题其实就是归并排序里的「合并」步骤,之前学排序的时候只知道归并排序要合并两个有序数组,现在用链表实现了一遍,一下子就把两个知识点串起来了,原来归并的思路不管是数组还是链表都是通用的!学会了这道题,之后的「合并 K 个有序链表」也就有了基础,本质上就是把多个合并成两个,再用这个思路处理。

另外我也测试了边界情况:两个都是空链表、其中一个是空链表、两个链表长度不一样,两种方法都能正确处理,不用额外加判断,代码的鲁棒性还是很好的。

总结

这道题作为链表的基础题,真的太经典了,两种方法各有优劣:

  • 迭代法:空间复杂度低,没有栈溢出的风险,效率高,生产环境首选,也是面试的时候最推荐写的解法,尤其是 dummy 节点的用法,面试官很喜欢看你会不会用这个技巧。

  • 递归法:代码非常简洁,几行就写完了,很能体现分治的思想,但是空间复杂度高,有栈溢出的风险,适合用来练习递归思维,实际项目里还是少用。

搞定了这道题,链表的基础思想又掌握了一个。

相关推荐
m0_743470372 小时前
C++中的装饰器模式变体
开发语言·c++·算法
花间相见2 小时前
【JAVA基础14】—— 二维数组详解:从基础到实战应用
java·python·算法
2401_864959282 小时前
分布式日志系统实现
开发语言·c++·算法
linhaijiao2 小时前
C++与人工智能框架
开发语言·c++·算法
前端付豪2 小时前
实现代码块复制和会话搜索
前端·人工智能·后端
zjjsctcdl2 小时前
Spring Boot 经典九设计模式全览
java·spring boot·设计模式
阿聪谈架构2 小时前
第06章:AI RAG 检索增强生成 — 从零到生产(上)
人工智能·后端
会算数的⑨2 小时前
Spring AI Alibaba 学习(四):ToolCalling —— 从LLM到Agent的华丽蜕变
java·开发语言·人工智能·后端·学习·saa·ai agent
Ivanqhz2 小时前
linearize:控制流图(CFG)转换为线性指令序列
开发语言·c++·后端·算法·rust