LeetCode23 合并k个升序链表(抖音面试原题,3种解法)

81 阅读4分钟

leetcode.cn/problems/me…

image.png

解法一:暴力法

利用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 次。
  • 空间复杂度:O(1)

越靠前的链表被重复遍历的次数越多,这就是这个算法低效的原因。我们只要减少这种重复,就能提高算法的效率

解法二:分治法

如果把上述解法写成递归的形式,不难发现重复的次数取决于树高,上面这个算法的递归树很不平衡,退化成链表,树高变为 O(k)。

如果能让递归树尽可能地平衡,就能减小树高,进而减少链表的重复遍历次数,提高算法的效率。

如何让递归树平衡呢?把链表从中间分成两部分,分别递归合并为两个有序链表,最后再将这两部分合并成一个有序链表。

整棵递归树的形态为一棵平衡二叉树,高度是 O(log⁡k)。每条链表需要被遍历(合并)的次数是树的高度。

/**
 * 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(log⁡k) 次
  • 空间复杂度:O(logk)
    • 分析:只有递归树堆栈的开销,也就是 O(log⁡k),要优于优先级队列解法的 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的内部包实现小顶堆