05.2-堆(Heap)与优先队列(手撕算法篇)

14 阅读20分钟

前言

我们上一篇文章聊了一下堆的概念和性质,相信大家对堆这样的数据结构已经有了一个初步的认识了,为了加深理解以及熟悉堆的应用场景,接下来带大家刷几道算法题加深一下记忆,在此之前还不太了解堆的性质的同学,请先移步堆(Heap)与优先队列(数据结构基础篇)

PS: 下面的算法解题思路不一定是解决这个算法最优的思路,但是,为了巩固对于堆的使用和特性的理解,我们都是用堆的方式实现,某些题目不使用堆,使用其他方式或许能够更加高效的完成任务,这些各位同学可以自己去琢磨。

LeetCode 剑指Offer 40. 最小的k个数

解题思路

对于这道题,我们要如何使用堆来解决问题呢?我们之前说过,堆适合用来维护集合中的最值,而依据题意,我们这里应该使用一个大顶堆,方便我们实时比较集合中的最大值。那么我们假设我们就有一个长度为k的集合,里面存放的都是符合题意的数字,当我们一个新的数据想要放入这个集合时,如果这个集合的长度小于或等于k,那么我们直接添加进来,如果长度大于k,那么我们就要比较一下要进来的这个值到底比这个集合中的最大值大还是小,如果比这个集合中的值大的话,这个数不是我们想要的,把它从集合里面移除,如果比最大值小,那么这个值应该要加入到集合中,但是因为我们只要k个数,所以需要把最大值弹出才行。

PS: 堆的实现在上一篇文章中有详细讲解,这里就不再赘述,如有不明白的地方,可以看一下上一篇文章

代码演示

// 大顶堆的实现在上一篇文章中有详细讲解,这里就不再赘述,如有不明白的地方,可以看一下上一篇文章
class Heap {
    private count: number = 0;
    private arr: number[] = [];
    constructor(){}

    _shiftUp(idx: number): void {
        while(idx && this.arr[parseInt(String((idx - 1) / 2))] < this.arr[idx]) {
            const pIdx = parseInt(String((idx - 1) / 2));
            [this.arr[idx], this.arr[pIdx]] = [this.arr[pIdx], this.arr[idx]];
            idx = pIdx;
        }
    }

    _shiftDown(idx: number): void {
        const n = this.count - 1;
        while(2*idx+1<=n) {
            let max = idx;
            if(this.arr[max] < this.arr[2*idx+1]) max = 2*idx+1;
            if(2*idx+2<=n && this.arr[max] < this.arr[2*idx+2]) max = 2*idx+2;
            if(max === idx) break;
            [this.arr[max], this.arr[idx]] = [this.arr[idx], this.arr[max]];
            idx = max;
        }
    }

    push(x: number): void {
        this.arr[this.count++] = x;
        this._shiftUp(this.count - 1);
    }
    pop(): number {
        if(!this.size()) return -1;
      	const tmp = this.arr[0];
        this.arr[0] = this.arr[this.count-1];
        this.count--;
        this._shiftDown(0);
      	return tmp;
    }
    size(): number {
        return this.count;
    }
    top(): number {
        return this.arr[0];
    }
    getArray(): number[] {
        // 由于我们弹出堆的操作知识逻辑弹出,并没有真正的物理删除,因此,我们再获取实际堆的数组时,需要截取this.count个元素
        return this.arr.slice(0, this.count);
    }
}

function getLeastNumbers(arr: number[], k: number): number[] {
    const heap = new Heap();
    arr.forEach(item=>{
        // 将数组的每一个元素都加入到堆中
        heap.push(item);
        // 如果堆的大小大于k时,弹出最大值
        if(heap.size()>k) {
            heap.pop();
        }
    });
    // 遍历结束后,其实堆里面存放的就是我们要的最小的第k个数的数组了
    return heap.getArray();
};

LeetCode 1046. 最后一块石头的重量

解题思路

这道题也是很明显的求集合最值的问题,我们只需要将所有的石头都放入大顶堆中,如果堆中石头的数量大于1个,就每次取出最大的石头和次大的石头进行粉碎,如果有剩余的就再加入到堆中,当循环结束时,要么堆中一个石头都没了,要么就只剩下一个,只剩下一个的话,这个石头的重量就是我们要的结果

代码演示

/*
 * @lc app=leetcode.cn id=1046 lang=typescript
 *
 * [1046] 最后一块石头的重量
 */

// @lc code=start
class Heap {
    private count: number = 0;
    private arr: number[] = [];
    constructor(){}

    _shiftUp(idx: number): void {
        while(idx && this.arr[parseInt(String((idx - 1) / 2))] < this.arr[idx]) {
            const pIdx = parseInt(String((idx - 1) / 2));
            [this.arr[idx], this.arr[pIdx]] = [this.arr[pIdx], this.arr[idx]];
            idx = pIdx;
        }
    }

    _shiftDown(idx: number): void {
        const n = this.count - 1;
        while(2*idx+1<=n) {
            let max = idx;
            if(this.arr[max] < this.arr[2*idx+1]) max = 2*idx+1;
            if(2*idx+2<=n && this.arr[max] < this.arr[2*idx+2]) max = 2*idx+2;
            if(max === idx) break;
            [this.arr[max], this.arr[idx]] = [this.arr[idx], this.arr[max]];
            idx = max;
        }
    }

    push(x: number): void {
        this.arr[this.count++] = x;
        this._shiftUp(this.count - 1);
    }
    pop(): number {
        if(!this.size()) return -1;
        const tmp = this.arr[0];
        this.arr[0] = this.arr[this.count-1];
        this.count--;
        this._shiftDown(0);
        return tmp;
    }
    size(): number {
        return this.count;
    }
    top(): number {
        return this.arr[0];
    }
    getArray(): number[] {
        // 由于我们弹出堆的操作知识逻辑弹出,并没有真正的物理删除,因此,我们再获取实际堆的数组时,需要截取this.count个元素
        return this.arr.slice(0, this.count);
    }
}
function lastStoneWeight(stones: number[]): number {
    const heap = new Heap();
    // 先将所有石头放到大顶堆中
    stones.forEach(stone => heap.push(stone));
    // 如果堆中石头的数量大于1个
    while(heap.size()>1) {
        // 将最大的石头和第二大的石头弹出来
        const s1 = heap.pop();
        const s2 = heap.pop();
        // 如果相等则不作任何操作
        if(s1 == s2) continue;
        // 否则将剩余石头的数量重新放入堆中
        heap.push(s1-s2);
    }
    // 如果堆没有元素了,直接返回0
    if(heap.size()===0) return 0;

    // 否则返回堆顶元素
    return heap.top();
};
// @lc code=end


