leetcode 23. 合并K个升序链表

148 阅读2分钟

力扣题目链接
牛客题目链接

合并K个升序链表是一道常考题。
下面分析如何解题。
设每个链表的平均长度是a。设所有链表的节点数之和是n,已知n=k*a。

本文给出三种方法

  1. 整体排序。
  2. 小顶堆。
  3. 分治思想。

解法一

整体排序。
代码已提交通过。

import "sort"

/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func mergeKLists(lists []*ListNode) *ListNode {
    if len(lists) == 0 {
        return nil
    }
    var nodes []*ListNode
    // 链表节点都加入数组
    for _, list := range lists {
        curr := list
        for curr != nil {
            nodes = append(nodes, curr)
            curr = curr.Next
        }
    }
    sort.Sort(Nodes(nodes)) // 数组排序
    var dummy ListNode
    curr := &dummy
    // 拼接出新链表
    for _, node := range nodes {
        curr.Next = node
        curr = node
    }
    // 注意尾节点的Next指针一定要置nil。
    // 因为使用的排序算法可能是“不稳定”的,
    // 所以排序完成后,最后一个节点的Next指针可能不是nil。
    curr.Next = nil
    return dummy.Next
}

type Nodes []*ListNode

func (o Nodes) Len() int {
    return len(o)
}

func (o Nodes) Swap(i, j int) {
    o[i], o[j] = o[j], o[i]
}

func (o Nodes) Less(i, j int) bool {
    return o[i].Val < o[j].Val
}

遍历每个链表,把节点都放进数组,时间复杂度O(n)O(n);数组排序最优时间复杂度是O(nlogn)O(nlogn)。所以整体上时间复杂度是O(nlogn)O(nlogn)
因为需要一个数组存储所有节点,空间复杂度O(n)O(n)

解法二

基于小顶堆。重点在MinHeap的Pop方法。
代码已提交通过。

/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func mergeKLists(lists []*ListNode) *ListNode {
    l := len(lists)
    if l == 0 {
        return nil
    }
    h := NewMinHeap(l)
    // 把每个链表的头节点都压入堆
    for _, list := range lists {
        h.Push(list)
    }
    var dummy ListNode
    curr := &dummy
    // 不断弹出堆顶,并串接到新链表尾部
    for !h.IsEmpty() {
        node := h.Pop()
        curr.Next = node
        curr = node
    }
    return dummy.Next
}

// 小顶堆
type MinHeap struct {
    data []*ListNode // 存储数据
    size int // data中有效元素的个数
}

// 创建堆对象
// maxLen需要的最大存储空间大小
func NewMinHeap(maxLen int) *MinHeap {
    return &MinHeap{
        data: make([]*ListNode, maxLen),        
    }
}

func (h *MinHeap) IsEmpty() bool {
    return h.size == 0
}

// 压入数据
func (h *MinHeap) Push(node *ListNode) {
    // 注意检查空指针
    if node == nil {
        return
    }
    h.data[h.size] = node
    h.size++
    h.siftUp()
}

// 从堆最后一个位置向上调整
func (h *MinHeap) siftUp() {
    if h.size < 2 {
        return
    }
    child := h.size - 1
    childV := h.data[child]
    for child > 0 {
        p := (child-1)/2
        pv := h.data[p]
        if pv.Val <= childV.Val {
            break
        }
        h.data[child] = pv
        child = p
    }
    h.data[child] = childV
}

// 弹出堆顶
func (h *MinHeap) Pop() *ListNode {
    if h.IsEmpty() {
        panic("MinHeap is empty")
    }
    node := h.data[0]
    next := node.Next
    if next != nil { // 堆顶节点在其链表中的直接后继非空
        h.data[0] = next // 修改堆顶,堆大小不变
        h.siftDown()
    } else { // 堆顶节点在其链表中的直接后继为空,说明那个链表已经处理完了
        if h.size > 1 {
            h.data[0] = h.data[h.size-1] // 堆最后一个元素覆盖堆顶
            h.size-- // 减小堆大小
            h.siftDown()
        } else {
            h.size--
        }
    }
    return node
}

// 从堆顶向下调整
func (h *MinHeap) siftDown() {
    if h.size < 2 {
        return
    }
    var p int
    pv := h.data[p]
    for {
        child := 2*p + 1
        if child >= h.size {
            break
        }
        if child+1 < h.size && h.data[child+1].Val < h.data[child].Val {
            child++
        }
        if pv.Val <= h.data[child].Val {
            break
        }
        h.data[p] = h.data[child]
        p = child
    }
    h.data[p] = pv
}

每次堆调整的时间复杂度O(logk)O(logk)。所以整体上时间复杂度O(nlogk)O(nlogk)
堆最多存储k个节点,空间复杂度O(k)O(k)

解法三

利用分治思想。具体方法是从底向上合并。
代码已提交通过。

/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func mergeKLists(lists []*ListNode) *ListNode {
    l := len(lists)
    if l == 0 {
        return nil
    }
    // lists首尾两两合并,迭代这个过程。
    // 下面注释内容只是示意,请结合代码理解具体操作。
    // 比如lists是这样的 [ l1, l2, l3, l4, l5, l6, l7 ],
    // 一次合并后 [ (l1;l7), (l2;l6), (l3;l5), l4 ],
    // 再次合并后 [ (l1;l7;l4), (l2;l6;l3;l5) ],
    // 再次合并后 [ (l1;l7;l4;l2;l6;l3;l5) ],合并完成。
    for i, j := 0, l-1; j > 0; i = 0 { // 注意进入内层循环之前i置为0
        for ; i < j; i, j = i+1, j-1 {
            lists[i] = mergeTwoLists(lists[i], lists[j]) // 复用lists[i]
        }
    }
    return lists[0]
}

// 按节点值从小到大的顺序合并两个链表
func mergeTwoLists(l1, l2 *ListNode) *ListNode {
    var (
        i1 = l1
        i2 = l2
        dummy ListNode
        curr = &dummy
    )
    for i1 != nil && i2 != nil {
        if i1.Val <= i2.Val {
            curr.Next = i1
            curr = i1
            i1 = i1.Next
        } else {
            curr.Next = i2
            curr = i2
            i2 = i2.Next
        }
    }
    if i1 != nil { // l1有剩余,串接它
        curr.Next = i1
    }
    if i2 != nil { // l2有剩余,串接它
        curr.Next = i2
    }
    return dummy.Next
}

分析mergeKLists函数的两层for循环。内层循环从进入循环到退出循环,每个节点处理一遍,所以时间复杂度是O(n)O(n)。外层循环后一次执行循环体的 j 都变成前一次的一半,可知外层循环的次数是logklogk。所以整体时间复杂度O(nlogk)O(nlogk)
从代码可见,空间复杂度O(1)O(1)