二叉堆与堆排序算法(JS)

202 阅读5分钟

二叉堆

基本概念

  1. 二叉树是常见的树数据结构.
  2. 二叉堆(堆)是特殊的二叉树.
  3. 二叉堆是完全二叉树(即,每一层都有左侧和右侧子节点,最后一层叶子节点,尽可能都是左侧节点).
  4. 二叉堆分为最大堆和最小堆,最大堆可以快速导出树的最大值,最小堆可以快速导出树的最小值.

用途

  1. 二叉堆能够高效地找出最大值最小值.
  2. 常用于优先队列,也就是堆排序中.

二叉堆与二叉搜索树

  1. 二叉堆是二叉树.但不一定是二叉搜索树.
  2. 我们知道二叉堆,分为最大堆和最小堆.
  3. 在最大堆中所有子节点都要小于等于父节点.
  4. 在最小堆中所有子节点都要大于等于父节点.
  5. 在二叉搜索树(BST)中,左侧子节点总是比父节点小,右侧子节点总是比父节点大.

用数组表示二叉树

  • 通过BFS(广度优先遍历)转换为Array.
tree
        1        ->    1          -> 1  2  3  4  5  6  7
    2       3    ->    2  3       array(广度优先遍历)
 4    5  6    7  ->    4  5  6  7
  • 父节点索引子节点索引存在以下关系.
      0       -> left  = parent*2 + 1 = 0*2 + 1 = 1 
   1     2    -> right = parent*2 + 2 = 0*2 + 2 = 2
 3   4 5   6  -> left  = parent*2 + 1 = 1*2 + 1 = 3
              -> right = parent*2 + 2 = 1*2 + 2 = 4
              -> left  = parent*2 + 1 = 2*2 + 1 = 5
              -> right = parent*2 + 2 = 2*2 + 2 = 6

构造最小堆

注意:

  1. 插入元素我们是不可以选位置的,我们需要从底部做上移操作.
  2. 删除元素我们也是不可以任意选位置的,我们删除的是堆顶元素,而且如果有子元素的话我们需要做下移操作.
class MinHeap {
  constructor() {
    this.heap = []
  }
  /** 
   * 根据父节点索引获取左侧子节点索引
   */
  getLeftIndex(index) {
    return 2 * index + 1;
  }
  /** 
   * 根据父节点索引获取右侧子节点索引
   */
  getRightIndex(index) {
    return 2 * index + 2;
  }
  /** 
   * 根据子节点索引获取父节点索引
   */
  getParentIndex(index) {
    // 索引为0,则没有父节点
    if (index === 0) {
      return undefined;
    }
    // (index - 1)/2 然后向下取整
    return Math.floor((index - 1) / 2);
  }
  /** 插入节点 */
  insert(value) {
    /** 
     * 返回 true 表示插入成功
     * 返回 false 表示插入失败
     * 
     * 插入成功和失败,取决于插入的数据类型是否为数字
     * 
     * 插入的具体方式:
     * 1. 直接给heap.push一个元素
     * 2. 根据最后一个索引,执行上移操作.
     */
    if (value != null) {
      this.heap.push(value);
      this.siftUp(this.heap.length - 1);
      return true;
    }
    return false;
  }
  /** 
   * 上移
   */
  siftUp(index) {
    /**
     * 1. 根据子节点索引获取父节点索引
     * 2. 执行上移操作的范围:上移操作的范围index 不能等于 0.
     * 3. 执行上移操作的条件: 父节点大于子节点.
     * 4. 执行上移操作的方法: 父节点和子节点调换位置.
     * 5. 执行上移操作的迭代: 子节点的索引不断替换成父节点的索引.
     */
    let parent = this.getParentIndex(index);
    while (
        index > 0 &&
        this.heap[parent] > this.heap[index]
      ) {
      [this.heap[parent],this.heap[index]] = [this.heap[index],this.heap[parent]]
      index = parent;
      parent = this.getParentIndex(index);
    }
  }