LeetCode 703. 数据流中的第 K 大元素

解题思路

如果要在一个集合中找到第k大的数,那么我们至少加入这个集合的元素不能比这个集合里面的最小的元素小。 所以我们每当集合满了的时候,我们就要将最小的元素剔除出去,为了比较最小值,所以我们这次要使用小顶堆

代码演示

/*
 * @lc app=leetcode.cn id=703 lang=typescript
 *
 * [703] 数据流中的第 K 大元素
 */

// @lc code=start
// 以下为对对结构的定义,让我们能够快速的创建一个大顶堆或小顶堆的实例,如果不理解的可以看一下上一篇文章,那里有详细的讲解。
export type HeapDataStruct<T> = {
    idx?: number,
    data: T
};
export type CompareFn<T> = (x: HeapDataStruct<T>, y: HeapDataStruct<T>) => boolean;
class Heap<T> {
    private arr: HeapDataStruct<T>[] = [];
    private count: number = 0;
    constructor(private cmpFn: CompareFn<T>) {

    }

    private _compare(curIdx: number, pIdx: number): boolean {
        return this.cmpFn(this.arr[curIdx], this.arr[pIdx])
    }

    private _shift_up(idx: number): void {
        // 然后对堆进行向上调整
        // 我们需要通过子节点坐标得到父节点的坐标,因为我们这边的下标是从0开始的,所以左子树根节点坐标为:2 * i + 1,右子树根节点坐标为:2 * i + 2。那么我们父节点的编号:parseInt((子节点编号 - 1) / 2),如知道左节点编号为:2 * i + 1,那么他的父节点坐标就是: parseInt((2 * i + 1 - 1) / 2) = parseInt(i);如果知道有节点的编号:2 * i + 2,那么他的父节点的坐标为:parseInt((2 * i + 2 - 1) / 2) = parseInt((2*i+1)/2) = i
        // 上面我们讲过,我们需要如果在一个大顶堆中,如果我们父节点的值小于子节点的值的话,就要交换这两个值
        // while(idx && this.arr[parseInt(String((idx - 1) / 2))].data < this.arr[idx].data) {
        while(idx && this._compare(parseInt(String((idx - 1) / 2)), idx)) {
          // 父节点编号
          const pIdx = parseInt(String((idx - 1) / 2));
          // 交换父节点和当前节点的值
          [ this.arr[pIdx], this.arr[idx] ] = [ this.arr[idx], this.arr[pIdx] ];
          // 然后再让idx变成父节点的编号,这样就能依次向上调整了
          idx = pIdx;
        }
      }

      private _shift_down(idx: number): void {
        // 交换之后,根节点再依次向下调整
        // 最大的子节点的下标
        let n = this.count - 1;
        // 当idx的子节点的下标比最大的子节点的下标小,就说明还有子节点,就要往下继续调整
        while(idx * 2 + 1 <= n) {
          // max代表在根节点、左子树、右子树中最大值的下标
          let max = idx;
          // 如果左子树的根节点的值比当前值大,就把max更新为左子树根节点的下标
        //   if(this.arr[max].data < this.arr[2*idx+1].data) max = 2*idx+1;
          if(this._compare(max, 2*idx+1)) max = 2*idx+1;
          // 如果右子树根节点的值比当前节点大,就把max更新为右子树根节点的下标
          // 需要注意的是,我们当前的节点,可能存在左子树,但不一定存在右子树,所以需要多加一个2*idx+2<=n的条件
          if(2*idx+2<=n && this._compare(max, 2*idx+2)) max = 2*idx+2;
        //   if(2*idx+2<=n && this.arr[max].data < this.arr[2*idx+2].data) max = 2*idx+2;
          // 如果我的最大值的下标就是当前的这个值,那就不需要向下调整了,直接结束循环
          if(max === idx) break;
          // 然后交换这个最大值的下标和根节点
          [ this.arr[idx], this.arr[max] ] = [ this.arr[max], this.arr[idx] ];
          // 然后把idx改为原最大值的下标,继续向下调整
    
          idx = max;
        }
      }

    push(item: HeapDataStruct<T>): void {
        this.arr[this.count++] = item;
        this._shift_up(this.count-1);
    }

    pop(): HeapDataStruct<T>|null {
      if(this.size()===0) return null;
      const tmp = this.arr[0];
      // 根据上面的讲解,我们首先需要让队尾元素与根节点交换
      [ this.arr[0], this.arr[this.count-1] ] = [ this.arr[this.count-1], this.arr[0] ];
      // 交换之后记得count要减1
      this.count--;
      // 进行向下调整
      this._shift_down(0);
      return tmp;
    }

    size(): number {
        return this.count;
    }

    top(): HeapDataStruct<T> {
        return this.arr[0];
    }

    output(): void {
        console.log(JSON.stringify(this.arr));
    }
}

class KthLargest {
    // 如果要在一个集合中找到第k大的数,那么我们至少加入这个集合的元素不能比这个集合里面的最小的元素小
    // 所以我们每当集合满了的时候,我们就要将最小的元素剔除出去,为了比较最小值,所以我们这次要使用小顶堆
    private heap = new Heap<number>((x,y)=>x.data>y.data);
    private k: number;
    constructor(k: number, nums: number[]) {
        this.k = k;
        // 将数组里面的元素都压入堆中
        nums.forEach(num => this.add(num));
        
    }

    add(val: number): number {
        this.heap.push({data: val});
        // 如果堆的大小超过k,就把最小值弹出
        if(this.heap.size()>this.k) {
            this.heap.pop();
        }
        // 返回堆顶元素的值就是我们要的第k大的值
        return this.heap.top().data;
    }
}

/**
 * Your KthLargest object will be instantiated and called as such:
 * var obj = new KthLargest(k, nums)
 * var param_1 = obj.add(val)
 */
// @lc code=end


LeetCode 215. 数组中的第K个最大元素

解题思路

跟上一题一样也是小顶堆的简单问题,思路都是一样的,先将每个元素依次加入到堆中,每加入一个元素看一下堆的大小是不是超过k了,如果超过k的话就把最小值弹出,最后,我们堆顶的元素就是我们要的结果

代码演示

/*
 * @lc app=leetcode.cn id=215 lang=typescript
 *
 * [215] 数组中的第K个最大元素
 */

