0x01 背景
多年之前写了一篇博客,实现非递归方式的堆排序,多年之后才发现写的有问题。连忙撤了下来,重新复习了《算法导论》,用Go实现了正确的非递归排序。算是填上了当年的坑。基本上是复原了书上的实现,加上自己的理解。
0x02 堆与堆排序
堆是一种完全二叉树。它具有如下特点:
每个节点都比其他子节点的值大或者小。
都大的叫大根堆,都小的叫小根堆。反过来表达,除根节点外,其他节点都比父节点小或者大。堆可以和一个数组对应起来。下标为i的元素的两个子节点分别为2i+1和2i+2。
堆排序就是利用堆的构建过程进行的排序。比如构建一个大根堆,然后将根与数组的最后一个元素交换,将堆的大小减1之后,再将剩下的元素恢复一个大根堆,这样重复到最后,堆的大小变为1,就完成了排序过程。
0x03 为什么之前的理解有问题?
之前的理解受到了二叉搜索树的查找过程的误导,认为只要递归地比较交换每个节点和它的两个子节点,就能完成建堆的过程。
其实这样是有问题的,这样递归构建时,是深度优先的。先构建h层,然后是h-1层。但h-1交换元素时,可能就破坏了之前已经执行过的h层堆的性质。同样,非递归实现时,也没有考虑到这个问题,所以也是有问题的。
如下图,第一次将3和6交换。递归到第1层时,将8和3交换,最终第2层的堆性质被破坏。
0x04 正确的非递归实现
实现很简单,将书中MAX-HEAPIFY修改为循环就行了。考虑到Go中也可以使用goto,最终为:
package sort
import (
stdsort "sort"
)
func leftChild(i int) int {
return i * 2 + 1
}
func rightChild(i int) int {
return i * 2 + 2
}
func maxHeapify(data stdsort.Interface, i, end int) {
retry_:
l := leftChild(i)
r := rightChild(i)
largest := i
if l <= end && data.Less(i, l) {
largest = l
}
if r <= end && data.Less(largest, r) {
largest = r
}
if largest != i {
data.Swap(i, largest)
i = largest
goto retry_
}
}
// buildMaxHeap builds a heap in a non-recursive way.
func buildMaxHeap(data stdsort.Interface, start, end int) {
e := (end+1)/2 - 1
for e >= start {
maxHeapify(data, e, end)
e--
}
}
// HeapSort sorts an array in inc order.
func HeapSort(data stdsort.Interface) {
end := data.Len() - 1
buildMaxHeap(data, 0, end)
for i := 0; i < end; i++ {
data.Swap(0, end-i)
maxHeapify(data, 0, end-i-1)
}
}
0x05 使用场景
堆排序是原址不稳定排序。原因是在交换父子节点值时,是跨层交换的,可能将一个节点与同值节点的先后顺序打乱。
但堆也有其独特的应用场景:
- 优先队列。
与堆排序过程一样。首先,构建出一个堆。- 需要出队时,将根出队,将队尾元素放到根位置,重新运行一次HEAPIFY,恢复堆。时间复杂度为O(lg(n))。
- 入队时,将元素放到队列尾部,沿此元素到根的路径上进行比较交换,直到找到合适的位置。时间复杂度同样为O(lg(n))。
- 最大k值。
海量数据的最大k值问题。堆非常适合这种场景。真的要去比较所有数据,构建有序数列的话,运算量会非常的大。而通过构建大小为K的小根堆,可以快速完成。数量量为n的话。快速排序的复杂度为O(nlg(n)。而使用堆的话,就是:O(klg(k)) + O(nlg(k)),在k远小于n的情况下最终为:O(nlg(k))。
实现过程也简单,先使用前k个数据构建出小根堆,遍历剩下的元素,如果大于根,就把与根交换,执行一个HEAPIFY恢复堆。这样直到所有数据遍历完成,剩下的堆就是最大的K个元素。
0x06 总结
对于算法要有严格的理解,经典教材里的原理一定要理解到位。否则可能在后面的编码生涯中挖坑埋雷。