二叉堆数据结构学习

105 阅读4分钟

今天我们将要学习一种特殊的二叉树,也就是堆数据结构,也叫作二叉堆。二叉堆是计算机科学中一种非常著名的数据结构,由于它能高效、快速地找出最大值和最小值,常被应用于优先队列。它也被用于著名的堆排序算法中。

二叉堆的特性

二叉堆是一种特殊的二叉树,有以下两个特性:

  1. 结构特性:它是一棵完全二叉树,表示树的每一层都有左右子节点(除了最后一层的叶节点),并且最后一层的叶节点尽可能都是左侧子节点(先左子树再右子树,左子树不完整则不能向右子树插入节点)。
  2. 堆特性:二叉堆不是最小堆就是最大堆。最小堆允许你快速导出树的最小值,最大堆允许你快速导出树的最大值。所有的节点都都大于等于(最大堆)或小于等于(最小堆)每个它的子节点。

image.png

二叉堆的实现

1. 创建最小二叉堆类

二叉树有两种表示方式。第一种是使用一个链表。第二种是使用一个数组,通过索引值检索父节点、左侧和右侧子节点的值。
今天我们采用数组来实现二叉堆。首先我们创建一个类,代码如下:

class MinHeap<T = unknown> {
  private heap: T[];
  private compare: (a: T, b: T) => boolean;

  constructor(defaultCompare?: (a: T, b: T) => boolean) {
    this.heap = [];
    this.compare = defaultCompare || MinHeap.compareFn;
  }

  /**
   * 默认的比较节点大小函数,a大于b返回true
   * @param a
   * @param b
   * @returns
   */
  private static compareFn(a: any, b: any) {
    return a > b;
  }
}

2. 添加访问特定节点的方法

因为我们是采用数组方式表示二叉堆,所以我们需要实现一些特定方法来访问其自身的父、子节点。 image.png
如上图的最小二叉堆,用数组表示为[1, 2, 3, 4, 5, 6, 7].
对于给定位置 index 的节点:

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

代码实现如下:

/**
   * 获取节点的左侧子节点的位置
   * @param idx 节点位置
   * @returns
   */
  private getLeftIdx(idx: number) {
    return idx * 2 + 1;
  }

  /**
   * 获取节点的右侧子节点的位置
   * @param idx 节点位置
   * @returns
   */
  private getRightIdx(idx: number) {
    return idx * 2 + 2;
  }

  /**
   * 根据子节点的位置得到父节点的位置
   * @param idx 子节点位置
   * @returns
   */
  private getParentNodeIdx(idx: number) {
    if (idx === 0) {
      return undefined;
    }

    return Math.floor((idx - 1) / 2);
  }

3. 向最小二叉堆中插入值

将值插入堆的底部叶节点,代码如下:

insert(value: T) {
    if (this.isEmpty()) {
      this.heap.push(value);
      return;
    }

    this.heap.push(value);
    this.siftUp(this.heap.length - 1);
}

将值插入堆后,我们还需要考虑堆是否合法。如果不合法,则需要通过siftUp方法将堆合法化,如图所示: image.png
代码实现如下:

  /**
   * 上移操作:插入节点后保证最小堆的特性(所有的节点都小于等于它的子节点)
   * @param idx 插入节点的位置
   */
  private siftUp(idx: number) {
    let tempIdx = idx;
    let parentIdx = this.getParentNodeIdx(tempIdx);

    // 如果插入的值小于其父节点的值,则交换它们的位置,直到其大于等于父节点的值
    while(parentIdx !== undefined && this.compare(this.heap[parentIdx], this.heap[tempIdx])) {
      [this.heap[tempIdx], this.heap[parentIdx]] = [this.heap[parentIdx], this.heap[tempIdx]];

      tempIdx = parentIdx;
      parentIdx = this.getParentNodeIdx(tempIdx);
    }
  }

4. 导出最小二叉堆的最小值

移除最小值表示移除数组的第一个元素(堆的根节点)。在移除后,我们将堆的最后一个元素移动至根部并执行 siftDown 函数,表示我们将交换元素直到堆的结构正常。如图所示: image.png
代码实现如下:

  /**
   * 下移操作:移除最小值后,保证二叉堆的特性
   */
  private siftDown(idx: number = 0): void {
    // 先找到左右子节点哪个更小,再交换位置,重复此操作
    let minIdx = idx;
    const leftIdx = this.getLeftIdx(idx);
    const rightIdx = this.getRightIdx(idx);

    if (leftIdx < this.heap.length && this.compare(this.heap[minIdx], this.heap[leftIdx])) {
      minIdx = leftIdx;
    }

    if (rightIdx < this.heap.length && this.compare(this.heap[minIdx], this.heap[rightIdx])) {
      minIdx = rightIdx;
    }

    if (minIdx === idx) {
      return;
    }

    [this.heap[idx], this.heap[minIdx]] = [this.heap[minIdx], this.heap[idx]];
    return this.siftDown(minIdx);
  }
  
   /**
   * 移除最小值并返回
   */
  extract() {
    if (this.heap.length === 0) {
      return undefined;
    }

    if (this.heap.length <= 2) {
      return this.heap.shift();
    }

    const backVal = this.heap.shift();
    this.heap.unshift(this.heap.pop() as T);
    this.siftDown();
    return backVal;
  }