// @lc code=start

export type HeapDataStruct<T> = {
    idx?: number,
    data: T
};
export type CompareFn<T> = (x: HeapDataStruct<T>, y: HeapDataStruct<T>) => boolean;
class Heap<T> {
    private arr: HeapDataStruct<T>[] = [];
    private count: number = 0;
    constructor(private cmpFn: CompareFn<T>) {

    }

    private _compare(curIdx: number, pIdx: number): boolean {
        return this.cmpFn(this.arr[curIdx], this.arr[pIdx])
    }

    private _shift_up(idx: number): void {
        // 然后对堆进行向上调整
        // 我们需要通过子节点坐标得到父节点的坐标,因为我们这边的下标是从0开始的,所以左子树根节点坐标为:2 * i + 1,右子树根节点坐标为:2 * i + 2。那么我们父节点的编号:parseInt((子节点编号 - 1) / 2),如知道左节点编号为:2 * i + 1,那么他的父节点坐标就是: parseInt((2 * i + 1 - 1) / 2) = parseInt(i);如果知道有节点的编号:2 * i + 2,那么他的父节点的坐标为:parseInt((2 * i + 2 - 1) / 2) = parseInt((2*i+1)/2) = i
        // 上面我们讲过,我们需要如果在一个大顶堆中,如果我们父节点的值小于子节点的值的话,就要交换这两个值
        // while(idx && this.arr[parseInt(String((idx - 1) / 2))].data < this.arr[idx].data) {
        while(idx && this._compare(parseInt(String((idx - 1) / 2)), idx)) {
          // 父节点编号
          const pIdx = parseInt(String((idx - 1) / 2));
          // 交换父节点和当前节点的值
          [ this.arr[pIdx], this.arr[idx] ] = [ this.arr[idx], this.arr[pIdx] ];
          // 然后再让idx变成父节点的编号,这样就能依次向上调整了
          idx = pIdx;
        }
      }

      private _shift_down(idx: number): void {
        // 交换之后,根节点再依次向下调整
        // 最大的子节点的下标
        let n = this.count - 1;
        // 当idx的子节点的下标比最大的子节点的下标小,就说明还有子节点,就要往下继续调整
        while(idx * 2 + 1 <= n) {
          // max代表在根节点、左子树、右子树中最大值的下标
          let max = idx;
          // 如果左子树的根节点的值比当前值大,就把max更新为左子树根节点的下标
        //   if(this.arr[max].data < this.arr[2*idx+1].data) max = 2*idx+1;
          if(this._compare(max, 2*idx+1)) max = 2*idx+1;
          // 如果右子树根节点的值比当前节点大,就把max更新为右子树根节点的下标
          // 需要注意的是,我们当前的节点,可能存在左子树,但不一定存在右子树,所以需要多加一个2*idx+2<=n的条件
          if(2*idx+2<=n && this._compare(max, 2*idx+2)) max = 2*idx+2;
        //   if(2*idx+2<=n && this.arr[max].data < this.arr[2*idx+2].data) max = 2*idx+2;
          // 如果我的最大值的下标就是当前的这个值,那就不需要向下调整了,直接结束循环
          if(max === idx) break;
          // 然后交换这个最大值的下标和根节点
          [ this.arr[idx], this.arr[max] ] = [ this.arr[max], this.arr[idx] ];
          // 然后把idx改为原最大值的下标,继续向下调整
    
          idx = max;
        }
      }

    push(item: HeapDataStruct<T>): void {
        this.arr[this.count++] = item;
        this._shift_up(this.count-1);
    }

    pop(): HeapDataStruct<T>|null {
      if(this.size()===0) return null;
      const tmp = this.arr[0];
      // 根据上面的讲解,我们首先需要让队尾元素与根节点交换
      [ this.arr[0], this.arr[this.count-1] ] = [ this.arr[this.count-1], this.arr[0] ];
      // 交换之后记得count要减1
      this.count--;
      // 进行向下调整
      this._shift_down(0);
      return tmp;
    }

    size(): number {
        return this.count;
    }

    top(): HeapDataStruct<T> {
        return this.arr[0];
    }

    output(): void {
        console.log(JSON.stringify(this.arr));
    }
}

function findKthLargest(nums: number[], k: number): number {
    // 实例化一个小顶堆 
    const heap = new Heap<number>((x,y) => x.data>y.data);
    nums.forEach(num=>{
        heap.push({data: num});
        if(heap.size()>k) {
            heap.pop();
        }
    });
    return heap.top().data;
};
// @lc code=end


LeetCode 373. 查找和最小的K对数字

解题思路

按照套路,需要最小k...,则用大顶堆,需要最大k...,则用小顶堆,因此 构建一个大顶堆,大顶堆里面存放的是一个数字类型的数组,如果要和最小,就把和计算出来,然后根据: 大顶堆用小于、小顶堆用大于的元素直接判断。

然后将两个数组中的元素组成一个有两个元素组成的数组放到堆里面,当堆满时则弹出最大的元素即可

代码演示

/*
 * @lc app=leetcode.cn id=373 lang=typescript
 *
 * [373] 查找和最小的K对数字
 */

// @lc code=start
export type HeapDataStruct<T> = {
    idx?: number,
    data: T
};
export type CompareFn<T> = (x: HeapDataStruct<T>, y: HeapDataStruct<T>) => boolean;
class Heap<T> {
    private arr: HeapDataStruct<T>[] = [];
    private count: number = 0;
    constructor(private cmpFn: CompareFn<T>) {

    }

    private _compare(curIdx: number, pIdx: number): boolean {
        return this.cmpFn(this.arr[curIdx], this.arr[pIdx])
    }

    private _shift_up(idx: number): void {
        // 然后对堆进行向上调整
        // 我们需要通过子节点坐标得到父节点的坐标,因为我们这边的下标是从0开始的,所以左子树根节点坐标为:2 * i + 1,右子树根节点坐标为:2 * i + 2。那么我们父节点的编号:parseInt((子节点编号 - 1) / 2),如知道左节点编号为:2 * i + 1,那么他的父节点坐标就是: parseInt((2 * i + 1 - 1) / 2) = parseInt(i);如果知道有节点的编号:2 * i + 2,那么他的父节点的坐标为:parseInt((2 * i + 2 - 1) / 2) = parseInt((2*i+1)/2) = i
        // 上面我们讲过,我们需要如果在一个大顶堆中,如果我们父节点的值小于子节点的值的话,就要交换这两个值
        // while(idx && this.arr[parseInt(String((idx - 1) / 2))].data < this.arr[idx].data) {
        while(idx && this._compare(parseInt(String((idx - 1) / 2)), idx)) {
          // 父节点编号
          const pIdx = parseInt(String((idx - 1) / 2));
          // 交换父节点和当前节点的值
          [ this.arr[pIdx], this.arr[idx] ] = [ this.arr[idx], this.arr[pIdx] ];
          // 然后再让idx变成父节点的编号,这样就能依次向上调整了
          idx = pIdx;
        }
      }

