[路飞]合并 K 个升序链表

95 阅读3分钟

「这是我参与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
    }

拆分成两两合并的解法。只有两个的时候,我们就可以很方便的管理指针。

  1. 依次合并
    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
    }
  1. 递归分成多个 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
    }

结束