JavaScript数据结构与算法——二叉堆之最小堆

639 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第 12 天,点击查看活动详情

介绍

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

  • 它是一个完全二叉树,表示树的每一层都有左侧和右侧子节点(除了最后一层的节点),并且最后一层的叶节点尽可能都是左侧子节点,这叫作结构特性
  • 二叉堆不是最小堆就是最大堆。最小堆允许你快速导出树的最小值,最大堆允许你快速导出树的最大值。所有节点都大于等于(最大堆)或小于等于(最小堆)每个它的子节点。这叫作堆特性

思路

我们先创建一个树,然后在插入节点的时候,遵循以下规则

  • 每个子节点都要大于等于父节点
  • 二叉树是一个满二叉树

根据以上的规则,我们可以选择使用对象或者数组来实现,这里我们使用数组来实现,这样更直观一点。

实现

创建最小堆类

class MinHeap<T> implements IMainHeap<T> {
  public heap: T[];
  public compare: (a: T, b: T) => number;
  constructor(compare: (a: T, b: T) => number) {
    //接收比较函数
    this.compare = compare;

    //使用数组来创建二叉堆
    this.heap = [];
  }
}

我们首先创建一个MinHeap类,在类的构造函数当中,我们初始化一个数组this.heap,并且接收比较函数compare

