js数据结构与算法随记-堆

266 阅读4分钟

最近在leetcode上遇到topK问题,但是js中没有堆这种数据结构,因此做这种题需要自己实现一个最大堆或最小堆。在此记录一下堆的相关知识点和js实现。

堆基本概念

  • 堆是一种特殊的二叉树。它的底层是一颗完全二叉树。一般用堆实现求最值和topk问题。
  • 最小堆:对于指定节点,它的值都小于等于其子节点,所以最小堆的根节点是最小值。
  • 最大堆:对于指定节点,它的值都大于等于其子节点,所以最大堆的根节点是最大值。

堆的数组表示

一般基于数组实现堆,下图是一个最小堆映射至数组的示例:

image.png

二叉堆从上到下,从左到右的顺序映射至数组,可以总结以下规律, 对于给定位置index的节点:

  1. 它的左侧子节点的位置是2 * index + 1(如果位置可用);
  2. 它的右侧子节点的位置是2 * index + 2(如果位置可用);
  3. 它的父节点位置是Math.floor((index - 1) / 2)(如果位置可用)。

使用js基于数组实现最小堆

堆常用的3个方法:

  1. 添加元素:offer()
  2. 删除堆顶元素: poll()
  3. 查看堆顶元素(堆顶元素就是最大/最小值):peek() tips:如果要解决topK问题,添加元素时需要判断堆的size是否超过k,其次是堆中如果存储了其他类型的数据,如map或者对象,只需要改造一下类中比较函数的实现。

以下基于最小堆,理解添加和删除操作中的两个重要过程。

理解添加元素时的上移操作过程:

下图展示的是新增元素1插入堆,并从最底层上移到堆顶的过程。因为1是堆中最小元素,最后上移到了最小堆的堆顶。

image.png

  1. 添加元素时,应该从上到下,从左到右找出第一个空缺位置,将新节点添加进去,数组中就是末尾添加元素。
  2. 插入空缺位置后,找到其父节点,如果新节点比其父节点的值小,则交换二者位置。
  3. 重复步骤2,直到满足下列条件之一:
    • 3.1 新节点的值大于等于其父节点的值

    • 3.2 新节点到达堆顶位置


// 上移操作
siftUp(index) {
     // 找到父节点位置
     let parentNodeIndex = this.getParentNodeIndex(index);
     // 比较插入节点与父节点值,如果父节点值大于插入节点,就交换位置。
     while(index > 0 && this.compareFn(this.heap[parentNodeIndex], this.heap[index]) > 0) {
           this.swap(this.heap, parentNodeIndex, index);
           index = parentNodeIndex;
           parentNodeIndex = this.getParentNodeIndex(index);
      }
}

理解删除堆顶元素时的下移操作过程:

下图展示了从最小堆中删除堆顶节点1,堆最底层最右边的节点9被移至堆顶,然后逐步从堆顶下移到最底层的过程

image.png

  1. 堆中删除元素一般指删除堆顶元素。删除堆顶元素(数组中第一个元素),并将堆最底层最右边的节点移到堆顶部。
  2. 比较堆顶元素与其左右子节点的值,如果堆顶值大于它的左右子节点的值,那么将堆顶元素与其左右子节点的较小值交换。
  3. 如果交换后节点值仍然大于其子节点的值,则继续交换。直到满足以下条件之一停止:
    • 3.1 节点值小于等于其左右子节点

    • 3.2 到达最底层位置

           // 下移操作
            siftDown(index) {
              // 记录下移元素的位置
              let curIndex = index;
              // 下移元素目前的左右子节点
              const leftNodeIndex = this.getLeftNodeIndex(index);
              const rightNodeIndex = this.getRightNodeIndex(index);
              // 最底层位置也就是数组的大小
              const size = this.size();
              // 以下两个判断可以得到左右子节点中的较小者
              if (leftNodeIndex < size && this.compareFn(this.heap[curIndex], this.heap[leftNodeIndex]) > 0) {
                curIndex = leftNodeIndex;
              }

              if (rightNodeIndex < size && this.compareFn(this.heap[curIndex], this.heap[rightNodeIndex]) > 0) {
                curIndex = rightNodeIndex;
              }
              // 判断最终是否需要调整
              if (index !== curIndex) {
                 this.swap(this.heap, index, curIndex);
                 // 相当于重复步骤2
                 this.siftDown(curIndex);
              }
            }

完整代码

      class MinHeap {
            constructor() {
                this.heap = [];
                this.compareFn = (a, b) => a - b;
            }
            // 向堆中插入新值
            offer(value) {
                if (value != null) {
                   this.heap.push(value);
                   this.siftUp(this.heap.length - 1);
                   return true;
                }
                return false;
            }
            // 获取最小值:堆顶元素
            peek() {
                return this.isEmpty() ? undefined : this.heap[0];
            }

            //堆中的删除:一般删除堆顶元素
            poll() {
                if (this.isEmpty()) {
                   return undefined;
                }
                if (this.size() === 1) {
                  return this.heap.shift();
                }
                // 获取堆顶元素
                const removedVal = this.heap[0];
                //将堆中最后一个元素移至堆顶
                this.heap[0] = this.heap.pop();
                // 堆顶元素下移
                this.siftDown(0);
                return removedVal;
            }

            size() {
                return this.heap.length;
            }

            isEmpty() {
                return this.size() === 0;
            }

             // 上移操作
             siftUp(index) {
               let parentNodeIndex = this.getParentNodeIndex(index);
               while(index > 0 && this.compareFn(this.heap[parentNodeIndex], this.heap[index]) > 0) {
                   this.swap(this.heap, parentNodeIndex, index);
                   index = parentNodeIndex;
                   parentNodeIndex = this.getParentNodeIndex(index);
               }
            }

            // 下移操作
            siftDown(index) {
              let curIndex = index;
              const leftNodeIndex = this.getLeftNodeIndex(index);
              const rightNodeIndex = this.getRightNodeIndex(index);
              const size = this.size();
              if (leftNodeIndex < size && this.compareFn(this.heap[curIndex], this.heap[leftNodeIndex]) > 0) {
                curIndex = leftNodeIndex;
              }

              if (rightNodeIndex < size && this.compareFn(this.heap[curIndex], this.heap[rightNodeIndex]) > 0) {
                curIndex = rightNodeIndex;
              }

              if (index !== curIndex) {
                 this.swap(this.heap, index, curIndex);
                 this.siftDown(curIndex);
              }
            }

            getLeftNodeIndex(index) {
              return 2 * index + 1;
            }

            getRightNodeIndex(index) {
               return 2 * index + 2;
            }

            getParentNodeIndex(index) {
              if (index === 0) return undefined;  
              return Math.floor((index - 1) / 2);
            }

            swap(arr, index1, index2) {
               const temp = arr[index1];
               arr[index1] = arr[index2];
               arr[index2] = temp;
            }
        }
 
        const heap = new MinHeap();      
        heap.offer(6)
        heap.offer(7)   
        heap.offer(3)
        heap.offer(4)
        heap.offer(2)
        heap.offer(5)
        heap.offer(1)
        console.log(heap);
        console.log(heap.peek()); // 1
        console.log(heap.poll()); // 1