理解堆

  • 堆是一个完全二叉树;
  • 堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值;

完全二叉树:除了最后一层,其他层的节点个数都是满的,最后一层的节点都靠左排列。

几个例子:

堆1:大顶堆
         10
        /  \
       9    8
      /\    /\
     6  5  7  4
    /
   3 

堆2:大顶堆
         10
        /  \
       8    9
      /\    /\
     7  3  5  6
    /
   4 

堆3:小顶堆
         3
        /  \
       4    6
      /\    /\
     5  8  9  10
    /
   7 

堆的存储及操作

1、堆的存储

完全二叉树适合用数组来存储。

        7
      /   \
     5      6
    / \     / 
   4   2   1 

用数组来存储: 
arr = [, 7, 5, 6, 4, 2, 1]
idx = 0  1  2  3  4  5  6

- 数组下标为0的位置置空;
- 数组中下标为i的节点的左子节点,就是下标为i* 2的节点,右子节点就是下标为i*2+1的节点,父节点就是下标为i/2的节点。

2、往堆中插入元素

往堆中插入一个元素后,需要继续满足堆的两个特性。

3、从堆中删除元素

实现大顶堆

class MaxHeap {
  constructor() {
    // 使用数组来存储,索引为0的位置默认置为0,主要方便获取左右子节点
    // 当下标 i 为父节点,下标 i * 2 为左子节点,下标 i * 2 + 1为右子节点
    this.storage = [0];
  }

  // 传入一个数组构建堆
  build(arr) {
    if (!Array.isArray(arr)) {
      return false;
    }

    // 初始化
    this.storage = [0];

    //插入元素
    for (let i = 0; i < arr.length; i++) {
      this.insert(arr[i]);
    }

    return true;
  }

  insert(nValue) {
    //添加节点值
    this.storage.push(nValue);

    //当前节点下标
    let nIndex = this.storage.length - 1;
    //当前节点的父节点下标
    let nFatherIndex = Math.floor(nIndex / 2);

    // 从下往上完成堆化
    while (nFatherIndex > 0) {
      // 当前节点值大于其父节点,则交换(构建大顶堆)
      if (this.storage[nIndex] > this.storage[nFatherIndex]) {
        var temp = this.storage[nIndex];
        this.storage[nIndex] = this.storage[nFatherIndex];
        this.storage[nFatherIndex] = temp;
      }
      // 更新当前节点下标
      nIndex = nFatherIndex;
      // 更新父节点下标
      nFatherIndex = Math.floor(nIndex / 2);
    }
  }

  //删除堆顶元素
  delete() {
    const heapTop = this.storage[1];
    //用最后一个元素替换堆顶元素
    this.storage[1] = this.storage.pop();

    //堆化(自上而下完成堆化)
    this._heapify();

    //返回删除的元素
    return heapTop;
  }

  _heapify() {
    const arr = this.storage,
      len = arr.length;
    let i = 1; // 大顶堆的第一个节点的下标(父节点)
    while (true) {
      //假设最大位置为 第一个节点的下标
      let maxPos = i;
      // 如果拥有叶左节点,并且叶左节点较大,则更新maxPos为左节点下标
      if (i * 2 <= len && arr[i] < arr[i * 2]) {
        maxPos = i * 2;
      }
      // 如果拥有叶右节点,并且叶右节点较大,则更新maxPos为右节点下标
      if (i * 2 + 1 <= len && arr[maxPos] < arr[i * 2 + 1]) {
        maxPos = i * 2 + 1;
      }
      // 循环直到i节点为最大值
      if (maxPos === i) {
        break;
      } 
      // 交换位置, 父节点为父/左/右中最大的一个(i下标处存最大值的节点,大顶堆嘛)
      const temp = arr[i];
      arr[i] = arr[maxPos];
      arr[maxPos] = temp;

      // i为左/右节点,并尝试向下查找更大的值(更新i的值)
      i = maxPos; 
    }
  }
}

测试代码:

var heap = new MaxHeap();
console.log(heap.storage); //[0]

heap.insert(10);
console.log(heap.storage); //[0,10]
10

heap.insert(9);
console.log(heap.storage); //[0,10,9]
   10
  /
 9

heap.insert(11);
console.log(heap.storage); //[0,11,9,10]
  11
  /\ 
 9  10

