数据结构与算法之堆: 堆和实现最小堆类 (Typescript版)

堆的数据结构与算法

  • 堆是一种特殊的完全二叉树

    • 完全二叉树:每层节点都完全填满;最后一层,如果不是满的,则缺少右边的若干节点
  • 堆:所有节点都大于等于(最大堆)或小于等于(最小堆)它的子节点

    • 可见堆是一种有顺序的数据结构
    • 如下图所示
  • 最大堆示例

        6
      /   \
     5     3
    / \   /
    4  2 1
    
  • 最小堆示例

        1
      /   \
     3     2
    / \    /
    4  6  5
    
  • 堆的规律

    • 一个堆,如果按广度优先遍历就是 0 1 2 3 4 5,我们把0 ~ 5作为数组的下标
    • 并把节点中的值填到数组中,如此一来就可以把数组来表示一个堆了,不仅如此有公式可以获取子节点和父节点的位置关系
    • 左侧子节点的位置是 2 * index + 1
    • 右侧子节点的位置是 2 * index + 2
    • 父节点的位置是 (index - 1) / 2 的商
  • 在前端js中表示堆的示例: 这是一个最小堆的结构

          1
        /   \
      3      6
     / \    /
    5   9  8
    
  • 给这个最小堆标记下索引(下标),如下

           1(0)
        /       \
      3(1)       6(2)
     /    \      /
    5(3)  9(4)  8(5)
    
  • 上图括号中的就是在堆中的下标

  • 这个最小堆在js中的表示形式是:[1,3,6,5,9,8], 数组中对应的各个元素的下标:0,1,2,3,4,5

    • 这个规律就是按照广度优先遍历来走的
    • 也就是从上到下,从左到右
  • 堆的应用

    • 快速高效找出最大值和最小值
    • 时间复杂度O(1)
    • 找出第k个最大(小)元素
  • 解决第k个最大元素

    • 构建一个最小堆,并将元素依次插入堆中
    • 当堆的容量超过K,就删除堆顶
    • 当插入结束后,堆顶就是第k个最大元素
  • 如果要找到第k个最小元素,反过来就行,道理一致

实现一个最小堆类

  • 在类里,声明一个数组,用来装元素
  • 主要方法:插入,删除堆顶、获取堆顶、获取堆大小
  • 插入
    • 将值插入堆的底部,即数组的尾部
    • 然后上移:将这个值和它的父节点进行交换换,直到父节点小于等于这个插入的值
    • 大小为k的堆中插入元素的时间复杂度为O(logk)
      • 复杂度主要体现在上移操作中,最多循环的次数是堆的高度,因为它不断上移
      • 这个堆的高度也就是二叉树的高度,二叉树的高度和树节点数量的关系是logk
      • 所以时间复杂度是O(logk)
  • 删除堆顶
    • 用数组尾部元素替换堆顶(直接删除堆顶会破坏堆结构)
    • 然后下移,将新堆顶和它的子节点进行交换,直到子节点大于等于这个新堆顶
    • 大小为k的堆中删除堆顶的事件复杂度为O(logk)
      • 和插入一样的道理,下移次数最大是堆的高度,所以时间复杂度也是logk
  • 获取堆顶:返回数组的头部
  • 获取堆的大小:返回数组的长度
// MinHeap.js
class MinHeap {
    constructor() {
        this.heap = []
    }
    // 获取父节点
    getParentIndex(i) {
        // return Math.floor(i - 1) / 2; // 这样做代码比较多
        return (i - 1) >> 1; // 比较极客的写法:把一个二进制的数字右移一位;往右移一位,就是除以2,就是取得商
    }
    // 获取左子节点
    getLeftIndex(i) {
        // return i * 2 + 1; // 老的写法
        return (i << 1) + 1; // 极客写法
    }
    // 获取右子节点
    getRightIndex(i) {
        // return i * 2 + 1;
        return (i << 1) + 2;
    }
    // 两数交换
    swap(i, j) {
        // 写法1, 比较繁琐
        // let temp = this.heap[i];
        // this.heap[i] = this.heap[j];
        // this.heap[j] = temp;

        // 写法3 对象结构,别名的方式
        // let a = 1,b = 2;
        // {a:b,b:a} = {a,b}


        // 写法2 通过数学运算加减关系,利用两个数字在坐标轴上的距离固定来交换
        // let a = 1,b = 2;
        // b = b - a; // b(原) - a(原)
        // a = a + b;  // a(原) + b(原) - a(原)
        // b = a - b;  // b(原) - (b(原) - a(原))
        // console.log(a); //2
        // console.log(b); //1

        // 使用es6解构数组赋值来交换两数
        [this.heap[j], this.heap[i]] = [this.heap[i], this.heap[j]];
    }
    // 上移操作封装 是个递归
    shiftUp(i) {
        // 如果到了堆顶元素,index是0,则不要再上移了
        if(!i) {
            return;
        }
        // 获得父节点下标: pi
        const pi = this.getParentIndex(i);
        // 开始做比较
        if(this.heap[pi] > this.heap[i]) {
            // 实现交换
            this.swap(pi, i);
            // 继续尝试上移操作
            this.shiftUp(pi);
        }
    }
    // 下移操作
    shiftDown(i) {
        // 如果到了堆尾元素,则不要再下移了
        if(i >= this.heap.length - 1) {
            return;
        }
        const size = this.size(); // 获取当前堆长度
        const li = this.getLeftIndex(i); // 左孩子索引
        const ri = this.getRightIndex(i); // 右孩子索引
        const cur = this.heap[i]; // 堆的当前元素
        // 左孩子节点的值 < 当前节点的值
        if(li < size && this.heap[li] < cur) {
             this.swap(li, i);
             this.shiftDown(li);
        }
        // 同样,对右孩子节点的值 < 当前节点的值
        if(ri < size && this.heap[ri] < cur) {
             this.swap(ri, i);
             this.shiftDown(ri);
        }
    }
    // 添加
    insert(value) {
        this.heap.push(value);
        // 保证堆的结构是正确的,要进行上移的操作,把该值移动到合适的位置
        this.shiftUp(this.heap.length - 1); // 这个方法封装上移操作,参数是接受一个上移值的下标
    }
    // 删除
    pop() {
        // 变相删除堆顶, 堆尾元素移动到堆顶
        this.heap[0] = this.heap.pop(); // 删除数组的最后一个元素并返回,返回值赋值给堆顶元素
        // 执行堆顶下移操作,维持堆的有序性
        this.shiftDown(0);
    }
    // 获取堆顶
    peak() {
        return this.heap[0];
    }
    // 获取堆大小
    size() {
        return this.heap.length;
    }
    // 输出最后的堆结构, 有参,输出参数;无参,输出堆结构
    print(o) {
        console.log(o ? o : this.heap);
    }
}

// 使用
const h = new MinHeap();
h.print();
h.insert(3);
h.insert(2);
h.insert(1);
h.print(); // [1,3,2]
h.pop();
h.print(); // [2,3]
h.print(h.peak()) // 2
h.print(h.size()) // 2

你可能感兴趣的:(Data,Structure,and,Algorithms,leetcode,算法)