      private _shift_down(idx: number): void {
        // 交换之后,根节点再依次向下调整
        // 最大的子节点的下标
        let n = this.count - 1;
        // 当idx的子节点的下标比最大的子节点的下标小,就说明还有子节点,就要往下继续调整
        while(idx * 2 + 1 <= n) {
          // max代表在根节点、左子树、右子树中最大值的下标
          let max = idx;
          // 如果左子树的根节点的值比当前值大,就把max更新为左子树根节点的下标
        //   if(this.arr[max].data < this.arr[2*idx+1].data) max = 2*idx+1;
          if(this._compare(max, 2*idx+1)) max = 2*idx+1;
          // 如果右子树根节点的值比当前节点大,就把max更新为右子树根节点的下标
          // 需要注意的是,我们当前的节点,可能存在左子树,但不一定存在右子树,所以需要多加一个2*idx+2<=n的条件
          if(2*idx+2<=n && this._compare(max, 2*idx+2)) max = 2*idx+2;
        //   if(2*idx+2<=n && this.arr[max].data < this.arr[2*idx+2].data) max = 2*idx+2;
          // 如果我的最大值的下标就是当前的这个值,那就不需要向下调整了,直接结束循环
          if(max === idx) break;
          // 然后交换这个最大值的下标和根节点
          [ this.arr[idx], this.arr[max] ] = [ this.arr[max], this.arr[idx] ];
          // 然后把idx改为原最大值的下标,继续向下调整
    
          idx = max;
        }
      }

    push(item: HeapDataStruct<T>): void {
        this.arr[this.count++] = item;
        this._shift_up(this.count-1);
        // this.output('push: ');
    }

    pop(): HeapDataStruct<T>|null {
      if(this.size()===0) return null;
      const tmp = this.arr[0];
      // 根据上面的讲解,我们首先需要让队尾元素与根节点交换
      [ this.arr[0], this.arr[this.count-1] ] = [ this.arr[this.count-1], this.arr[0] ];
      // 交换之后记得count要减1
      this.count--;
      // 进行向下调整
      this._shift_down(0);
    //   this.output('pop: ');
      return tmp;
    }

    size(): number {
        return this.count;
    }

    top(): HeapDataStruct<T> {
        return this.arr[0];
    }

    output(pre: string = ""): void {
        console.log(pre+JSON.stringify(this.arr));
    }

    // 将堆中的数据里面的data单独拿出来放到数组返回
    getArray(): T[] {
        return this.arr.slice(0, this.count).map(item=>item.data);
    }
}

function kSmallestPairs(nums1: number[], nums2: number[], k: number): number[][] {
    // 按照套路,需要最小k...,则用大顶堆,需要最大k...,则用小顶堆,因此
    // 构建一个小顶堆,大顶堆里面存放的是一个数字类型的数组,如果要和最小,就把和计算出来,然后根据:
    // 大顶堆用小于、小顶堆用大于的元素直接判断
    const cmp = (x, y) => x.data[0] + x.data[1] < y.data[0] + y.data[1];
    const heap = new Heap<number[]>(cmp);
    nums1.forEach(num1 => {
        nums2.forEach(num2 => {
            const top = heap.top();
            // 当堆的大小不超过k并且我们要加入的数据比堆顶元素小时才加入堆中,否则直接跳出当前循环
            // 为什么要直接跳出当前循环呢?我们再看一下题目,题目给的nums1和nums2是一个有序数组,如果当前的num2与num1相加的结果都已经大于堆顶元素了,
            // 那nums2后面的元素跟nums1相加的结果肯定更大,根本没有对比下去的必要,因此,处于性能原因,如果出现这种情况,我们直接跳出nums2的循环,
            // 继续进入下一轮nums1的循环
          	if(heap.size() < k || cmp({data: [num1, num2]}, top)) {
              // 将两个数组中的数据添加到堆中
              heap.push({
                  data: [num1, num2]
              });
              // 如果堆的大小超过k,就弹出最大值
              if(heap.size()>k) heap.pop();
            } else {
              return false;
            }
        });
    });
    // 最后,我们堆里面存放的就是我们要的答案
    return heap.getArray();
};
// @lc code=end


LeetCode 692. 前K个高频单词

解题思路

这题的解题思路是:首先计算出每个单词出现的频次,因为我们要算的是出现频次最多的k个,看到“多”我们就要联想到使用小顶堆,因此,我们需要构建一个小顶堆,但是,这道题有一个特殊要求,当两个单词出现频次一样时,需要按照单词的ASCII表排序,因此,我们再定义小顶堆的比较函数和最终输出结果数组时,都需要对这种情况做一下特殊处理。在js中,字符串提供了一个很方便的按照ASCII表排序的辅助方法String.prototype.localeCompare()可以让我们很方便的比较两个字符串的ASCII谁打谁小。

代码演示

/*
 * @lc app=leetcode.cn id=692 lang=typescript
 *
 * [692] 前K个高频单词
 */

// @lc code=start

export type HeapDataStruct<T> = {
    idx?: number,
    data: T
};
export type CompareFn<T> = (x: HeapDataStruct<T>, y: HeapDataStruct<T>) => boolean;
class Heap<T> {
    private arr: HeapDataStruct<T>[] = [];
    private count: number = 0;
    constructor(private cmpFn: CompareFn<T>) {

    }

    private _compare(curIdx: number, pIdx: number): boolean {
        return this.cmpFn(this.arr[curIdx], this.arr[pIdx])
    }