  size() {
    return this.heap.length;
  }
  isEmpty() {
    return this.size() === 0;
  }
  /**
   * 获取堆顶
   */
  findMinimum() {
    return this.isEmpty() ? undefined : this.heap[0];
  }
  /**
   * 移除堆顶
   */
  extract() {
    /**
     * 如果是空的就不移除
     */
    if (this.isEmpty()) {
      return undefined;
    }
    /**
     * 如果只有一个元素就只移除,不做下移操作
     */
    if (this.size() === 1) {
      return this.heap.shift();
    }
    /**
     * 如果有多个元素,则需要做下移操作
     */
    const removedValue = this.heap.shift();
    this.siftDown(0);
    return removedValue;
  }

  /**
   * 下移
   */
  siftDown(index) {
    /**
     * 1. 根于父元素索引,获取左右两边的子元素索引
     * 2. 获取堆的长度.
     * 
     * 3. 由于我们这里的递归比较复杂我们不用while,直接使用函数递归.
     * 4. 具体递归操作:
     * 4.1 保证left/right是非空的前提下,用左中右去替换父级索引,知道不需要替换为止.
     * 4.2 谁小谁要跟本尊替换
     */
    let element = index;
    const left = this.getLeftIndex(index); // {1}
    const right = this.getRightIndex(index); // {2}
    const size = this.size();
    if (
      left < size &&
      this.heap[element] > this.heap[left]
    ) {
      element = left;
    }
    if (
      right < size &&
      this.heap[element] > this.heap[right]
    ) {
      element = right;
    }
    /** 如果这把不需要替换,就说明没必要在在替换下去 */
    if (index !== element) {
      [this.heap[index],this.heap[element]] = [this.heap[element],this.heap[index]] // 需要替换就替换 (下移)
      this.siftDown(element); // 替换之后还需要递归向下检查
    }
  }
}

const minHeap = new MinHeap();

minHeap.insert(3)
minHeap.insert(5)
minHeap.insert(1)
minHeap.insert(6)
minHeap.extract()
console.log(minHeap.findMinimum())
minHeap.extract()
console.log(minHeap.findMinimum())

堆排序算法

思路:

  1. 创建一个最大堆做数据源
  2. 创建最大堆之后,最大值会被存储在堆的第一个位置,我们需要将它替换为堆的最后一个位置,然后将堆的大小减1.
  3. 最后,我们将堆的根节点下移并重复回到(第2个步骤),直到大小为1.
function heapSort(array) {
  if (array.length <= 1) return array;
  /** 
   * 注意:设置heapify函数获取的`左右分支的范围`. 
   * 这里取的是length所以所有的索引都必须小于,不能等于range
   */
  let effectRange = array.length;
  /**
   * 1. 创建一个最大堆做数据源(从数组中点开始向起点遍历,保证所有元素都参与).
   */
  for (let i = array.length >> 1; i >= 0; i--) {
    heapify(i, effectRange) // 完整的范围
  }
  /** 
   * 2. 创建最大堆之后,最大值会被存储在堆的第一个位置.
   *    我们需要将它替换为堆的最后一个位置,然后将堆的大小减1.
   * 3. 最后,我们将堆的根节点下移并重复回到(第2个步骤),直到大小为1.
   */
  for (let i = array.length - 1; i > 0; i--) {
    [array[0], array[i]] = [array[i], array[0]];// 把堆顶元素,即最大值放到最后.
    effectRange--;// 范围递减1
    heapify(0, effectRange) // 递减后的范围
  }
  /**
   * 注意:由于堆排序算法中,没有遇到插入的操作,所以只用到了下移操作.
   */
  function heapify(source, range) {
    /** 
     * 整体思路:
     * 比较:source vs source*2+1 vs source*2+2
     * 谁大,就替换到target的位置
     * 如果已经发生替换,我们再补递归确认.
     */
    let target = source;
    const left = source * 2 + 1;
    const right = source * 2 + 2;
    if (left < range && array[left] > array[target]) {
      target = left;
    }
    if (right < range && array[right] > array[target]) {
      target = right;
    }
    if (target !== source) {
      [array[target], array[source]] = [array[source], array[target]]
      heapify(target, range)
    }
  }

  return array;
}

console.log("heapSort", heapSort([7, 6, 3, 5, 4, 1, 2]))