跟着大佬走——js 实现堆的数据结构

522 阅读3分钟

堆(heap)是计算机科学中一类特殊的数据结构统称。通常是一个可被看做一棵树的数组对象。

当一个数据结构满足:

  • 任意节点 P 和 C,若 P 是 C 的父节点,那么一定有 P 的值小于(或大于)C 的值。
  • 总是一棵完全二叉树

则称这个数据结构为最小堆(最大堆)。

堆是一个非线性结构,即一维数组有两个直接后继。

最小堆

最大堆

不难看出,上图中堆的数据结构,可用数组模拟,既:

// 最小堆
const minHeap = [
  10, 11, 12, 13,
  14, 15, 16
];

// 最大堆
const maxHeap = [
  10, 9, 8, 7,
  6, 5, 4
]

root 节点子节点索引分别为1, 2,而索引等于 1 的节点,其子节点所以则是3, 4,由此推得:父节点为 i 时,子节点的索引分别为: 2 * i + 1, 2 * i + 2

子节点索引为 j 时,父节点索引 i 为: i = Math.floor(j / 2)

堆的操作

首先,初始化一个最小堆:

class MinHeap {
  constructor() {
    this.heap = []
  }
  getMin() {
    return this.heap[0]
  }
  getParentNode(index) {
    return
      this.heap[Math.floor(index / 2)]
  }
  swap(indx1, indx2) {
    const temp = this.heap[indx1]
    this.heap[indx1] = this.heap[indx2]
    this.heap[indx2] = temp
  }
}

插入数据

  • heap 为空时,键值 10 作为根节点
  • 23 > 10,可作为 10 的 子节点
  • 36 > 10,可作为 10 的 子节点
  • 18 < 23,不可作为 23 的子节点,调换 23 与 18 的位置。18 > 23 符合条件,构造完成

代码实现如下:

...
insert(node) {
  this.heap.push(node)
  if(this.heap.length > 1) {
    let currentIdx = this.heap.length - 1
    // 依次向上查找
    while(currentIdx > 1 &&
      this.getParentNode(node) >
      this.heap[currentIdx]) {
        // 最小堆子节点大于父节点时,需互换父子节点
        const parentIndex = Math.floor(currentIndex / 2)
        this.swap(parentIndex,currentIndex)
        currentIndex = parentIndex
      }
  }
}
...

删除数据

最小堆删除根节点时,将最后一位移动至根节点,依次比较根节点与子节点大小,若根节点大于子节点,则调换位置,重复至比较完成。

...
remove() {
  let min = this.getMin()
  if(this.heap.length === 2) {
    this.heap.splice(1, 1)
  } else if (this.heap.length > 2){
    // 最后一位移动至根节点
    const len = this.heap.length
    this.heap[1] = this.heap[len - 1]
    this.heap.splice(len - 1)
    if (len === 3) {
      if(this.heap[1] > this.heap[2]) {
        // 交换位置函数
        this.swap(12)
      }
      return min
    }
    
    let currentIndex = 1
    // 首位是null,所以不加1
    let leftChildIndex = currentIndex * 2
    let rightChildIndex = currentIndex * 2 + 1
    
    while(
      this.heap[leftChildIndex] &&
      this.heap[rightChildIndex] &&
      (this.heap[currentIndex] > this.heap[leftChildIndex] ||
      this.heap[currentIndex] > this.heap[rightChildIndex])
    ) {
      if(this.heap[leftChildIndex] < this.heap[rightChildIndex]) {
      this.swap(currentIndex, leftChildIndex)
      } else {
        this.swap(currentIndex, rightChildIndex)
      }
      leftChildIndex = currentIndex * 2
      rightChildIndex = currentIndex * 2 + 1
    }
  } else {
    return null
  }
}
...

总的来说堆的插入与删除,主要与子节点与父节点索引关系相关,大家多理解一下上方的推算。

建堆效率

n 个节点的堆,其高度为 h=logn。根为第 0 层,第 i 层节点个数为 2^i,由于堆高度与节点数目相关,因此插入节点删除普通元素删除最小元素的平均时间复杂度都为O(logn)

不要问数据结构在前端有什么用,学计算机的同学去了解基本算法与数据结构难道不是应该的吗。

我不能理解一些外企用“算法”定义一个人业务能力好坏的方式,但也不认为写代码的人理解学习基础知识是“卷”。

所以放下对算法的偏见,这只是一种思考方式而已。