合并k个有序链表

316 阅读3分钟

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

题意:有一个由 k 个链表组成的数组,其中每个链表都已经是有序的,需要我们把它们合并成一个链表。链表节点的数量上限在10的4次方的数量级。

要把 k 个链表合并,我们先考虑最朴素的办法,我们新建一个空链表依次和每一个链表做合并操作,这样合并的最终结果就是我们要求的链表了。

我们会发现这个其实是一个函数式编程中的reduce操作。(这里先不考虑效率)

而reduce作为一个高阶函数,最重要的就是就是传给他的参数:用来合并两个链表的函数。

我们不一定写过合并有序链表的算法,但我们基本都写过合并有序数组的算法。二者都是线性表,我们可以参考一下。

不过值得注意的是,我们其实可以借助链表的特点,每次不做复制,而是先直接把节点接到新链表的后面。

这样做可以避免频繁的申请和弃用内存。空间复杂度可以达到O(1)的级别

然后我们可以顺着这个思路往下做,从依次合并到两两合并做优化。

但是我想换一个思路来写,回到我们对两个链表的合并。

那如果我是要想办法合并多个有序链表呢,不是也可以每次取k个链表的最小值吗?

而说到取最小值,我们是不是会有一些想法,我们有学习过靠每次选取最小值的排序方法,并且其中还有一种特殊的结构用来优化求最小值的过程。它就是堆这个数据结构。

我们参考堆排序的过程,不一样的是我们再每个最小值被拿出来的时候,要考虑把他的后继放入堆中,当然前提是它是存在的。

然后我们这样写我们的代码:

    fun mergeKLists(lists: Array<ListNode?>): ListNode? {
        val que = PriorityQueue<ListNode> { o1, o2 -> o1.`val` - o2.`val` }
        for (node in lists) {
            node?.let {
                que.offer( it)
            }
        }
        val head = ListNode(0)//伪头
        var tail: ListNode? = head
        while (!que.isEmpty()) {
            val node = que.poll()
            tail = tail?.let {
                it.next = node
                it.next
            }
            node.next?.let {
                que.offer(it)
            }
        }
        return head.next
    }

借助kotlin足够填的语法糖(let内联函数),我们连判断null都不需要显式的书写,舒舒服服(逃

用堆来完成整个链表的合并,其实只需要我们稍稍转变下思路,想法应该还是很自然的,参考堆排序编写就可以了。

在时间复杂性上,使用堆其实也是足够快的,最大的问题在于它对空间的消耗是线性复杂度的。在k太大的时候可能会让人难以接受。不过我个人认为他还是有可取之处的,比分治算法可能要更让人感觉自然一些。