这是我参与更文挑战的第21天,活动详情查看: 更文挑战
合并K个升序链表(题号23)
题目
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
示例 1:
输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
1->4->5,
1->3->4,
2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6
示例 2:
输入:lists = []
输出:[]
示例 3:
输入:lists = [[]]
输出:[]
提示:
k == lists.length
0 <= k <= 10^4
0 <= lists[i].length <= 500
-10^4 <= lists[i][j] <= 10^4
lists[i]
按 升序 排列lists[i].length
的总和不超过10^4
链接
解释
这题啊,这题是升级版合并链表。
有时间可以先看看简单题,合并两个有序链表(题号21)。这题是简单版本啦,也可以为困难版本提供思路。
先说说笔者的思路,这其实很简单。
依然是先搞一个新的链表,用来进行存储操作,后序开始遍历数组中的链表,从头开始进行比较,取最小值记录下它的值和index
。
之后插入新的链表中,同时修改数组中的元素,这样就完事了。
当数组的长度为0时,结束递归,返回新的链表就完事了,非常简单。
官方给出的答案这里其实并不是很推荐,它推荐的三种方法中有两种JavaScript比较适用,这里简单介绍下:
-
顺序合并
顺序合并其实很简单,先写一个方法来合并两个链表。
之后从头开始循环数组,将数组中的每个链表都与新建链表进行合并,在合并
n
次后,新建的链表就是数组中所有链表的合并结果了。 -
分治合并
这个思路也比较简单,首先将数组中的链表两两合并,如此本来数组中的
n
个链表就变成了n /2
个链表,之后开始第二次,数组的元素就变成了n / 4
,就这样直到最后长度就会变成1,此时返回即可。
以上三种方法(包括笔者的想法),其实对运行效率都差不多,没有本质上的提升,在本文的最后会介绍一种超强的JavaScript解法,内存占用和运行时间都可以达到90%以上。
自己的答案(暴力)
思路上面说过了,先看看代码👇:
var mergeKLists = function(lists) {
var head = new ListNode(0)
node = head
while (lists.length) {
var active = null
index = null
for (let i = 0; i < lists.length; i++) {
var item = lists[i]
if (!item) {
lists.splice(i, 1)
--i
} else {
if (!active) {
active = item
index = i
} else {
if (item.val < active.val) {
active = item
index = i
}
}
}
}
if (active) {
node.next = active
node = node.next
active = active.next
lists[index] = active
}
}
return head.next
};
代码优点长,主要的内容都在while
循环里。
递归中的逻辑主要可以分为两部分,在for
循环中,主要是拿到active
和index
,还有就是去掉已经为空的链表。
在for
循环之后,开始给新链表添加元素,添加完成后别忘了修改数组元素,这样就完事了。
最后就可以拿到最后的结果了。
更好的方法(顺序合并)
还是老规矩,先看代码👇:
var mergeKLists = function(lists) {
function mergeTwoLink(l1, l2) {
var head = new ListNode(0)
node = head
while (l1 && l2) {
if (l1.val > l2.val) {
node.next = l2
l2 = l2.next
} else {
node.next = l1
l1 = l1.next
}
node = node.next
}
node.next = l1 ? l1 : l2
return head.next
}
var newLink = null
for (let i = 0; i < lists.length; i++) {
newLink = mergeTwoLink(newLink, lists[i])
}
return newLink
};
方法比较简单,首先是mergeTwoLink
方法,该方法就是简单题的答案,没啥可说的,不记得的可以看上一篇文章。
接下来就更简单了,循环整个数组,取其元素挨个和newLink
合并,都合并完了就是最后的结果了。
更好的方法(分治合并)
分治的思路在上面说了,这里两两合并修改数组就需用到双指针了👇:
var mergeKLists = function(lists) {
if (!lists.length) return null
function mergeTwoLink(l1, l2) {
var head = new ListNode(0)
node = head
while (l1 && l2) {
if (l1.val > l2.val) {
node.next = l2
l2 = l2.next
} else {
node.next = l1
l1 = l1.next
}
node = node.next
}
node.next = l1 ? l1 : l2
return head.next
}
var left = 0
right = lists.length - 1
while (lists.length !== 1 || left < right) {
if (left < right) {
lists[left] = mergeTwoLink(lists[left], lists[right])
lists.pop()
left++
right--
} else {
left = 0
right = lists.length - 1
}
}
return lists[0]
};
这里从数组的头尾开始,先合并头尾的,合并完成后的链表放到left
指针的位置,然后pop()
掉数组最后一位的元素。
当左指针的位置和右指针重合或者超过右指针时,重制两个指针的位置,从头尾开始再走一次。
每走一次,数组的长度都会减少二分之一左右的长度(数组长度为奇数时会减少的长度是二分之一的长度减一),如果到最后数组长度为1时,就是最后的答案了。
因为减少了。n / 2
次的链表合并次数(和顺序合并的方法相比,该方法合并了n
次),执行时间和内存占用都减少了不少,属于中上游水平了,但依旧不是最优解法。
更好的方法(排序组合)
这方法是在翻看别人的答案时发现了,感觉巨强,不仅代码量少,而且性能好,笔者认为这可以是最优解了,没有之一。
整体思路并不复杂,分为三步:
- 拆解所有节点,放到一个数组中
- 对数组中的节点进行排序(从小到大)
- 根据排序后的结果进行节点组合
这就完事了,是不是非常简单。
而且这个老哥用reduce
和reduceRight
一组合,直接省去了新增一个数组的步骤,一波链式调用,一次解决👇:
var mergeKLists = function(lists) {
return lists.reduce((p, n) => {
while (n) {
p.push(n), n = n.next
}
return p
},[]).sort((a, b) => a.val - b.val).reduceRight((p, n) => (n.next = p, p = n, p), null)
};
按照上面的三部曲拆分出来分别是:
-
拆分节点
lists.reduce((p, n) => { while (n) { p.push(n), n = n.next } return p },[])
-
节点排序
.sort((a, b) => a.val - b.val)
-
组合节点
.reduceRight((p, n) => (n.next = p, p = n, p), null)
简简单单三部曲,直接搞定,这老哥真的太强了~
PS:想查看往期文章和题目可以点击下面的链接:
这里是按照日期分类的👇
经过有些朋友的提醒,感觉也应该按照题型分类
这里是按照题型分类的👇
有兴趣的也可以看看我的个人主页👇