    private _shift_up(idx: number): void {
        // 然后对堆进行向上调整
        // 我们需要通过子节点坐标得到父节点的坐标,因为我们这边的下标是从0开始的,所以左子树根节点坐标为:2 * i + 1,右子树根节点坐标为:2 * i + 2。那么我们父节点的编号:parseInt((子节点编号 - 1) / 2),如知道左节点编号为:2 * i + 1,那么他的父节点坐标就是: parseInt((2 * i + 1 - 1) / 2) = parseInt(i);如果知道有节点的编号:2 * i + 2,那么他的父节点的坐标为:parseInt((2 * i + 2 - 1) / 2) = parseInt((2*i+1)/2) = i
        // 上面我们讲过,我们需要如果在一个大顶堆中,如果我们父节点的值小于子节点的值的话,就要交换这两个值
        // while(idx && this.arr[parseInt(String((idx - 1) / 2))].data < this.arr[idx].data) {
        while(idx && this._compare(parseInt(String((idx - 1) / 2)), idx)) {
          // 父节点编号
          const pIdx = parseInt(String((idx - 1) / 2));
          // 交换父节点和当前节点的值
          [ this.arr[pIdx], this.arr[idx] ] = [ this.arr[idx], this.arr[pIdx] ];
          // 然后再让idx变成父节点的编号,这样就能依次向上调整了
          idx = pIdx;
        }
      }

      private _shift_down(idx: number): void {
        // 交换之后,根节点再依次向下调整
        // 最大的子节点的下标
        let n = this.count - 1;
        // 当idx的子节点的下标比最大的子节点的下标小,就说明还有子节点,就要往下继续调整
        while(idx * 2 + 1 <= n) {
          // max代表在根节点、左子树、右子树中最大值的下标
          let max = idx;
          // 如果左子树的根节点的值比当前值大,就把max更新为左子树根节点的下标
        //   if(this.arr[max].data < this.arr[2*idx+1].data) max = 2*idx+1;
          if(this._compare(max, 2*idx+1)) max = 2*idx+1;
          // 如果右子树根节点的值比当前节点大,就把max更新为右子树根节点的下标
          // 需要注意的是,我们当前的节点,可能存在左子树,但不一定存在右子树,所以需要多加一个2*idx+2<=n的条件
          if(2*idx+2<=n && this._compare(max, 2*idx+2)) max = 2*idx+2;
        //   if(2*idx+2<=n && this.arr[max].data < this.arr[2*idx+2].data) max = 2*idx+2;
          // 如果我的最大值的下标就是当前的这个值,那就不需要向下调整了,直接结束循环
          if(max === idx) break;
          // 然后交换这个最大值的下标和根节点
          [ this.arr[idx], this.arr[max] ] = [ this.arr[max], this.arr[idx] ];
          // 然后把idx改为原最大值的下标,继续向下调整
    
          idx = max;
        }
      }

    push(item: HeapDataStruct<T>): void {
        this.arr[this.count++] = item;
        this._shift_up(this.count-1);
        // this.output('push: ');
    }

    pop(): HeapDataStruct<T>|null {
      if(this.size()===0) return null;
      const tmp = this.arr[0];
      // 根据上面的讲解,我们首先需要让队尾元素与根节点交换
      [ this.arr[0], this.arr[this.count-1] ] = [ this.arr[this.count-1], this.arr[0] ];
      // 交换之后记得count要减1
      this.count--;
      // 进行向下调整
      this._shift_down(0);
    //   this.output('pop: ');
      return tmp;
    }

    size(): number {
        return this.count;
    }

    top(): HeapDataStruct<T> {
        return this.arr[0];
    }

    output(pre: string = ""): void {
        console.log(pre+JSON.stringify(this.getArray()));
    }

    // 将堆中的数据里面的data单独拿出来放到数组返回
    getArray(sort: (a: HeapDataStruct<T>, b: HeapDataStruct<T>) => number = () => 0): T[] {
        return this.arr.slice(0, this.count).sort(sort).map(item=>item.data);
    }
}

function topKFrequent(words: string[], k: number): string[] {
    // 定义一个map用来存储每个单词出现的次数,key为单词,val为次数
    const map = {};
    // 定义小顶堆的比较规则
    const cmp = (x,y) => {
        // 如果两个单词出现频率不一样,因为是一个小顶堆,那么当前节点的次数应该要大于父节点的次数
        if(x.idx!==y.idx) return x.idx>y.idx;
        // 如果两个单词出现的频率是一样的,那我们就根据ascrii的顺序从小到大排
        return x.data<y.data;
    };
    // 实例化一个小顶堆
    const heap = new Heap<string>(cmp);
    // 计算所有单词出现的次数
    words.forEach(word=>{
        map[word]=(map[word]||0)+1;
    });
    Object.keys(map).forEach(key=>{
        // 将每个单词和他对应的次数放入堆中
        heap.push({
            idx: map[key],
            data: key
        });
        // 当堆满时弹出最小值
        if(heap.size()>k) heap.pop();
    });
    // 输出的结果还需要再排序依次,首先是根据出现次数从大到小排序,如果次数相同,则||左侧的表达式将会是0,
    // 则会进入右侧表达式,右侧表达式将会根据本地规则(ASCII)比较两个字符串的大小
    return heap.getArray((a,b) => {
        return (b.idx - a.idx) || a.data.localeCompare(b.data);
    });
};
// @lc code=end


LeetCode 295. 数据流的中位数

解题思路

这道题我们需要使用一个叫做对顶堆的结构来实现,其实对顶堆就是左边一个大顶堆,右边一个小顶堆,我们通过大顶堆小顶堆的堆顶元素就能够维护两个堆组成的集合的中位数。

      中位数
┌┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┐
				┆
└┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┘
  大顶堆   小顶堆
  
# 如上图我们大顶堆的堆顶和小顶堆的堆顶都在中位数那条虚线一侧,我们尽可能保证大顶堆和小顶堆的内元素的数量一致,但大顶堆的数量过大时,我们可以吧大顶堆中的堆顶元素移动到小顶堆中,让他们尽可能的均匀。这样,当我们所有的树都加入到了两个堆中时,如果总数是奇数,那么,中位数就是我们两个堆中数量较大的那个堆的堆顶元素,如果总数是偶数,则是两个堆顶元素的平均值

代码演示

/*
 * @lc app=leetcode.cn id=295 lang=typescript
 *
 * [295] 数据流的中位数
 */

// @lc code=start
export type HeapDataStruct<T> = {
    idx?: number,
    data: T
};
export type CompareFn<T> = (x: HeapDataStruct<T>, y: HeapDataStruct<T>) => boolean;
class Heap<T> {
    private arr: HeapDataStruct<T>[] = [];
    private count: number = 0;
    constructor(private cmpFn: CompareFn<T>) {

    }