heap.insert(8);
console.log(heap.storage); //[0,11,9,10,8]
  11
  /\ 
 9  10
 /
8

heap.insert(22);
console.log(heap.storage); //[0,22,11,10,8,9]
  22
  /\ 
 11  10
 / \
8  9

heap.delete(); //22
console.log(heap.storage); // [0, 11, 9, 10, 8]
  11
  /\ 
 9  10
 /
8

heap.delete(); //11
console.log(heap.storage); // [0, 10, 9, 8]
   10
  /  \
 9    8

小顶堆

class MinHeap {
  constructor() {
    // 使用数组来存储,索引为0的位置默认置为0,主要方便获取左右子节点
    // 当下标 i 为父节点,下标 i * 2 为左子节点,下标 i * 2 + 1为右子节点
    this.storage = [0];
  }

  // 传入一个数组构建堆
  build(arr) {
    if (!Array.isArray(arr)) {
      return false;
    }

    // 初始化
    this.storage = [0];

    //入堆
    for (let i = 0; i < arr.length; i++) {
      this.insert(arr[i]);
    }

    return true;
  }

  insert(nValue) {
    //添加节点值
    this.storage.push(nValue);

    //当前节点下标
    let nIndex = this.storage.length - 1;
    //当前节点的父节点下标
    let nFatherIndex = Math.floor(nIndex / 2);

    // 从下往上完成堆化
    while (nFatherIndex > 0) {
      // 当前节点值小于其父节点,则交换(构建小顶堆)
      if (this.storage[nIndex] < this.storage[nFatherIndex]) {
        var temp = this.storage[nIndex];
        this.storage[nIndex] = this.storage[nFatherIndex];
        this.storage[nFatherIndex] = temp;
      }
      // 更新当前节点下标
      nIndex = nFatherIndex;
      // 更新父节点下标
      nFatherIndex = Math.floor(nIndex / 2);
    }
  }

  //删除堆顶元素
  delete() {
    const heapTop = this.storage[1];
    //用最后一个元素替换堆顶元素
    this.storage[1] = this.storage.pop();

    //堆化(自上而下完成堆化)
    this._heapify();

    //返回删除的元素
    return heapTop;
  }

  _heapify() {
    const arr = this.storage,
      len = arr.length;
    let i = 1; //小顶堆的第一个节点的下标(父节点)
    while (true) {
      let minPos = i;
      // 如果拥有叶左节点,并且叶左节点小于较小,则更新minPos为左节点下标
      if (i * 2 <= len && arr[i] > arr[i * 2]) {
        minPos = i * 2;
      }
      // 如果拥有叶右节点,并且叶右节点小于较小,则更新minPos为右节点下标
      if (i * 2 + 1 <= len && arr[minPos] > arr[i * 2 + 1]) {
        minPos = i * 2 + 1;
      }
      //如果已经是最小了,则直接退出
      if (minPos === i) {
        break;
      }
      // 交换节点,下标i处存放最小值节点(父/左/右中最小的,小顶堆嘛)
      const temp = arr[i];
      arr[i] = arr[minPos];
      arr[minPos] = temp;
      
      // 更新i的值,向下继续寻找最小的
      i = minPos;
    }
  }
}

测试代码:

var heap = new MinHeap();
console.log(heap.storage); //[0]

heap.insert(10);
console.log(heap.storage); //[0, 10]
10

heap.insert(9);
console.log(heap.storage); //[0, 9, 10]
  9
 /
10

heap.insert(11);
console.log(heap.storage); //[0, 9, 10, 11]
  9
 / \
10  11

heap.insert(8);
console.log(heap.storage); //[0, 8, 9, 10, 11]
    8
   / \
  9   10
 /
11

heap.insert(22);
console.log(heap.storage); //[0, 8, 9, 11, 10, 22]
    8
   / \
  9   11
 / \
10  22

heap.delete(); //8
console.log(heap.storage); //[0, 9, 10, 11, 22]
   9
  /  \
 10  11
 /
22

heap.delete(); //9
console.log(heap.storage); //[0, 10, 22, 11]
  10
 /  \
22  11 

heap.delete(); //10
console.log(heap.storage); //[0, 11, 22]
  11
 / 
22