「这是我参与2022首次更文挑战的第13天,活动详情查看:2022首次更文挑战」。
要求:提供一个存放 k 个链表的数组,将里面的链表的每个节点按照升序合成一个链表。提供的链表自身是升序的。
由于链表已经是升序的,所以比较好处理,但是链表里前后两个值相差有多大是不确定的,所以可能会出现要在同一个链表里拿走几个,然后再去拿其他链表里面的节点。
合并 K 个升序链表意味着我们要维护每个链表的第一个节点。然后当取了这个节点后指针往后移一位。
但是我们不知道有多少个链表,所以想维护第一个节点的指针只能遍历 k 次。
优化的方法有两个,第一个是将链表放进堆内,利用堆得出第一个节点最小的链表,然后传入刚取到的链表的 next。当链表取完之后就不再添加。第二个是将 k 个链表的合并分成了多次两个链表合并。其中分为一个个依次合并和递归进行多次2+2。递归的做法会减少重复。
首先是堆的解法,由于要最小的在堆顶,所以用小顶堆,那么数组升序排列。 Heap 是自己实现的堆,详细可以看这篇文章,这里不再赘述。
function mergeKLists(lists) {
const heap = new Heap((a, b) => a.val - b.val)
// 将链表放进堆内
for (let list of lists) {
list && heap.push(list)
}
// 用伪节点方便合并
const head = new ListNode()
let last = head
while (heap.size() > 0) {
// 取出第一个元素最小的链表
const top = heap.pop()
const val = top.val
// 插入到新链表
last = last.next = new ListNode(val)
if (top.next) {
// 如果链表还有节点有就放进堆内
heap.push(top.next)
}
}
return head.next
}
拆分成两两合并的解法。只有两个的时候,我们就可以很方便的管理指针。
- 依次合并
function mergeKLists(lists) {
let head = lists[0] ?? null
// 取出第一个作为新链表,然后从第二个开始依次和新链表合并
for (let i = 1; i < lists.length; i++) {
head = merge(head, lists[i])
}
return head
}
function merge(list1, list2) {
// 两个指针
let a = list1
let b = list2
const head = new ListNode()
let last = head
// 当其中一个链表已经取完的时候,
// 剩下的链表可以直接添加到新链表后面
while (a && b) {
// 合并最小的节点,然后移动指针
if (a.val < b.val) {
last = last.next = a
a = a.next
} else {
last = last.next = b
b = b.next
}
}
if (a) {
last.next = a
}
if (b) {
last.next = b
}
return head.next
}
- 递归分成多个 2 + 2
merge 函数不需要做改动,只需要设置递归就可以,每次将节点数组分成两拨,也就是分治。 递归的终止条件就是无法再分治的时候。
function mergeKLists(lists) {
const len = lists.length
// 如果传进来的 lists 长度是 0,
// 当然有 len === 1 作为终止条件,0 是不会进判断的,
// 但是有可能在一开始传进来的 lists 长度就是 0
if (len === 0) return null
if (len === 1) return lists[0]
const middle = len / 2
head = merge(
mergeKLists(lists.slice(0, middle)),
mergeKLists(lists.slice(middle, len))
)
return head
}
结束