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 节点的用法,面试官很喜欢看你会不会用这个技巧。

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

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

相关推荐
你不是我我6 小时前
【Java 开发日记】HTTP3 性能更好,为什么内网微服务依然多用 HTTP2?HTTP2 内网优势是什么?
java·开发语言·微服务
雪碧聊技术6 小时前
大模型爆火!Java后端如何抓住Agent全栈开发的风口
java·大模型·agent·全栈开发
IT大白鼠6 小时前
AIGC性能的关键瓶颈:算力、数据、算法三者如何互相制约?
算法·aigc
白雪茫茫7 小时前
监督学习、半监督学习、无监督学习算法详解
python·学习·算法·ai
FengyunSky7 小时前
浅析 空间频率响应 SFR 计算
算法
树下水月7 小时前
PHP 一种改良版的雪花算法
算法·php·dreamweaver
逻辑驱动的ken7 小时前
Java高频面试场景题25
java·开发语言·深度学习·面试·职场和发展
一只数据集8 小时前
全尺寸人形机器人灵巧手力觉触觉数据集-2908条ROSbag数据覆盖14大应用场景深度解析
大数据·人工智能·算法·机器人
AI人工智能+电脑小能手8 小时前
【大白话说Java面试题】【Java基础篇】第32题:Java的异常处理机制是什么
java·开发语言·后端·面试
ltl8 小时前
Softmax 与概率分布:从分数到选择的桥
后端