21、合并两个有序链表
21、合并两个有序链表
鲁迅说过,每天刷一遍二哥的 LeetCode 刷题笔记,离大厂就会更近一步
(dog)。
题意
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的
所有节点组成的。
难度
简单
标签
链表、排序
示例
021.%E5%90%88%E5%B9%B6%E4%B8%A4%E4%B8%AA%E6%9C%89%E5%BA%8F%E
9%93%BE%E8%A1%A8-20240202165557.png
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
输入:l1 = [], l2 = []
输出:[]
输入:l1 = [], l2 = [0]
输出:[0]
分析 1
先来读题,关键词,两个升序链表,合并,一个新的升序链表。
既然升序过,那最小的数字肯定是两个链表 l1 和 l2 中头节点中的一个。
如果最小的是 l1 的头节点,那么第二小的节点肯定是 l2 的头节点或者 l1.next,只要再比
较一下两个的大小就能够得到第二小的节点。
如果最小的是 l2 的头节点,那么第二小的肯定是 l1 的头节点或者 l2.next;
那我们就可以采用递归的方式:
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 的 next 应该是 l1.next 或者 l2;
如果 l1 的头节点大于 l2 的头节点,那么 l2 的 next 应该是 l1 或者 l2.next;
每次递归调用都会返回当前两个链表头部较小的那个节点作为合并链表的当前节点。
递归的结束条件就是 l1 或者 l2 为空,那么返回另一个链表即可。因为如果其中一个链表
为空,就意味着另一个链表已经是当前最终的排序链表了,我们可以直接返回非空链表作
为合并后的链表。
// 如果 l1 为空,返回 l2
if (l1 == null) {
return l2;
}
// 如果 l2 为空,返回 l1
if (l2 == null) {
return l1;
}
021.%E5%90%88%E5%B9%B6%E4%B8%A4%E4%B8%AA%E6%9C%89%E5%BA%8F%E
9%93%BE%E8%A1%A8-20240202192400.png
OK,我们来看完整的题解:
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
// 如果 l1 为空,返回 l2
if (l1 == null) {
return l2;
}
// 如果 l2 为空,返回 l1
if (l2 == null) {
return l1;
}
// 选择较小的节点,递归地合并剩余部分
if (l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
}
递归的关键在于理解每一步递归调用都在做什么,以及递归何时结束。在这个问题中,每
一步递归都在处理两个链表当前的头节点,并通过递归调用来处理剩下的节点。递归使得
问题规模逐渐变小,直至达到简单的基本情况(一个链表为空),从而完成整个合并过
程。
我们来看题解的效率:
021.%E5%90%88%E5%B9%B6%E4%B8%A4%E4%B8%AA%E6%9C%89%E5%BA%8F%E
9%93%BE%E8%A1%A8-20240202193356.png
分析 2
递归看起来简单,但对于新手来说,理解起来很痛苦,尤其是递归的调用栈,很深。。。
深的脑子一片乱麻。
那我们换一种更直接的思路,直接遍历两个链表,依次比较两个链表的节点,将较小的节
点放到新链表中。
然后移动指针,继续比较,直到其中一个链表为空,然后将另一个链表剩余的部分直接连
接到新链表的末尾。
021.%E5%90%88%E5%B9%B6%E4%B8%A4%E4%B8%AA%E6%9C%89%E5%BA%8F%E
9%93%BE%E8%A1%A8-20240202194320.png
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
// 初始化哨兵节点
ListNode sentinel = new ListNode(-1);
// 初始化一个指针,最初指向哨兵节点
ListNode prev = sentinel;
// 当两个链表都不为空时,循环继续
while (l1 != null && l2 != null) {
if (l1.val <= l2.val) {
prev.next = l1;
l1 = l1.next;
} else {
prev.next = l2;
l2 = l2.next;
}
// 移动 prev 指针
prev = prev.next;
}
// 直接将非空链表剩余部分连接到新链表末尾
prev.next = l1 == null ? l2 : l1;
// 返回哨兵节点的下一个节点,即合并后链表的头节点
return sentinel.next;
}
}
这个题解的效率也非常不错,但更容易理解一些:
021.%E5%90%88%E5%B9%B6%E4%B8%A4%E4%B8%AA%E6%9C%89%E5%BA%8F%E
9%93%BE%E8%A1%A8-20240202194626.png
总结
这道题目是链表的基础题,递归和迭代都可以解决,递归的思路更加简洁,但是对于新手
来说,理解起来可能会有些困难,迭代的思路更加直接,但是代码量会多一些。
关键还是理解链表的数据结构,这个视频讲的不错,推荐一手。
视频链接:https://www.bilibili.com/video/BV1ea4y1r75V
还有这个网站,对链表也进行了详细的讲解,推荐一手。
网站链接:https://www.hello-algo.com/chapter_array_and_linkedlist/
linked_list/#1
链表另外一个关键点在于理解指针的移动,比如说 prev = prev.next,这个操作不只是
移动指针,还相当于删除了 prev 节点,大家可以感受一下。
力扣链接:https://leetcode.cn/problems/merge-two-sorted-lists/