「这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战」 今天我们来一起深入了解和学习 堆排序。
本篇文章中的代码在这里.
堆排序(Heapsort)
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆是一个完全二叉树的结构,并同时满足堆的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。
想要理解堆排序,我们先理解一下二叉树的概念
二叉树(binary tree)
二叉树是指树中节点的度不大于2的有序树,它是一种最简单且最重要的树。
二叉树中的相关术语
- 结点:包含一个数据元素及若干指向子树分支的信息
- 结点的度:一个结点拥有子树的数目称为结点的度
- 叶子结点:也称为终端结点,没有子树的节点或者度为0的结点
- 分支结点:也称为非终端结点,度不为0的结点。
- 树的度:树种所有结点的度的最大值。
- 结点的层次:从根结点开始,假设根结点为第1层,根结点的子节点为第2层,依此类推,如果某一个结点位于第L层,则其子节点位于第L+1层
- 树的深度:也称为树的高度,树中所有结点的层次最大值称为树的深度
- 有序树:如果树中各棵子树的次序是有先后次序,则称该树为有序树
- 无序树:如果树中各棵子树的次序没有先后次序,则称该树为无序树
- 由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
堆排序的口诀
- 将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
- 将堆定元素与堆尾元素交换
- 将此操作看作是堆顶元素弹出操作
- 按照头部弹出以后的策略调整堆,重复2~4,直到堆的元素为1.
图解
代码实现
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)