![36b3a293aab70d6235db46b6e4d37b0d.png](https://i-blog.csdnimg.cn/blog_migrate/4fea01823dadffaebcab296962e5c4e8.jpeg)
堆排序是一种原地、时间复杂度
![equation?tex=nlogn](https://i-blog.csdnimg.cn/blog_migrate/ab9ce758f9d3601d24d1471de2c77173.png)
什么是堆
首先堆是一种树,一种满足以下特质的树结构:
- 堆是一个完全二叉树
- 堆中每一个节点的值必须大于或者等于(小于或者等于)其左右子树的值。【大顶堆、小顶堆】
接下来看看同一组数据的不同堆的形态:
![666b4365a8fd204c33ad79d9dadf2959.png](https://i-blog.csdnimg.cn/blog_migrate/4b8b8c2ff7001b144f2695933a74aea7.png)
如何实现一个堆
对于一个ADT,我们需要知道它有哪些操作方法、以及数据存储的方式。
存储一个完全二叉树,最适合使用数组,因为它相比链表不需要存储左、右子树的指针,更加节省内存空间,通过数组索引即可以随机访问到对应元素。
![1907631e52cf94d4e0f565d95c2a1fb2.png](https://i-blog.csdnimg.cn/blog_migrate/6cbc0f37dab8b36f00e15fc25699fa59.jpeg)
【数组第一个位置留空,方便计算】
假设你知道某个节点下标为
![equation?tex=x](https://i-blog.csdnimg.cn/blog_migrate/79271a73b75b1b2971964504afad8a27.png)
![equation?tex=x%2A2](https://i-blog.csdnimg.cn/blog_migrate/79271a73b75b1b2971964504afad8a27.png%2A2)
![equation?tex=x%2A2%2B1](https://i-blog.csdnimg.cn/blog_migrate/79271a73b75b1b2971964504afad8a27.png%2A2%2B1)
![equation?tex=x%2F2](https://i-blog.csdnimg.cn/blog_migrate/79271a73b75b1b2971964504afad8a27.png%2F2)
常见的堆操作
往堆里面插入一个元素
当我们往堆里面插入一个元素后,我们需要对新的堆进行调整,使其能够满足上述的两个特性,那么这个过程我们称为堆化。
堆化实际上有两个操作,一个是从下往上、一个是从上往下。
从下往上
![a1072c1deaca9fc242d833275f43ef67.png](https://i-blog.csdnimg.cn/blog_migrate/881e4e3c87ad9c1b95c2f4a57986052e.jpeg)
从下往上堆化就是指把该元素与其父节点对比、满足条件后交换位置、继续对比、交换,直到不能交换了。
![161be23eab8a8d8aecc283ec6ce3700b.png](https://i-blog.csdnimg.cn/blog_migrate/a47afe78804152e65c521ee5e508ef16.jpeg)
删除堆顶元素
我们都知道大顶堆、小顶堆的堆顶位置存储的分别是最大值、最小值。
假设我们构造的是一个大顶堆,当我们删除堆顶元素后,我们需要把之前堆内第二大元素移到堆顶位置,依次类推,我们依次删除第二大节点、第三大节点、直到叶子节点。
![120ef8ecb91d740444ab10e346d8b062.png](https://i-blog.csdnimg.cn/blog_migrate/ac4ed8a25ac49b21f99f0fd5c410e412.jpeg)
上面的思路其实就是:当我们删除堆顶元素时候,直接把最后一个节点放到堆顶位置,然后从上往下进行堆化。如果父节点的值小于其子节点的值,则进行交换、继续往下对比。
接下来我们分析下堆的插入元素操作【从下往上堆化】、删除堆顶元素操作【从上往下堆化】的时间复杂度:
由于一个完全二叉树的高度不会超过
![equation?tex=log2%5En](https://i-blog.csdnimg.cn/blog_migrate/e743ce462c796b1dff5155b4142fa8fe.png)
![equation?tex=logn](https://i-blog.csdnimg.cn/blog_migrate/2f4a8bccb706ad9bac5493f1566538d6.png)
代码实现:基于堆的排序
堆排序如此优秀,那么它是如何做到的呢?其过程包括两点:建堆、排序。
建堆
我们把无序数组构建成一个二叉堆:
如果你需要对无序数组进行从小到大排序,那么你应该构建为大顶堆;
如果你需要对无序数组进行从大到小排序,那么你应该构建为小顶堆。
假设我们对无序数组[4, 4, 6, 5, 3, 2, 8, 1]进行升序排序。
当建堆结束后,会得到这样一个大顶堆。
8
/
5 6
/ /
4 3 2 4
/
1
分析堆化操作时间复杂度:我们可以画出每一层完全二叉树的高度以及该层的节点个数【这里去除了叶子节点】
![6db5c543cdf208195207cc06942f85ea.png](https://i-blog.csdnimg.cn/blog_migrate/c1d49079762290640f209d11b95390c9.jpeg)
每个节点堆化的过程中,它需要对比、交互的次数与这个节点所在高度成正比,因此我们只需要计算出每个节点所在高度之和就是时间复杂度了。
![169be5322fc0556eddac4fde7db517ea.png](https://i-blog.csdnimg.cn/blog_migrate/971b040c37c7a903f1d77503086fb5b0.jpeg)
![dc7abe23b3fa0774026b8b06b68b5296.png](https://i-blog.csdnimg.cn/blog_migrate/cf25430985a942ee6def5dd384a39d2c.jpeg)
因为 h=log2n,代入公式 S,就能得到 S=O(n),所以,建堆的时间复杂度就是 O(n)。
排序
建堆结束之后,数组中的数据已经是按照大顶堆的特性来组织的。
数组中的第一个元素就是堆顶,也就是最大的元素。我们把它跟最后一个元素交换,那最大元素就放到了下标为 n 的位置。这个过程有点类似上面讲的“删除堆顶元素”的操作,当堆顶元素移除之后,我们把下标为 n 的元素放到堆顶,然后再通过堆化的方法,将剩下的 n−1 个元素重新构建成堆。
堆化完成之后,我们再取堆顶的元素,放到下标是 n−1 的位置,一直重复这个过程,直到最后堆中只剩下标为 1 的一个元素,排序工作就完成了。
![d76fa7b95f4a751581238fe8d4152dea.png](https://i-blog.csdnimg.cn/blog_migrate/d3ccb63f3cb6fa1bbb6b55abb8798534.jpeg)
/**
* 下沉操作
* @param {array} arr 待调整的堆
* @param {number} parentIndex 要下沉的父节点
* @param {number} length 堆的有效大小
*/
function downAdjust(arr, parentIndex, length) {
// temp保存父节点的值,用于最后赋值
let temp = arr[parentIndex]
let childrenIndex = 2 * parentIndex + 1
while(childrenIndex < length) {
// 如果有右孩子,且右孩子大于左孩子的值,则定位到右孩子
// 这里其实是比较左、右子树的大小,选择更大的
if (childrenIndex + 1 < length && arr[childrenIndex + 1] > arr[childrenIndex]) {
childrenIndex++
}
// 如果父节点大于任何一个孩子得值,则直接跳出
if (temp >= arr[childrenIndex]) {
break
}
// 当左、右子树比父节点更大,进行交换
arr[parentIndex] = arr[childrenIndex]
parentIndex = childrenIndex
childrenIndex = 2 * childrenIndex + 1
}
arr[parentIndex] = temp
}
/**
* 堆排序(升序)
* @param {array} arr 待调整的堆
*/
function heapSort(arr) {
// 把无序数组构建成最大堆, 这里-2,是因为从索引0开始、另外就是叶子节点【最后一层是不需要堆化的】
for(let i = (arr.length - 2)/2; i >= 0; i--) {
downAdjust(arr, i, arr.length)
}
// 循环删除堆顶元素,并且移到集合尾部,调整堆产生新的堆顶
for(let i = arr.length - 1; i > 0; i--) {
// 交换最后一个元素与第一个元素
let temp = arr[i]
arr[i] = arr[0]
arr[0] = temp
// 下沉调整最大堆
downAdjust(arr, 0, i)
}
return arr
}
// test case
console.log(heapSort([4, 4, 6, 5, 3, 2, 8, 1]))
整个堆排序的过程,都只需要极个别临时存储空间,所以堆排序是原地排序算法。堆排序包括建堆和排序两个操作,建堆过程的时间复杂度是 O(n),排序过程的时间复杂度是 O(nlogn),所以,堆排序整体的时间复杂度是 O(nlogn)。
堆排序不是稳定的排序算法,因为在排序的过程,存在将堆的最后一个节点跟堆顶节点互换的操作,所以就有可能改变值相同数据的原始相对顺序。
堆排序 VS 快排
对于同样的数据,堆排序对于数据的交换次数要多于快排,因为堆排序第一步是建堆,这个过程会打乱数据的有序度。