老白的算法学习笔记02

401 阅读4分钟

「这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战」 今天我们来一起深入了解和学习 堆排序。

本篇文章中的代码在这里.

堆排序(Heapsort)

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆是一个完全二叉树的结构,并同时满足堆的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。

想要理解堆排序,我们先理解一下二叉树的概念

二叉树(binary tree)

二叉树是指树中节点的度不大于2的有序树,它是一种最简单且最重要的树。

二叉树中的相关术语

  1. 结点:包含一个数据元素及若干指向子树分支的信息
  2. 结点的度:一个结点拥有子树的数目称为结点的度
  3. 叶子结点:也称为终端结点,没有子树的节点或者度为0的结点
  4. 分支结点:也称为非终端结点,度不为0的结点。
  5. 树的度:树种所有结点的度的最大值。
  6. 结点的层次:从根结点开始,假设根结点为第1层,根结点的子节点为第2层,依此类推,如果某一个结点位于第L层,则其子节点位于第L+1层
  7. 树的深度:也称为树的高度,树中所有结点的层次最大值称为树的深度
  8. 有序树:如果树中各棵子树的次序是有先后次序,则称该树为有序树
  9. 无序树:如果树中各棵子树的次序没有先后次序,则称该树为无序树
  10. 由m(m≥0)棵互不相交的树构成一片森林。如果把一棵非空的树的根结点删除,则该树就变成了一片森林,森林中的树由原来根结点的各棵子树构成

基本性质

  • 每个节点的度最多为2
  • 度为0的节点比度为2的节点多一个 证明:设度为0的结点为n0,度为1的结点为n1,度为2的结点为n2。那么总结点数为n0+n1+n2,而总边数为0·n0+ 1·n1+ 2·n2。而我们知道总边数等于总结点数减去1,那么有n0+n1+n2−1 = 0·n0+ 1·n1+ 2·n2,即n0−1 =n2。
  • 深度为h的二叉树中至多含有2h-1个节点

特殊的二叉树

完全二叉树(complete binary tree)

一棵深度为k的有n个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进行编号,如果编号为i(1≤i≤n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树。

  • 父子节点的编号存在可计算的关系 因此不需要存储边的信息
  • 可以用连续空间存e
满二叉树(full binary tree)

只有度为0和2的二叉树

完美二叉树(perfect binary tree)

每次层都满了,对称且完美。

注意: 几种二叉树的定义在不同的资料说明中可能存在一定差异,因此在实际场合中提到时请务必进行确认。

堆(Heap)

堆是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。堆总是满足下列性质

  • 堆中某个结点的值总是不大于或不小于其父结点的值
  • 堆总是一棵完全二叉树

大顶堆

任意的三元组,父节点都大于两个子节点。根节点为最大值。

小顶堆

任意的三元组,父节点都小于两个子节点。根节点为最小值。

堆的基本操作(以大顶堆为例)

  • 尾部插入
    • 比父节点大就和父节点交换 递归向上调整
    • 这个过程成为SIFT-UP
  • 头部弹出
    • 用最后一个元素(叶子结点)补位 递归向下调整
    • 这个过程成为SIFT-DOWN

堆排序的口诀

  1. 将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
  2. 将堆定元素与堆尾元素交换
  3. 将此操作看作是堆顶元素弹出操作
  4. 按照头部弹出以后的策略调整堆,重复2~4,直到堆的元素为1.

图解

heapSort.gif

代码实现

class Heap {
  constructor(data) {
    this.data = data
    this.compartor = (a, b) => a - b
    this.heapify()
  }

  size() {
    return this.data.length
  }

  heapify() {
    if (this.size() < 2) {
      return
    }

    for (let i = 1; i < this.size(); i++) {
      this.bubbleUp(i)
    }
  }

  peek() {
    if (!this.size()) return null
    return this.data[0]
  }

  offer(val) {
    this.data.push(val)
    this.bubbleUp(this.size() - 1)
  }

  poll() {
    if (!this.size()) return null
    if (this.size() === 1) return this.data.pop()

    let res = this.data[0]

    this.data[0] = this.data.pop()

    if (this.size()) {
      this.bubbleDown(0)
    }
    return res
  }

  swap(i, j) {
    if (i === j) {
      return
    }
    ;[this.data[i], this.data[j]] = [this.data[j], this.data[i]]
  }

  bubbleUp(index) {
    // 向上调整,我们最⾼就要调整到0 号位置

    while (index) {
      // 获取到当前节点的⽗节点,
      const parenIndex = (index - 1) >> 1

      // const parenIndex = Math.floor((index - 1) / 2);
      // const parenIndex = (index - 1) / 2 | 0;
      // ⽐较⽗节点的值和我们当前的值哪个⼩。
      if (this.compartor(this.data[index], this.data[parenIndex]) < 0) {
        //if 交换⽗节点和⼦节点
        this.swap(index, parenIndex)
        // index 向上⾛⼀步,进⾏下⼀次交换

        index = parenIndex
      } else {
        // 防⽌死循环。
        break
      }
    }
  }
  bubbleDown(index) {
    // 我们要获取到最⼤的下标,保证不会交换出界。

    let lastIndex = this.size() - 1
    while (index < lastIndex) {
      // 获取左右⼉⼦的下标
      let leftIndex = index * 2 + 1
      let rightIndex = index * 2 + 2
      // 待交换节点
      let findIndex = index
      if (
        leftIndex <= lastIndex &&
        this.compartor(this.data[leftIndex], this.data[findIndex]) < 0
      ) {
        findIndex = leftIndex
      }

      if (
        rightIndex <= lastIndex &&
        this.compartor(this.data[rightIndex], this.data[findIndex]) < 0
      ) {
        findIndex = rightIndex
      }
      if (index !== findIndex) {
        this.swap(index, findIndex)

        index = findIndex
      } else {
        break
      }
    }
  }
}


const heapArr = getRandomArr();
console.log('before heapArr ===>', heapArr);
const len = heapArr.length;
const minHeap = new Heap(heapArr)
const resHeapArr = []
for (let i = 0; i < len; i++) {
	resHeapArr.push(minHeap.poll())
}
console.log('after heapArr ===>', resHeapArr)