「这是我参与2022首次更文挑战的第12天,活动详情查看:2022首次更文挑战」
堆排序
介绍
堆排序 这是十大排序算法里 较难的其中一种排序,牵扯的知识点很多。它是选择排序中的一种,它的排序算法思想,是根据 数据结构中的 堆 来设计的。
那想了解什么是 数据结构中的 堆 是什么,可以看下之前写的文章 数据结构-堆 及 相关代码 数据结构-堆-代码讲解 。
下面是 堆排序 动态图的一个演示
步骤点
由上面的演示图,我这里将 堆排序 分为以下 10个 步骤点
-
插入元素
-
产生堆
-
交换值: 根节点 与 最后一个叶子节点
-
删除: 最新数组 的最后一个元素
-
将删除的元素,unshift 到一个新数组中
-
从现在数组的 根节点开始
-
比较:根节点 与 左右子孩子 进行比较
-
交换
-
产生新的堆
-
循环结束
讲解
随机 数组的值为 [7,15,1,20,2,9,3,6],图如下
1. 插入元素
首先,堆排序,肯定是我们先给定一个随机的数组,然后让其生成为一个 堆 结构。
插入元素,我们需要将这个随机数组中的元素,依次进行遍历,将元素进行插入操作
const arr = [7, 15, 1, 20, 2, 9, 3, 6]
arr.forEach((item) => {
insert(item)
})
2. 产生堆
插入后的元素,从下往上,循环找到父元素,进行 当前元素 与 父元素 的比对后,找到其中最大值,交换,并重新更新赋值 新元素的下标。来产生 堆 结构。
通过 insert 这个方法,我们将会得到的新数组为 [20,15,9,7,2,1,3,6],是一个 堆 结构。
// 插入新元素
const insert = (value) => {
dataArray.push(value)
// 获取 当前元素 的 下标
let currentIndex = dataArray.length - 1
// 获取 当前元素 的 父元素 下标
let currentFatherIndex = getFatherIndex(currentIndex)
// 判断 元素的父元素的下标是否存在
while (currentFatherIndex >= 0) {
// 如果 当前元素 的值 大于 当前元素的父元素 的值,进行交换操作
if (compareFunc(currentIndex, currentFatherIndex)) {
exchangeFunc(currentIndex, currentFatherIndex)
}
// 更新 当前元素下标 和 父元素下标
currentIndex = currentFatherIndex
currentFatherIndex = getFatherIndex(currentIndex)
}
}
3. 交换值
从上面两步骤,得到数组结果为 [20,15,9,7,2,1,3,6]。
接下来 将此时的 根节点(数组中的第一个值,下标 0) 与 最后一个叶子节点(数组中的最后一个值,下标 dataArrayLength - 1) 进行交换值。
交换后的数组为:[6,15,9,7,2,1,3,20]
const exchangeFunc = (A, B) => {
const temp = dataArray[B]
dataArray[B] = dataArray[A]
dataArray[A] = temp
}
4. 删除
删除 现在这个数组中的 最后一个元素(也就是刚开始的根节点)
删除后的数组为:[6,15,9,7,2,1,3]
dataArray.pop()
5. unshift 到 排序数组中
将 pop 出来的元素,unshift 到 我们要的排序数组中 [20]
dataSortArray.unshift(dataArray.pop())
这里注意,这里我默认是 从小到大 进行排序,如果想要进行 从大到小 排序,只需将代码中的 unshift 变成 push 即可。
6. 从现在数组的 根节点开始
这里从 数组的 根节点开始,从上往下,找到 左右两个子孩子。
这里从根节点开始查找的原因,是因为上面的步骤所导致的。交换后,又把最大值 pop 出去了。那么现在剩下的数组肯定不是 堆 结构,我们把对其产生影响的元素(根元素),重新的进行 比较,让它回到自己应该在的位置后,得到新的 堆 结构。
7. 比较:根节点 与 左右子孩子 进行比较
我们知道当前根元素的下标,那么自然通过下面两个公式,来获得左右孩子的下标,
- 左孩子:
2*i+1 - 右孩子:
2*i+2
目前这三个下标 所对应的值 进行比对,找到最大值下标。
这里注意,需要查看左右孩子是否存在,如果不存在的话,那么我们就不需要进行比对了。
// 获取 当前元素 的 左边孩子下标
const getLeftChildIndex = (index) => {
return 2 * index + 1
}
// 获取 当前元素 的 右边孩子下标
const getRightChildIndex = (index) => {
return 2 * index + 2
}
// 定义左右孩子最大值的下标
let maxIndex = -1
// 查看左孩子是否存在
if (existenceFunc(currentLeftChildIndex, dataArrayLength - 1)) {
// 左孩子存在,查看右孩子是否存在
if (existenceFunc(currentRightChildIndex, dataArrayLength - 1)) {
// 进行 左孩子 和 右孩子 值 大小比较,获得最大值的 元素下标
maxIndex = compareFunc(currentLeftChildIndex, currentRightChildIndex) ? currentLeftChildIndex : currentRightChildIndex
} else {
// 只有左孩子存在
maxIndex = currentLeftChildIndex
}
}
8. 交换
将最大值的值,与 当前元素的值进行交换,并且更新当前元素的下标。
if (maxIndex >= 0 && compareFunc(maxIndex, currentIndex)) {
// 大于则 交换
exchangeFunc(maxIndex, currentIndex)
}
9. 产生新的堆
依次往下查找,直到当前元素为叶子结点,这样我们就会得到一个新的堆 及 排序数组的结果。
新堆结果为: [15,7,9,6,2,1,3]
排序数组结果为:[20]
// 查找条件
while (currentIndex >= 0 && currentIndex <= dataArrayLength - 1) {}
10. 循环结束
判断 当前数组中,是否还有数据,直至为空。
循环的堆结构:
- 初始化:[20, 15, 9, 7, 2, 1, 3, 6]
- 第一次 堆排序: [15, 7, 9, 6, 2, 1, 3]
- 第二次 堆排序:[ 9, 7, 3, 6, 2, 1 ]
- 第三次 堆排序:[ 7, 6, 3, 1, 2 ]
- 第四次 堆排序:[ 6, 2, 3, 1 ]
- 第五次 堆排序:[ 3, 2, 1 ]
- 第六次 堆排序:[ 2, 1 ]
- 第七次 堆排序:[ 1 ]
- 第八次 堆排序:[]
循环后的排序数组:[1, 2, 3, 6, 7, 9, 15, 20]
这是上图 gif 的最终结果,可以很明显看到 最上到下 从左到右 的排序结果。
总结
堆排序 ,作为 选择排序 中的一种,它涉及到 数据结构 中众多的知识点。
这里有我自己的一个总结理解就是:
形成 堆 结构,那么我们就需要从最后一个元素,进行循环对比,来将最大的值赋值到根节点处。这也就是我们常说的 大顶堆
堆排序,就是 现将根节点的值和最后一个元素的值进行交换,再把最后一个元素的值删除,并放到排序数组中。然后将删除后的数组 再次打造为一个 大顶堆。 依次循环,直到结束后。我们得到的排序数组的结果 就是一个 小顶堆。