解法一:暴力法
利用juejin.cn/post/747420… 中的解法,对数组遍历一遍,两两合并即为答案
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func mergeKLists(lists []*ListNode) *ListNode {
if len(lists) == 0{
return nil
}
if len(lists) == 1{
return lists[0]
}
res := lists[0]
for _, l := range lists[1:]{
res = mergeTwoLists(res, l)
}
return res
}
func mergeTwoLists(list1 *ListNode, list2 *ListNode) *ListNode {
dummy := &ListNode{}
cur, p1, p2 := dummy, list1, list2
for p1 != nil && p2 != nil{
if p1.Val < p2.Val{
cur.Next = p1
p1 = p1.Next
}else{
cur.Next = p2
p2 = p2.Next
}
cur = cur.Next
}
if p1 == nil{
cur.Next = p2
}else if p2 == nil{
cur.Next = p1
}
return dummy.Next
}
- 时间复杂度:O(n*k^2),k为链表个数
- 分析:假设输入的 k 个链表的长度分别为 l0, l1, ⋯ ,lk−1,算法会调用 k−1 次
mergeTwoLists函数,每次调用mergeTwoLists方法的时间复杂度为两个链表的长度之和。 - 第一次调用的时间复杂度是 (l_0 + l_1),第二次调用的时间复杂度是 (l_0 + l_1 + l_2),以此类推,最后一次调用的时间复杂度是 (l_0 + l_1 + 。。。 + l_{k-1})。综上,链表 (l_0) 和 (l_1) 的长度会被遍历 (k-1) 次,(l_2) 会被遍历 (k-2) 次,以此类推,最后一条链表 (l_{k-1}) 仅被遍历 1 次。
- 分析:假设输入的 k 个链表的长度分别为 l0, l1, ⋯ ,lk−1,算法会调用 k−1 次
- 空间复杂度:O(1)
越靠前的链表被重复遍历的次数越多,这就是这个算法低效的原因。我们只要减少这种重复,就能提高算法的效率。
解法二:分治法
如果把上述解法写成递归的形式,不难发现重复的次数取决于树高,上面这个算法的递归树很不平衡,退化成链表,树高变为 O(k)。
如果能让递归树尽可能地平衡,就能减小树高,进而减少链表的重复遍历次数,提高算法的效率。
如何让递归树平衡呢?把链表从中间分成两部分,分别递归合并为两个有序链表,最后再将这两部分合并成一个有序链表。
整棵递归树的形态为一棵平衡二叉树,高度是 O(logk)。每条链表需要被遍历(合并)的次数是树的高度。
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func mergeKLists(lists []*ListNode) *ListNode {
if len(lists) == 0{
return nil
}
if len(lists) == 1{
return lists[0]
}
return mergeHelper(lists, 0, len(lists)-1)
}
func mergeHelper(lists []*ListNode, start, end int) *ListNode{
if start == end{
return lists[start]
}
mid := start + (end - start)/2
left := mergeHelper(lists, start, mid)
right := mergeHelper(lists, mid+1, end)
return mergeTwoLists(left, right)
}
func mergeTwoLists(l1, l2 *ListNode) *ListNode{
dummy := &ListNode{}
cur := dummy
for l1 != nil && l2 != nil{
if l1.Val <= l2.Val{
cur.Next = l1
l1 = l1.Next
}else{
cur.Next = l2
l2 = l2.Next
}
cur = cur.Next
}
if l1 != nil{
cur.Next = l1
}else if l2 != nil{
cur.Next = l2
}
return dummy.Next
}
- 时间复杂度:O(n*logk)
- 分析:相当于是把 k 条链表分别遍历 O(logk) 次
- 空间复杂度:O(logk)
- 分析:只有递归树堆栈的开销,也就是 O(logk),要优于优先级队列解法的 O(k)
解法三:优先级队列
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func mergeKLists(lists []*ListNode) *ListNode {
if len(lists) == 0 {
return nil
}
// 虚拟头结点
dummy := &ListNode{-1, nil}
p := dummy
// 优先级队列,最小堆
pq := &PriorityQueue{}
heap.Init(pq)
// 将 k 个链表的头结点加入最小堆
for _, head := range lists {
if head != nil {
heap.Push(pq, head)
}
}
for pq.Len() > 0 {
// 获取最小节点,接到结果链表中
node := heap.Pop(pq).(*ListNode)
p.Next = node
if node.Next != nil {
heap.Push(pq, node.Next)
}
// p 指针不断前进
p = p.Next
}
return dummy.Next
}
// PriorityQueue implements heap.Interface and holds ListNodes
type PriorityQueue []*ListNode
func (pq PriorityQueue) Len() int { return len(pq) }
func (pq PriorityQueue) Less(i, j int) bool {
return pq[i].Val < pq[j].Val
}
func (pq PriorityQueue) Swap(i, j int) {
pq[i], pq[j] = pq[j], pq[i]
}
func (pq *PriorityQueue) Push(x interface{}) {
*pq = append(*pq, x.(*ListNode))
}
func (pq *PriorityQueue) Pop() interface{} {
old := *pq
n := len(old)
item := old[n-1]
*pq = old[0 : n-1]
return item
}
- 时间复杂度:O(n*logk)
- 分析:优先级队列
pq中的元素个数最多是 k,所以一次poll或者add方法的时间复杂度是 O(logk);所有的链表节点都会被加入和弹出pq,所以算法整体的时间复杂度是 O(Nlogk),其中 k 是链表的条数,N 是这些链表的节点总数。
- 分析:优先级队列
- 空间复杂度:O(k)
- 分析:维护这个小顶堆(优先级队列)的空间,最多存放k个元素
尝试不用golang的内部包实现小顶堆