    private _compare(curIdx: number, pIdx: number): boolean {
        return this.cmpFn(this.arr[curIdx], this.arr[pIdx])
    }

    private _shift_up(idx: number): void {
        // 然后对堆进行向上调整
        // 我们需要通过子节点坐标得到父节点的坐标,因为我们这边的下标是从0开始的,所以左子树根节点坐标为:2 * i + 1,右子树根节点坐标为:2 * i + 2。那么我们父节点的编号:parseInt((子节点编号 - 1) / 2),如知道左节点编号为:2 * i + 1,那么他的父节点坐标就是: parseInt((2 * i + 1 - 1) / 2) = parseInt(i);如果知道有节点的编号:2 * i + 2,那么他的父节点的坐标为:parseInt((2 * i + 2 - 1) / 2) = parseInt((2*i+1)/2) = i
        // 上面我们讲过,我们需要如果在一个大顶堆中,如果我们父节点的值小于子节点的值的话,就要交换这两个值
        // while(idx && this.arr[parseInt(String((idx - 1) / 2))].data < this.arr[idx].data) {
        while(idx && this._compare(parseInt(String((idx - 1) / 2)), idx)) {
          // 父节点编号
          const pIdx = parseInt(String((idx - 1) / 2));
          // 交换父节点和当前节点的值
          [ this.arr[pIdx], this.arr[idx] ] = [ this.arr[idx], this.arr[pIdx] ];
          // 然后再让idx变成父节点的编号,这样就能依次向上调整了
          idx = pIdx;
        }
      }

      private _shift_down(idx: number): void {
        // 交换之后,根节点再依次向下调整
        // 最大的子节点的下标
        let n = this.count - 1;
        // 当idx的子节点的下标比最大的子节点的下标小,就说明还有子节点,就要往下继续调整
        while(idx * 2 + 1 <= n) {
          // max代表在根节点、左子树、右子树中最大值的下标
          let max = idx;
          // 如果左子树的根节点的值比当前值大,就把max更新为左子树根节点的下标
        //   if(this.arr[max].data < this.arr[2*idx+1].data) max = 2*idx+1;
          if(this._compare(max, 2*idx+1)) max = 2*idx+1;
          // 如果右子树根节点的值比当前节点大,就把max更新为右子树根节点的下标
          // 需要注意的是,我们当前的节点,可能存在左子树,但不一定存在右子树,所以需要多加一个2*idx+2<=n的条件
          if(2*idx+2<=n && this._compare(max, 2*idx+2)) max = 2*idx+2;
        //   if(2*idx+2<=n && this.arr[max].data < this.arr[2*idx+2].data) max = 2*idx+2;
          // 如果我的最大值的下标就是当前的这个值,那就不需要向下调整了,直接结束循环
          if(max === idx) break;
          // 然后交换这个最大值的下标和根节点
          [ this.arr[idx], this.arr[max] ] = [ this.arr[max], this.arr[idx] ];
          // 然后把idx改为原最大值的下标,继续向下调整
    
          idx = max;
        }
      }

    push(item: HeapDataStruct<T>): void {
        this.arr[this.count++] = item;
        this._shift_up(this.count-1);
        // this.output('push: ');
    }

    pop(): HeapDataStruct<T>|null {
      if(this.size()===0) return null;
      const tmp = this.arr[0];
      // 根据上面的讲解,我们首先需要让队尾元素与根节点交换
      [ this.arr[0], this.arr[this.count-1] ] = [ this.arr[this.count-1], this.arr[0] ];
      // 交换之后记得count要减1
      this.count--;
      // 进行向下调整
      this._shift_down(0);
    //   this.output('pop: ');
      return tmp;
    }

    size(): number {
        return this.count;
    }

    top(): HeapDataStruct<T> {
        return this.arr[0];
    }

    output(pre: string = ""): void {
        console.log(pre+JSON.stringify(this.getArray()));
    }

    // 将堆中的数据里面的data单独拿出来放到数组返回
    getArray(sort: (a: HeapDataStruct<T>, b: HeapDataStruct<T>) => number = () => 0): T[] {
        return this.arr.slice(0, this.count).sort(sort).map(item=>item.data);
    }
}

class MedianFinder {
    private lHeap: Heap<number>;
    private rHeap: Heap<number>;
    constructor() {
        // 左侧维护一个大顶堆
        this.lHeap = new Heap((x,y)=>x.data<y.data);
        // 右侧维护一个小顶堆
        this.rHeap = new Heap((x,y)=>x.data>y.data);
    }

    addNum(num: number): void {
        // 我们尽量现在左侧的堆中插入元素
        // 所以,当左侧的堆为空或者要插入的元素小于等于左侧堆顶元素时插入左堆
        if(this.lHeap.size()===0 || num <= this.lHeap.top().data) {
            this.lHeap.push({data: num});
        } else {
            this.rHeap.push({data: num});
        }

        // 调整左右两个堆元素数量,始终保证左堆最多比右堆多一个元素
        this.transformHeap();
        
    }

    transformHeap(){
        // 我们约定,左侧的堆lHeap最多只能比右侧的堆多一个元素
        // 当我们总数是奇数时,左侧堆的堆顶元素就是我们要的中位数
        // 当我们总数是偶数时,左右两个堆的堆顶元素的平均数就是我们要的中位数
        if(this.rHeap.size() > this.lHeap.size()) {
            this.lHeap.push(this.rHeap.pop());
        }
        if(this.lHeap.size() === this.rHeap.size()+2) {
            this.rHeap.push(this.lHeap.pop());
        }
    }

    findMedian(): number {
        // 因为我们尽可能往左侧的堆中先插入元素,并且左侧最多比右侧多一个,因此,当左侧堆的数量大于右侧时
        // 说明此时中暑是基数,直接将左堆的堆顶元素返回即可
        if(this.lHeap.size()>this.rHeap.size()) {
            return this.lHeap.top().data;
        } else {// 为偶数时,则需要去左右两个堆的平均值
            return (this.lHeap.top().data + this.rHeap.top().data) / 2;
        }
    }
}

/**
 * Your MedianFinder object will be instantiated and called as such:
 * var obj = new MedianFinder()
 * obj.addNum(num)
 * var param_2 = obj.findMedian()
 */
// @lc code=end


LeetCode 面试题 17.20. 连续中值

解题思路

这道题跟上一道题解法一模一样,这里就不再重复了

LeetCode 264. 丑数 II

解题思路

