算法介绍
堆排序是一种基于堆数据结构的排序算法,它的时间复杂度为 O(n log n)。堆是一种完全二叉树,可以分为最大堆和最小堆两种类型。在最大堆中,每个父节点的值都大于或等于其子节点的值;在最小堆中,每个父节点的值都小于或等于其子节点的值。
注意这里的堆和内存模型中的堆不是一个层次的东西,这里的堆是指的是数据结构中的堆,而内存模型中的堆指的是堆内存。搜集了下资料,应该是最早使用堆数据结构来进行堆区的内存分配管理(比如优先分配哪块内存),故把该内存分区称为堆。
堆排序的基本思路是先将待排序的序列构建成一个堆,然后将堆顶元素与堆底元素交换,并将堆的大小减一,使得堆底元素成为有序区的一部分。接着,重新调整剩余的元素,使其构成一个新的堆。重复这个过程,直到堆的大小为1,排序完成。
堆排序的优点是不占用额外的空间(原地对排序算法中只有常数项变量,空间复杂度为O(1)),同时具有比较高的效率和稳定性。它适用于大数据量的排序,尤其是在数据规模较大而内存较小的情况下。
除了叫做堆,很多地方也叫作优先队列(priority queue)。因此,在调用一些函数或者使用STL的时候,看到优先队列,就知道是堆这样的结构。
实现理论
原地排序
可以用一种称作原地堆排序的技巧避免保存每次取得的堆顶的空间开销,具体做法如下。
首先将原待排序数组 arr[] 建立为一个大顶堆 ( heapify 堆化方法)。
交换堆顶和当前未排序部分中最末尾元素,则堆顶元素已排序(此时在数组最末尾)。
剩余元素中 只有当前堆顶 (上一步被交换的末尾元素) 可能造成 堆失序,因此只需对堆顶调用一次调整堆序的下滤 (siftDown) 操作 (操作范围为未排序部分) ,即可恢复未排序部分的堆序。
重复 2,3 直到所有元素已排序,返回 arr[] 。
上述通过交换堆顶与当前未排序部分末尾元素的做法,避免了额外的空间开销,即 原地堆排序,程序结束后返回的 arr[] 为已排序状态。
堆化方法 (heapify)
将原输入数组看作一棵 完全二叉树(Complete Binary Tree) 。根节点下标为 0,于是根据完全二叉树的结构性质,任意一个节点 (下标为 i ) 的左子节点下标为 2∗i+1,右子节点下标为 2∗i+2,父节点下标为 (i−1)/2。 堆化过程即使得整棵树满足堆序性质,也即任意一个节点大于等于其子节点(大顶堆)。
一句话总结堆化操作:对最后一个非叶子节点到根节点,依次执行下滤操作 (siftDown) 。
从最后一个非叶子开始下滤的原因是此节点之后的节点均为叶子节点,叶子节点无子节点,故其本身已满足堆序性质,也就无下滤的必要 (也不会下滤)。每一次下滤使得该节点及其之后的节点都满足堆序性质,直到根节点。
最后一个非叶子节点 (也即最后一个元素的父节点) 下标为 (n−1)/2,n 为数组长度。
下滤方法(siftDown)
下滤 (siftDown) 是堆排序的核心方法,在堆排序中的如下两种操作中调用:
排序开始时 创建大顶堆 的堆化方法
每次排序堆顶时用于 恢复未排序部分的堆序
该方法在堆数据结构中用于删除堆顶元素操作,因此先介绍下滤在删除堆顶元素操作中的处理过程。
以下为删除大顶堆 {9,8,5,6,7,2,4,1,3} 堆顶元素 9 的步骤(动图中出现的100表示堆顶,值为9)。
- 删除堆顶,堆中元素减 1,将当前最后一个元素 3 暂时置为堆顶。
- 可以看到,此时只有该堆顶元素 3 导致堆失序,于是交换其与左右子节点中的较大者。
- 对元素 3 重复操作 2 ,直到恢复堆序。
恢复堆序的过程就是将影响堆序的元素不断向下层移动 (并交换) 的过程,因此形象地称之为下滤 (siftDown) 。
注意,此处沿用 JDK 源码中下滤操作的方法名 siftDown,sift 为过滤之意。
对节点 x 的下滤操作的本质是恢复以 x 为根节点的树的堆序。因此在堆化操作中,只需要分别依次地对最后一个非叶子节点到根节点执行下滤操作,即可使整棵树满足堆序。在排序过程中,每次原地交换后 (交换当前堆顶与当前未排序部分最后一个元素),只有新堆顶影响堆序,对其执行 一次 下滤操作 (范围为未排序部分) 即可使未排序部分重新满足堆序。
实现代码
Go语言实现代码:
package main
import "fmt"
func heapSort(arr []int) []int {
if len(arr) < 2 {
return arr
}
heapify(arr, len(arr)-1) // 构建大顶堆
for i := len(arr) - 1; i > 0; i-- {
swap(arr, 0, i) // 交换堆顶和当前未排序部分最后一个元素
siftDown(arr, 0, i-1) // i - 1 是未排序部分最后一个元素下标,传入此参数确保下滤不会超过此范围
}
return arr
}
// 堆化方法
func heapify(arr []int, r int) {
for hole := (r - 1) / 2; hole >= 0; hole-- {
siftDown(arr, hole, r)
}
}
// 下滤方法
func siftDown(arr []int, hole, r int) {
target, child := arr[hole], hole*2+1
for child <= r {
if child < r && arr[child+1] > arr[child] {
child++
}
if arr[child] > target {
arr[hole] = arr[child]
hole = child
child = hole*2 + 1
} else {
break
}
}
arr[hole] = target
}
func swap(arr []int, i, j int) {
arr[i], arr[j] = arr[j], arr[i]
}
func main() {
arr := []int{7, 3, 2, 8, 1, 9, 5, 4, 6}
result := heapSort(arr)
fmt.Println(result)
}