访问特定节点


  /**
   *
   * @param index 通过当前index获取左子树的index
   */
  getLeftIndex(index: number): number {
    return 2 * index + 1;
  }

  /**
   *
   * @param index 通过当前index获取右子树的index
   */
  getRightIndex(index: number): number {
    return 2 * index + 2;
  }

  /**
   *
   * @param index 通过当前index获取父节点的index
   */
  getParentIndex(index: number): number {
    if (index === 0) {
      return undefined;
    }

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

向堆当中插入值

  /**
   *
   * @param value 插入一个元素到最小二叉堆
   */
  insert(value: T): boolean {
    // 插入值
    if (value === null) {
      return false;
    }
    //将当前值插入到树的最后一位
    this.heap.push(value);
    this.siftUp(this.heap.length - 1);
    return true;
  }

    /**
   *
   * @param length 获取数组的尾元素
   */
  siftUp(index: number) {
    let parent = this.getParentIndex(index);
    //遍历堆,将插入值和其父节点做对比没如果插入值小于其父节点的值,那么就互换位置
    while (
      index > 0 &&
      this.compare(this.heap[parent], this.heap[index]) === compareConfig.SMALL
    ) {
      //交换这两个元素
      this.swap(this.heap, parent, index);

      //将元素的下标交换
      index = parent;
      parent = this.getParentIndex(index);
    }
  }

  /**
   *
   * @param array 需要交换元素的数组
   * @param indexA 需要交换的元素下标
   * @param indexB 需要交换的元素下标
   */
  swap(array: T[], indexA: number, indexB: number) {
    let temp: T = array[indexA];

    array[indexA] = array[indexB];
    array[indexB] = temp;
  }

向堆当中插入值,首先判断插入的值是否为空,如果为空直接返回fasle。我们先将值插入到对当中的最后this.heap.push(value);,然后调用siftUp方法对堆进行自下而上的从新排列。siftUp方法当种,接收一个index参数,是当前堆的最后一个元素的下标,将堆最后一个元素的值和其父元素比较,如果堆最后一个元素小于父元素,那么就交换位置。然后将index赋值为parent

获取堆当中的最小值

  /**
   *
   * @returns 返回最小值,但是不会删除它
   */
  findMinMum(): T {
    return this.isEmpty() ? undefined : this.heap[0];
  }

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

  isEmpty(): boolean {
    if (this.size() === 0) {
      return true;
    }
    return false;
  }

导出对当中的最小值

   /**
   *
   * @returns 移除最小值,并且将他返回
   */
  extract(): T {
    if (this.isEmpty()) {
      return undefined;
    }

    if (this.size() === 1) {
      return this.heap[0];
    }

    //把堆当中第一个元素取出,最小对当中,这个元素是最小值,最大堆当中,这个值是最大值
    const removeValue = this.heap.shift();

    this.heap.unshift(this.heap.pop());
    this.siftDown(0);
    return removeValue;
  }

  /**
   *
   * @param index 获取更新节点
   */
  siftDown(index: number) {
    //将index保存到index
    let element = index;

    //获取左节点下标
    const left = this.getLeftIndex(element);
    // 获取右节点下标
    const right = this.getRightIndex(element);
    // 获取堆长度
    const size = this.size();

    //判断:如果当前值小于左节点的值
    if (
      left < size &&
      this.compare(this.heap[left], this.heap[element]) === compareConfig.SMALL
    ) {
      element = left;
    }

    if (
      right < size &&
      this.compare(this.heap[right], this.heap[element]) === compareConfig.SMALL
    ) {
      element = right;
    }

    //如果index不等于element,说明element改变了
    if (index !== element) {
      this.swap(this.heap, element, index);
      this.siftDown(element);
    }
  }

先将首元素弹出堆,这样最小值已经被弹出堆了,然后我们要对最小堆进行重新排列,找出最小值放到根节点上(也就是数组下标为 0 的位置),我们首先将堆的最后一个元素放到根节点的位置上,然后调用siftDown方法对其进行自上而下的重新排序。 siftDown方法接收一个参数index,表示从下标为index的位置开始重新排序,我们先将index赋值给element。然后我们虎丘当前index节点的左右节点leftright,还有获取堆长度size。我们首先判断element下标的节点值和它左节点的值比较,如果element节点值大于左节点值,那么就将左左节点的下标值赋值给element,然后再判端右节点的值和element的节点值进行比较,如果右节点的值小于element节点值,那么就将right赋值给element。最后,如果element不等于index,说明中间有节点需要进行交换,所以调用swap方法来交换,然后递归调用this.siftDown(element);

完整代码

interface ICompareConfig {
  BIG: number;
  SMALL: number;
  EQUAL: number;
}

const compareConfig: ICompareConfig = {
  BIG: 1,
  SMALL: -1,
  EQUAL: 0,
};

/**
 * 最小二叉堆
 */
interface IMainHeap<T> {
  getLeftIndex(index: number): number;
  getRightIndex(index: number): number;
  getParentIndex(index: number): number;

  insert(value: T): boolean;
  extract(): T;
  findMinMum(): T;
  siftUp(length: number);
  swap(array: T[], indexA: number, indexB: number);
}

class MinHeap<T> implements IMainHeap<T> {
  public heap: T[];
  public compare: (a: T, b: T) => number;
  constructor(compare: (a: T, b: T) => number) {
    //接收比较函数
    this.compare = compare;

    //使用数组来创建二叉堆
    this.heap = [];
  }

  /**
   *
   * @param length 获取数组的尾元素
   */
  siftUp(index: number) {
    let parent = this.getParentIndex(index);
    //遍历堆,将插入值和其父节点做对比没如果插入值小于其父节点的值,那么就互换位置
    while (
      index > 0 &&
      this.compare(this.heap[parent], this.heap[index]) === compareConfig.SMALL
    ) {
      //交换这两个元素
      this.swap(this.heap, parent, index);

      //将元素的下标交换
      index = parent;
      parent = this.getParentIndex(index);
    }
  }

  /**
   *
   * @param array 需要交换元素的数组
   * @param indexA 需要交换的元素下标
   * @param indexB 需要交换的元素下标
   */
  swap(array: T[], indexA: number, indexB: number) {
    let temp: T = array[indexA];

    array[indexA] = array[indexB];
    array[indexB] = temp;
  }

  /**
   *
   * @param index 通过当前index获取左子树的index
   */
  getLeftIndex(index: number): number {
    return 2 * index + 1;
  }

  /**
   *
   * @param index 通过当前index获取右子树的index
   */
  getRightIndex(index: number): number {
    return 2 * index + 2;
  }

  /**
   *
   * @param index 通过当前index获取父节点的index
   */
  getParentIndex(index: number): number {
    if (index === 0) {
      return undefined;
    }

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

  /**
   *
   * @param value 插入一个元素到最小二叉堆
   */
  insert(value: T): boolean {
    // 插入值
    if (value === null) {
      return false;
    }
    //将当前值插入到树的最后一位
    this.heap.push(value);
    this.siftUp(this.heap.length - 1);
    return true;
  }

  /**
   *
   * @returns 移除最小值,并且将他返回
   */
  extract(): T {
    if (this.isEmpty()) {
      return undefined;
    }

    if (this.size() === 1) {
      return this.heap[0];
    }

    //把堆当中第一个元素取出,最小对当中,这个元素是最小值,最大堆当中,这个值是最大值
    const removeValue = this.heap.shift();

    this.heap.unshift(this.heap.pop());
    this.siftDown(0);
    return removeValue;
  }

  /**
   *
   * @returns 返回最小值,但是不会删除它
   */
  findMinMum(): T {
    return this.isEmpty() ? undefined : this.heap[0];
  }

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

  isEmpty(): boolean {
    if (this.size() === 0) {
      return true;
    }
    return false;
  }

  /**
   *
   * @param index 获取更新节点
   */
  siftDown(index: number) {
    //将index保存到index
    let element = index;

    //获取左节点下标
    const left = this.getLeftIndex(element);
    // 获取右节点下标
    const right = this.getRightIndex(element);
    // 获取堆长度
    const size = this.size();

    //判断:如果当前值小于左节点的值
    if (
      left < size &&
      this.compare(this.heap[left], this.heap[element]) === compareConfig.SMALL
    ) {
      element = left;
    }

    if (
      right < size &&
      this.compare(this.heap[right], this.heap[element]) === compareConfig.SMALL
    ) {
      element = right;
    }

    //如果index不等于element,说明element改变了
    if (index !== element) {
      this.swap(this.heap, element, index);
      this.siftDown(element);
    }
  }
}

function compare<T>(indexKey: T, insertKey: T) {
  if (indexKey > insertKey) {
    return -1;
  } else if (indexKey < insertKey) {
    return 1;
  } else {
    return 0;
  }
}