这道题其实关键点在于,我们怎么避免重复计算某些树,也就是对这个堆进行剪枝,其实我们只需要在只有当与某个质因数取模为0时,只乘上它本身和比它大的质因数即可,对于比它小的质因数,之前已经计算过了,无需重复计算。

代码演示

/*
 * @lc app=leetcode.cn id=264 lang=typescript
 *
 * [264] 丑数 II
 */

// @lc code=start
export type HeapDataStruct<T> = {
    idx?: number,
    data: T
};
export type CompareFn<T> = (x: HeapDataStruct<T>, y: HeapDataStruct<T>) => boolean;
class Heap<T> {
    private arr: HeapDataStruct<T>[] = [];
    private count: number = 0;
    constructor(private cmpFn: CompareFn<T>) {

    }

    private _compare(curIdx: number, pIdx: number): boolean {
        return this.cmpFn(this.arr[curIdx], this.arr[pIdx])
    }

    private _shift_up(idx: number): void {
        // 然后对堆进行向上调整
        // 我们需要通过子节点坐标得到父节点的坐标,因为我们这边的下标是从0开始的,所以左子树根节点坐标为:2 * i + 1,右子树根节点坐标为:2 * i + 2。那么我们父节点的编号:parseInt((子节点编号 - 1) / 2),如知道左节点编号为:2 * i + 1,那么他的父节点坐标就是: parseInt((2 * i + 1 - 1) / 2) = parseInt(i);如果知道有节点的编号:2 * i + 2,那么他的父节点的坐标为:parseInt((2 * i + 2 - 1) / 2) = parseInt((2*i+1)/2) = i
        // 上面我们讲过,我们需要如果在一个大顶堆中,如果我们父节点的值小于子节点的值的话,就要交换这两个值
        // while(idx && this.arr[parseInt(String((idx - 1) / 2))].data < this.arr[idx].data) {
        while(idx && this._compare(parseInt(String((idx - 1) / 2)), idx)) {
          // 父节点编号
          const pIdx = parseInt(String((idx - 1) / 2));
          // 交换父节点和当前节点的值
          [ this.arr[pIdx], this.arr[idx] ] = [ this.arr[idx], this.arr[pIdx] ];
          // 然后再让idx变成父节点的编号,这样就能依次向上调整了
          idx = pIdx;
        }
      }

      private _shift_down(idx: number): void {
        // 交换之后,根节点再依次向下调整
        // 最大的子节点的下标
        let n = this.count - 1;
        // 当idx的子节点的下标比最大的子节点的下标小,就说明还有子节点,就要往下继续调整
        while(idx * 2 + 1 <= n) {
          // max代表在根节点、左子树、右子树中最大值的下标
          let max = idx;
          // 如果左子树的根节点的值比当前值大,就把max更新为左子树根节点的下标
        //   if(this.arr[max].data < this.arr[2*idx+1].data) max = 2*idx+1;
          if(this._compare(max, 2*idx+1)) max = 2*idx+1;
          // 如果右子树根节点的值比当前节点大,就把max更新为右子树根节点的下标
          // 需要注意的是,我们当前的节点,可能存在左子树,但不一定存在右子树,所以需要多加一个2*idx+2<=n的条件
          if(2*idx+2<=n && this._compare(max, 2*idx+2)) max = 2*idx+2;
        //   if(2*idx+2<=n && this.arr[max].data < this.arr[2*idx+2].data) max = 2*idx+2;
          // 如果我的最大值的下标就是当前的这个值,那就不需要向下调整了,直接结束循环
          if(max === idx) break;
          // 然后交换这个最大值的下标和根节点
          [ this.arr[idx], this.arr[max] ] = [ this.arr[max], this.arr[idx] ];
          // 然后把idx改为原最大值的下标,继续向下调整
    
          idx = max;
        }
      }

    push(item: HeapDataStruct<T>): void {
        this.arr[this.count++] = item;
        this._shift_up(this.count-1);
        // this.output('push: ');
    }

    pop(): HeapDataStruct<T>|null {
      if(this.size()===0) return null;
      const tmp = this.arr[0];
      // 根据上面的讲解,我们首先需要让队尾元素与根节点交换
      [ this.arr[0], this.arr[this.count-1] ] = [ this.arr[this.count-1], this.arr[0] ];
      // 交换之后记得count要减1
      this.count--;
      // 进行向下调整
      this._shift_down(0);
    //   this.output('pop: ');
      return tmp;
    }

    size(): number {
        return this.count;
    }

    top(): HeapDataStruct<T> {
        return this.arr[0];
    }

    output(pre: string = ""): void {
        console.log(pre+JSON.stringify(this.getArray()));
    }

    // 将堆中的数据里面的data单独拿出来放到数组返回
    getArray(sort: (a: HeapDataStruct<T>, b: HeapDataStruct<T>) => number = () => 0): T[] {
        return this.arr.slice(0, this.count).sort(sort).map(item=>item.data);
    }
}
function nthUglyNumber(n: number): number {
    // 定义一个小顶堆
    const heap = new Heap<number>((x, y)=>x.data>y.data);
    // 先把1放进去
    heap.push({data: 1});
    let res;
    while(n--) {
        // 弹出堆顶元素作为这一轮的结果
        res = heap.pop();
        // 为了避免重复计算,我们可以对我们的堆进行剪枝
        // 只有当与某个质因数取模为0时,只乘上它本身和比它大的质因数
        if(res.data % 5 === 0) {
            heap.push({data: res.data * 5});
        } else if(res.data % 3 === 0) {
            heap.push({data: res.data * 5});
            heap.push({data: res.data * 3});
        } else {
            heap.push({data: res.data * 5});
            heap.push({data: res.data * 3});
            heap.push({data: res.data * 2});
        }
    }
    // 经过了n轮的遍历之后,最终得到的结果就是我们要的丑数
    return res.data;
};
// @lc code=end


LeetCode 1801. 积压订单中的订单总数

解题思路

对于这道题,我们肯定是希望我们的购买订单金额越大越好,所以需要维护最大值,要用大顶堆,而销售订单,金额越小越好,这样才能挣更多的差价,所以要维护最小值,用小顶堆。

代码演示

export type HeapDataStruct<T> = {
    idx?: number,
    data: T
};
export type CompareFn<T> = (x: HeapDataStruct<T>, y: HeapDataStruct<T>) => boolean;
class Heap<T> {
    private arr: HeapDataStruct<T>[] = [];
    private count: number = 0;
    constructor(private cmpFn: CompareFn<T>) {

    }

