前端必会数据结构与算法系列之二叉堆(九)

427 阅读3分钟

1. 什么是二叉堆

二叉堆是计算机科学中一种非常著名的数据结构,由于它能高效、快速地找出最大值和最小值

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

  • 它是一棵完全二叉树,表示树的每一层都有左侧和右侧子节点(除了最后一层的叶节点), 并且最后一层的叶节点尽可能都是左侧子节点,这叫作结构特性。

  • 二叉堆不是最小堆就是最大堆。最小堆允许你快速导出树的最小值,最大堆允许你快速导出树的最大值。所有的节点都大于等于(最大堆)或小于等于(最小堆)每个它的子节点。这叫作堆特性

二叉树有两种表示方式。第一种是使用一个动态的表示方式,也就是指针(用节点表示),。第二种是使用一个数组,通过索引值检索父节点、左侧和右侧子节点的值。下图展示了两种不同的表示方式

image.png

2. 创建二叉堆

要访问使用普通数组的二叉树节点,我们可以用下面的方式操作 index。 对于给定位置 index 的节点:

  • 它的左侧子节点的位置是 2 * index + 1(如果位置可用);
  • 它的右侧子节点的位置是 2 * index + 2(如果位置可用);
  • 它的父节点位置是 index / 2(如果位置可用)
class MinHeap {
  constructor(compareFn = defaultCompare) {
    this.compareFn = compareFn;
    this.heap = [];
  }
  getLeftIndex(index) { // 左节点
    return (2 * index) + 1;
  }
  getRightIndex(index) { // 右节点
    return (2 * index) + 2;
  }
  getParentIndex(index) { // 父节点
    if (index === 0) {
      return undefined;
    }
    return Math.floor((index - 1) / 2);
  }
  size() {
    return this.heap.length;
  }
  isEmpty() {
    return this.size() <= 0;
  }
  clear() {
    this.heap = [];
  }
  findMinimum() { // 返回最小值(最小堆)或最大值
    return this.isEmpty() ? undefined : this.heap[0];
  }
  insert(value) { // 向堆中插入一个新的值
    if (value != null) {
      const index = this.heap.length;
      this.heap.push(value);
      this.siftUp(index);
      return true;
    }
    return false;
  }
  
  siftUp(index) { // 上移操作
    let parent = this.getParentIndex(index);
    while (
      index > 0 &&
      this.compareFn(this.heap[parent], this.heap[index]) === Compare.BIGGER_THAN
    ) {
      swap(this.heap, parent, index);
      index = parent;
      parent = this.getParentIndex(index);
    }
  }
  
  siftDown(index) { // 下移操作
    // 将 index 复制到 element 变量中
    let element = index;
    // 获取左侧子节点和右侧子节点的值
    const left = this.getLeftIndex(index);
    const right = this.getRightIndex(index);
    const size = this.size();
    // 将元素和最小子节点(最小堆)和最大子节点(最大堆)进行交换
    if (
      left < size &&
      this.compareFn(this.heap[element], this.heap[left]) === Compare.BIGGER_THAN
    ) {
      // 如果元素比左侧子节点要小,我们就交换元素和它的左侧子节点
      element = left;
    }
    if (
      right < size &&
      this.compareFn(this.heap[element], this.heap[right]) === Compare.BIGGER_THAN
    ) {
      // 如果元素小于它的右侧子节点,我们就交换元素和它的右侧子节点
      element = right;
    }
    if (index !== element) { // 检验它的值是否和 element 相同
      // 如果不是,就将它和最小的 element 交换
      swap(this.heap, index, element);
      this.siftDown(element);
    }
  }
  
  extract() { // 移除最小值(最小堆)或最大值
    if (this.isEmpty()) {
      return undefined;
    }
    if (this.size() === 1) {
      return this.heap.shift();
    }
    const removedValue = this.heap[0];
    this.heap[0] = this.heap.pop();
    this.siftDown(0);
    return removedValue;
  }
  heapify(array) {
    if (array) {
      this.heap = array;
    }
    const maxIndex = Math.floor(this.size() / 2) - 1;
    for (let i = 0; i <= maxIndex; i++) {
      this.siftDown(i);
    }
    return this.heap;
  }
  getAsArray() {
    return this.heap;
  }
}
export class MaxHeap extends MinHeap {
  constructor(compareFn = defaultCompare) {
    super(compareFn);
    this.compareFn = compareFn;
    this.compareFn = reverseCompare(compareFn);
  }
}

插入:

image.png

向堆中插入一个值 1。算法会进行一些少量的上移操作

image.png

下移操作

image.png

MaxHeap 类的算法和 MinHeap 类的算法一模一样。不同之处在于我们要把所有>(大于)的比较换成<(小于)的比较

export class MaxHeap extends MinHeap { 
 constructor(compareFn = defaultCompare) { 
 super(compareFn); 
 this.compareFn = reverseCompare(compareFn); // {1} 
 } 
}

