• 数据结构与算法之堆: Leetcode 23. 合并 K 个升序链表 (Typescript版)


    合并K个升序链表

    • https://leetcode.cn/problems/merge-k-sorted-lists/

    描述

    • 给你一个链表数组,每个链表都已经按升序排列
    • 请你将所有链表合并到一个升序链表中,返回合并后的链表

    示例 1

    输入:lists = [[1,4,5],[1,3,4],[2,6]]
    输出:[1,1,2,3,4,4,5,6]
    解释:链表数组如下:
    [
      1->4->5,
      1->3->4,
      2->6
    ]
    将它们合并到一个有序链表中得到。
    1->1->2->3->4->4->5->6
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    示例 2

    输入:lists = []
    输出:[]
    
    • 1
    • 2

    示例 3

    输入:lists = [[]]
    输出:[]
    
    • 1
    • 2

    提示

    • k == lists.length
    • 0 <= k <= 1 0 4 10^4 104
    • 0 <= lists[i].length <= 500
    • - 1 0 4 10^4 104 <= lists[i][j] <= 1 0 4 10^4 104
    • lists[i] 按 升序 排列
    • lists[i].length 的总和不超过 1 0 4 10^4 104

    算法实现

    1 )使用堆

    /**
     * Definition for singly-linked list.
     * class ListNode {
     *     val: number
     *     next: ListNode | null
     *     constructor(val?: number, next?: ListNode | null) {
     *         this.val = (val===undefined ? 0 : val)
     *         this.next = (next===undefined ? null : next)
     *     }
     * }
     */
    
    class MinHeap {
        heap: Array<ListNode | null> = [];
        // 交换节点位置
        swap(i, j) {
        	if (i === j) return;
            [this.heap[i], this.heap[j]] = [this.heap[j], this.heap[i]];
        }
        // 获得父节点
        getParentIndex(i) {
            return (i - 1) >> 1;
        }
        // 获取左子节点
        getLeftIndex(i) {
            return (i << 1) + 1; // 极客写法
        }
        // 获取右子节点
        getRightIndex(i) {
            return (i << 1) + 2;
        }
        // 上移操作封装 是个递归
        shiftUp(i) {
            // 如果到了堆顶元素,index是0,则不要再上移了
            if(!i) return;
            // 获得父节点下标: pi
            const pi = this.getParentIndex(i);
            // 开始做比较
            if(this.heap[pi]?.val > this.heap[i].val) {
                // 实现交换
                this.swap(pi, i);
                // 继续尝试上移操作
                this.shiftUp(pi);
            }
        }
        // 下移操作
        shiftDown(i) {
            // 如果到了堆尾元素,则不要再下移了
            if(i >= this.heap.length - 1) return;
            const li = this.getLeftIndex(i); // 左孩子索引
            const ri = this.getRightIndex(i); // 右孩子索引
            // 左孩子节点的值 < 当前节点的值 注意:左右对比中,this.heap[i]不能用变量替代,因为内部有swap和shiftDown,使用变量会导致bug
            if(this.heap[li]?.val < this.heap[i].val) {
                    this.swap(li, i);
                    this.shiftDown(li);
            }
            // 同样,对右孩子节点的值 < 当前节点的值
            if(this.heap[ri]?.val < this.heap[i].val) {
                    this.swap(ri, i);
                    this.shiftDown(ri);
            }
        }
        // 插入
        insert(value) {
            this.heap.push(value);
            this.shiftUp(this.heap.length - 1);
        }
        // 删除堆顶
        pop() {
            if(this.size() === 1) return this.heap.shift();
            const top = this.heap[0];
            // 变相删除堆顶, 堆尾元素移动到堆顶
            this.heap[0] = this.heap.pop(); // 删除数组的最后一个元素并返回,返回值赋值给堆顶元素
            // 执行堆顶下移操作,维持堆的有序性
            this.shiftDown(0);
            return top;
        }
        // 获取堆顶
        peak() {
            return this.heap[0];
        }
        // 获取堆的大小
        size() {
            return this.heap.length;
        }
    }
    
    function mergeKLists(lists: Array<ListNode | null>): ListNode | null {
        const res = new ListNode(0);
        let p = res;
        const h = new MinHeap();
        // 通过输入来构建堆结构
        // 三个链表,堆中存入的是三个链表的头
        lists.forEach(l => {
            l && h.insert(l); // l是个链表,其实用的是链表头表示,也就是链表的第一个元素
        })
        // while循环,每一轮pk的都是堆内的元素
        // 谁出队,就把谁的下一个入堆,逐个比较
        // 最终堆空了,比较全部结束
        while(h.size()) {
            const n = h.pop(); // 弹出堆顶元素,最小值
            p.next = n; // 将堆顶元素挂载在新链表上
            p = p.next; // 链表指针移位,为下次连接做准备
            n.next && h.insert(n.next) // 将弹出的堆顶元素的下一个元素入堆,进行重新构建堆结构
        }
        return res.next; // 返回的是next, 因为其第一个节点是我们new出来,用于连接的,所以不包含第一个节点
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 时间复杂度 O(nlogk)
      • forEach O(k), k是链表数量
      • while循环遍历了所有链表的所有节点 O(n),n是所有链表节点之和
      • 并且堆操作是O(logk), 两者结合:O(nlogk)
      • 整体:O(nlogk)
    • 空间复杂度 O(k)
      • 就是堆的大小
    • 堆能高效、快速找出最大值和最小值,时间复杂度O(1),堆顶是最大值或最小值
    • 找出第K个最大(小)元素
      • 构建一个容量为k的堆,让每个元素都插入这个堆
      • 保持容量始终为k, 最终堆顶就是最大元素或最小元素
    • 这个解法不算精妙,但是是两个数据结构(堆和链表)融合解题的典型
  • 相关阅读:
    微调 TrOCR – 训练 TrOCR 识别弯曲文本
    移动端/嵌入式-CV模型-2017:MobelNets-v1
    轻量级Composition
    java稀疏数组(含稀疏数组代码展示)
    Redis企业版数据库如何支持实时金融服务?
    深入理解MySQL分区表原理与企业级实战
    SpringBoot下的代理注解
    Vue实现简单的接口封装
    程序员因薪资低拒绝offer,HR恼羞成怒,网友瞬间炸翻了..
    巧记书本结构--思维导图
  • 原文地址:https://blog.csdn.net/Tyro_java/article/details/133499836