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

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

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

相关推荐
我学上瘾了37 分钟前
Spring Cloud的前世今生
后端·spring·spring cloud
波波0072 小时前
ASP.NET Core 健康检查实战:不只是一个 /health 接口
后端·asp.net
小码哥_常2 小时前
Spring Boot 搭建邮件发送系统:开启你的邮件自动化之旅
后端
故事和你912 小时前
洛谷-数据结构1-1-线性表1
开发语言·数据结构·c++·算法·leetcode·动态规划·图论
脱氧核糖核酸__2 小时前
LeetCode热题100——53.最大子数组和(题解+答案+要点)
数据结构·c++·算法·leetcode
脱氧核糖核酸__3 小时前
LeetCode 热题100——42.接雨水(题目+题解+答案)
数据结构·c++·算法·leetcode
石榴树下的七彩鱼3 小时前
图片修复 API 接入实战:网站如何自动去除图片水印(Python / PHP / C# 示例)
图像处理·后端·python·c#·php·api·图片去水印
我叫黑大帅3 小时前
为什么TCP是三次握手?
后端·网络协议·面试
我叫黑大帅3 小时前
如何排查 MySQL 慢查询
后端·sql·面试
techdashen3 小时前
Rust项目公开征测:Cargo 构建目录新布局方案
开发语言·后端·rust