可以扩展 MinHeap 类来继承我们在本章创建的所有代码,并在需要时进行反向的比较。要将比较反转,不将 a 和 b 进行比较,而是将 b 和 a 进行比较

function reverseCompare(compareFn) { 
 return (a, b) => compareFn(b, a); 
}

简版

class MinHeap {
    constructor() {
        this.heap = [];
    }
    // 交换节点
    swap(i1, i2) {
        const temp = this.heap[i1];
        this.heap[i1] = this.heap[i2];
        this.heap[i2] = temp;
    }
    // 获取父节点
    getParentIndex(i) {
        // return Math.floor((i - 1) / 2);
        return (i - 1) >> 1; // 2进制操作,取商
    }
    getLeftIndex(i) {
        return i * 2 + 1;
    }
    getRightIndex(i) {
        return i * 2 + 2;
    }
    // 上移
    shiftUp(index) {
        if (index == 0) { return; }
        const parentIndex = this.getParentIndex(index);
        if (this.heap[parentIndex] > this.heap[index]) { // 父节点大于当前节点
            this.swap(parentIndex, index);
            this.shiftUp(parentIndex);
        }
    }
    // 下移操作
    shiftDown(index) {
        const leftIndex = this.getLeftIndex(index);
        const rightIndex = this.getRightIndex(index);
        if (this.heap[leftIndex] < this.heap[index]) {
            this.swap(leftIndex, index);
            this.shiftDown(leftIndex);
        }
        if (this.heap[rightIndex] < this.heap[index]) {
            this.swap(rightIndex, index);
            this.shiftDown(rightIndex);
        }
    }
    // 将值插入堆的底部,即数组的尾部。
    // 然后_上移:将这个值和它的父节点进行交换,直到父节点小于等于这个插入的值
    // 大小为k的堆中插入元素的时间复杂度为O(logK)
    insert(value) {
        this.heap.push(value);
        this.shiftUp(this.heap.length - 1);
    }
    // 删除堆顶
    // 用数组尾部元素替换堆顶(直接删除堆顶会破坏堆结构)。
    // 然后下移:将新堆顶和它的子节点进行交换,直到子节点大于等于这个新堆顶。
    // 大小为k的堆中删除堆顶的时间复杂度为O(logK)。
    pop() {
        this.heap[0] = this.heap.pop();
        this.shiftDown(0);
    }
    peek() {
        return this.heap[0];
    }
    size() {
        return this.heap.length;
    }
}

const h = new MinHeap();
h.insert(3);
h.insert(2);
h.insert(1);
h.pop();

3. 堆排序算法

我们可以使用二叉堆数据结构来帮助我们创建一个非常著名的排序算法:堆排序算法。它包含下面三个步骤。

(1) 用数组创建一个最大堆用作源数据。
(2) 在创建最大堆后,最大的值会被存储在堆的第一个位置。我们要将它替换为堆的最后一个值,将堆的大小减 1。
(3) 最后,我们将堆的根节点下移并重复步骤 2 直到堆的大小为 1

我们用最大堆得到一个升序排列的数组(从最小到最大)。如果我们想要这个数组按降序排列,可以用最小堆代替

堆排序算法

function heapSort(array, compareFn = defaultCompare) { 
 let heapSize = array.length; 
 buildMaxHeap(array, compareFn); // 步骤 1 
 while (heapSize > 1) { 
 swap(array, 0, --heapSize); // 步骤 2 
 heapify(array, 0, heapSize, compareFn); // 步骤 3 
 } 
 return array; 
}

构建最大堆

function buildMaxHeap(array, compareFn) { 
 for (let i = Math.floor(array.length / 2); i >= 0; i -= 1) { 
   heapify(array, i, array.length, compareFn); 
 } 
 return array; 
}

最大堆函数会重新组织数组的顺序。归功于要进行的所有比较,我们只需要对后半部分数组执行 heapify(下移)函数(前半部分会被自动排好序,所以不需要对已经知道排好序的部分执行函数)。

heapify 函数和我们创建的 siftDown 方法有相同的代码。不同之处是我们会将堆本身、堆的大小和要使用的比较函数传入作为参数。这是因为我们不会直接使用堆数据结构,而是使用它的逻辑来开发 heapSort 算法

image.png

4. 堆的应用

◆ 堆能高效、快速地找出最大值和最小值,时间复杂度:O(1)
◆ 找出第 K 个最大(小)元素

第K个最大元素

◆ 构建一个最小堆,并将元素一次插入堆中
◆ 当堆的容量超过K,就删除堆顶。
◆ 插入结束后,堆顶就是第K个最大元素

5. leetcode常见考题

1.数组中的第K个最大元素

难度:中等

题解:数组中第K个最大元素(最小堆)

2.前 K 个高频元素

难度:中等

题解:前 K 个高频元素(最小堆)

3.合并K个升序链表

难度:困难

题解:合并K个升序链表(最小堆)