至此我们实现了一个简单的最小二叉堆结构。

二叉堆实现完整代码

最小二叉堆完整实现代码如下:

class MinHeap<T = unknown> {
  private heap: T[];
  private compare: (a: T, b: T) => boolean;

  constructor(defaultCompare?: (a: T, b: T) => boolean) {
    this.heap = [];
    this.compare = defaultCompare || MinHeap.compareFn;
  }

  /**
   * 默认的比较节点大小函数,a大于b返回true
   * @param a 
   * @param b 
   * @returns 
   */
  private static compareFn(a: any, b: any) {
    return a > b;
  }

  /**
   * 获取节点的左侧子节点的位置
   * @param idx 节点位置
   * @returns 
   */
  private getLeftIdx(idx: number) {
    return idx * 2 + 1;
  }

  /**
   * 获取节点的右侧子节点的位置
   * @param idx 节点位置
   * @returns 
   */
  private getRightIdx(idx: number) {
    return idx * 2 + 2;
  }

  /**
   * 根据子节点的位置得到父节点的位置
   * @param idx 子节点位置
   * @returns 
   */
  private getParentNodeIdx(idx: number) {
    if (idx === 0) {
      return undefined;
    }

    return Math.floor((idx - 1) / 2);
  }

  /**
   * 上移操作:插入节点后保证最小堆的特性(所有的节点都小于等于它的子节点)
   * @param idx 插入节点的位置
   */
  private siftUp(idx: number) {
    let tempIdx = idx;
    let parentIdx = this.getParentNodeIdx(tempIdx);

    // 如果插入的值小于其父节点的值,则交换它们的位置,直到其大于等于父节点的值
    while(parentIdx !== undefined && this.compare(this.heap[parentIdx], this.heap[tempIdx])) {
      [this.heap[tempIdx], this.heap[parentIdx]] = [this.heap[parentIdx], this.heap[tempIdx]];

      tempIdx = parentIdx;
      parentIdx = this.getParentNodeIdx(tempIdx);
    }
  }

  /**
   * 下移操作:移除最小值后,保证二叉堆的特性
   */
  private siftDown(idx: number = 0): void {
    // 先找到左右子节点哪个更小,再交换位置,重复此操作
    let minIdx = idx;
    const leftIdx = this.getLeftIdx(idx);
    const rightIdx = this.getRightIdx(idx);

    if (leftIdx < this.heap.length && this.compare(this.heap[minIdx], this.heap[leftIdx])) {
      minIdx = leftIdx;
    }

    if (rightIdx < this.heap.length && this.compare(this.heap[minIdx], this.heap[rightIdx])) {
      minIdx = rightIdx;
    }

    if (minIdx === idx) {
      return;
    }

    [this.heap[idx], this.heap[minIdx]] = [this.heap[minIdx], this.heap[idx]];
    return this.siftDown(minIdx);
  }

  /**
   * 查找最小值并返回
   * @returns 
   */
  findMinimum() {
    return this.heap[0];
  }

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

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

  insert(value: T) {
    if (this.isEmpty()) {
      this.heap.push(value);
      return true;
    }

    this.heap.push(value);
    this.siftUp(this.heap.length - 1);
    return true;
  }

  /**
   * 移除最小值并返回
   */
  extract() {
    if (this.heap.length === 0) {
      return undefined;
    }

    if (this.heap.length <= 2) {
      return this.heap.shift();
    }

    const backVal = this.heap.shift();
    this.heap.unshift(this.heap.pop() as T);
    this.siftDown();
    return backVal;
  }

  toString(nodeToString: (key: T) => string = (key) => `${key}`) {
    if (this.heap.length === 0) {
      return '';
    }

    const twoArr: string[][] = [];
    this.heap.forEach((value, idx) => {
      const str = nodeToString(value);
      if (idx === 0) {
        twoArr.push([str]);
        return;
      }

      const lastIdx = twoArr.length - 1;
      if (twoArr[lastIdx].length === 2 ** lastIdx){
        twoArr.push([]);
      }

      twoArr[twoArr.length - 1].push(str);
    });

    return twoArr.map((val, idx) => {
      return `第${idx + 1}层:${val.join(' -- ')}`;
    }).join('\n');
  }
}

最大二叉堆完整实现代码见GitHub.