    private _compare(curIdx: number, pIdx: number): boolean {
        return this.cmpFn(this.arr[curIdx], this.arr[pIdx])
    }

    private _shift_up(idx: number): void {
        // 然后对堆进行向上调整
        // 我们需要通过子节点坐标得到父节点的坐标,因为我们这边的下标是从0开始的,所以左子树根节点坐标为:2 * i + 1,右子树根节点坐标为:2 * i + 2。那么我们父节点的编号:parseInt((子节点编号 - 1) / 2),如知道左节点编号为:2 * i + 1,那么他的父节点坐标就是: parseInt((2 * i + 1 - 1) / 2) = parseInt(i);如果知道有节点的编号:2 * i + 2,那么他的父节点的坐标为:parseInt((2 * i + 2 - 1) / 2) = parseInt((2*i+1)/2) = i
        // 上面我们讲过,我们需要如果在一个大顶堆中,如果我们父节点的值小于子节点的值的话,就要交换这两个值
        // while(idx && this.arr[parseInt(String((idx - 1) / 2))].data < this.arr[idx].data) {
        while(idx && this._compare(parseInt(String((idx - 1) / 2)), idx)) {
          // 父节点编号
          const pIdx = parseInt(String((idx - 1) / 2));
          // 交换父节点和当前节点的值
          [ this.arr[pIdx], this.arr[idx] ] = [ this.arr[idx], this.arr[pIdx] ];
          // 然后再让idx变成父节点的编号,这样就能依次向上调整了
          idx = pIdx;
        }
      }

      private _shift_down(idx: number): void {
        // 交换之后,根节点再依次向下调整
        // 最大的子节点的下标
        let n = this.count - 1;
        // 当idx的子节点的下标比最大的子节点的下标小,就说明还有子节点,就要往下继续调整
        while(idx * 2 + 1 <= n) {
          // max代表在根节点、左子树、右子树中最大值的下标
          let max = idx;
          // 如果左子树的根节点的值比当前值大,就把max更新为左子树根节点的下标
        //   if(this.arr[max].data < this.arr[2*idx+1].data) max = 2*idx+1;
          if(this._compare(max, 2*idx+1)) max = 2*idx+1;
          // 如果右子树根节点的值比当前节点大,就把max更新为右子树根节点的下标
          // 需要注意的是,我们当前的节点,可能存在左子树,但不一定存在右子树,所以需要多加一个2*idx+2<=n的条件
          if(2*idx+2<=n && this._compare(max, 2*idx+2)) max = 2*idx+2;
        //   if(2*idx+2<=n && this.arr[max].data < this.arr[2*idx+2].data) max = 2*idx+2;
          // 如果我的最大值的下标就是当前的这个值,那就不需要向下调整了,直接结束循环
          if(max === idx) break;
          // 然后交换这个最大值的下标和根节点
          [ this.arr[idx], this.arr[max] ] = [ this.arr[max], this.arr[idx] ];
          // 然后把idx改为原最大值的下标,继续向下调整
    
          idx = max;
        }
      }

    push(item: HeapDataStruct<T>): void {
        this.arr[this.count++] = item;
        this._shift_up(this.count-1);
        // this.output('push: ');
    }

    pop(): HeapDataStruct<T>|null {
      if(this.size()===0) return null;
      const tmp = this.arr[0];
      // 根据上面的讲解,我们首先需要让队尾元素与根节点交换
      [ this.arr[0], this.arr[this.count-1] ] = [ this.arr[this.count-1], this.arr[0] ];
      // 交换之后记得count要减1
      this.count--;
      // 进行向下调整
      this._shift_down(0);
    //   this.output('pop: ');
      return tmp;
    }

    size(): number {
        return this.count;
    }

    top(): HeapDataStruct<T> {
        return this.arr[0];
    }

    output(pre: string = ""): void {
        console.log(pre+JSON.stringify(this.getArray()));
    }

    // 将堆中的数据里面的data单独拿出来放到数组返回
    getArray(sort: (a: HeapDataStruct<T>, b: HeapDataStruct<T>) => number = () => 0): T[] {
        return this.arr.slice(0, this.count).sort(sort).map(item=>item.data);
    }
}

function getNumberOfBacklogOrders(orders: number[][]): number {
    // 用大顶堆存储采购订单
    const buyHeap = new Heap<number[]>((x, y) => x.data[0]<y.data[0]);
    // 用小顶堆存储销售订单
    const sellHeap = new Heap<number[]>((x, y) => x.data[0]>y.data[0]);
    // 遍历所有订单
    orders.forEach(order=> {
        if(order[2] === 0) {// 采购订单
            // 1. 采购订单的数量不为0
            // 2. 销售订单的数量不为0
            // 3. 采购订单的金额要大于或等于销售订单的金额
            while(order[1] && sellHeap.size() && order[0] >= sellHeap.top().data[0]) {
                // 需要考虑销售订单和采购订单的数量问题,我们应该从这两个订单数量中取个最小值作为这次要操作的订单数量
                const count = Math.min(order[1], sellHeap.top().data[1]);
                // 明确要操作的订单数量之后,需要让采购订单和销售订单都减去这个数量
                order[1] -= count;
                sellHeap.top().data[1] -= count;
                // 此时,如果我们销售订单数量为0时,我们就把这个订单从堆中弹出
                if(sellHeap.top().data[1] === 0) sellHeap.pop();
            }
            // 如果经历了一轮循环之后,我们当前的订单order还有剩余的话,就说明存在积压订单,我们把积压订单放到购买订单中
            order[1]!==0 && buyHeap.push({data: order});
        } else {// 销售订单
            while(order[1] && buyHeap.size() && order[0] <= buyHeap.top().data[0]) {
                const count = Math.min(order[1], buyHeap.top().data[1]);
                
                order[1] -= count;
                buyHeap.top().data[1] -= count;
            
                if(buyHeap.top().data[1] === 0) buyHeap.pop();
            }
            
            order[1]!==0 && sellHeap.push({data: order});
        }
    });

    let res = 0;
    // 依据题意,数值可能过大,我们需要对订单数量与mod求模
    const mod = 1000000007;
    // 拿到采购堆的数组格式数据
    const buyArray = buyHeap.getArray();
    // 拿到销售堆的数组格式数据
    const sellArray = sellHeap.getArray();

    // 累计订单总数到res中
    buyArray.forEach(order => {
        res += (res + order[1]) % mod;
    });

    sellArray.forEach(order => {
        res += (res + order[1]) % mod;
    });

    return res;

};