1、堆定义
堆定义如下:n个元素的序列{k1,k2,k3,…..,kn}当且仅当满足一下关系时,称为堆。
就是这两个公式,仔细体会,i是指结点下标,ki指第i个结点,i的范围(i=i,2,……,n/2)n是要排序的长度。
将一维数组看成是一个完全二叉树,则堆的含义表明,完全二叉树中所有非终端结点(叶子结点)的值均不大于(或者均不小于)其左右孩子结点的值。
例如:[1,2,3,17,19,36,7,25,100] 这个数组,生成的堆就是如下图,
这是我们常用的二叉树堆,还有其他的一些二项式堆和斐波纳契堆等。
2、二叉树堆的存储
一般都用数组来存储堆,i结点的父节点就是(n-1)/2,它的左结点是2*i+1,右结点是2*i+2,这些特性都是二叉树的特性,一个完全二叉树。
3、如何使用堆排序
上述的两个公式定义的是公式中的最大堆和最小堆,就是父节点比左右结点大则为最大堆,父节点比左右结点小则为最小堆,我们第一步要先使用数组来构建一个最大堆或者最小堆,之后再进行堆排序。
1、堆化数组
把数组的原始序列默认为堆结构,之后进行调整使其满足上述的两个公式
例如:下面这个数组进行最小堆的堆化
这里面进行了四个步骤操作
1、A步骤,进行最后一个结点和父节点的调整,再堆化过程中我们都是先进行最后一个结点进行堆化,通过数组的长度就能找出这个最后的结点。i=(n-1)/2=4,i–
2、B步骤,进行i=3这个结点的调整,i–
3、C步骤,进行i=2结点的调整,i–
4、D步骤,进行i=1结点的调整,i–
5、最后依次,进行结点0的调整
直接上代码:
//创建最大树堆
func MakeMaxHeap(data []int) {
n := len(data)
for i := (n - 1) >> 1; i >= 0; i-- {
MaxHeapFixDows(data, i, n)
}
}
//最大堆:父节点比左右结点都大
//最小堆:父节点比左右结点都小
//如果n为奇数,则最后一个结点一定是包含一个左结点,n为偶数,则一定是一个满二叉树,则没有空余的子节点
func MaxHeapFixDows(data []int, i, n int) {
//保存根元素
root := data[i]
//左孩子结点
j := i<<1 + 1
for j < n {
//右孩子和左孩子进行比较,j+1<n是为了防止数组下标越界
if j+1 < n && data[j+1] > data[j] {
//如果右孩子较大则用右孩子和父节点做比较,否则用左孩子进行比较,j++就得到右孩子结点
j++
}
//如果最大的孩子结点都小于父节点,则break
if data[j] < root {
break
}
//这里的j,如果上面的if不成立,则说明是左孩子大,则j=i<<1+1,否则j=i<<1+2
if data[j] > root {
//如果孩子比父亲大,则进行交换,这里忽略一种情况就是相等就不进行交换了
data[i] = data[j]
}
//这里是一个递归进入下一个结点的过程,比如:上述步骤中D,i=1,则子节点j等于3,到这里之后就会进入变为i=3,j=7就是它的下一个堆的左结点下标
i = j
j = i<<1 + 1
}
data[i] = root
}
output:
[65 50 60 30 19 20 17 12 9 4],这就是最大堆的结果,看到这里要自己动手实现一个最小堆就能彻底理解了。
4、堆排序
//使用最小堆得到的是降序,最大堆得到的是升序
func HeapSort(data []int) {
n := len(data)
for i := (n - 1); i >= 1; i-- {
data[i], data[0] = data[0], data[i]
MinHeapFixdown(data, 0, i)
// MaxHeapFixDows(data, 0, i)
}
}
output:
[4 9 17 12 19 20 60 65 30 50]
[65 60 50 30 20 19 17 12 9 4]
排序思路:如果是 最小堆,那么堆顶元素肯定是最小的,就把堆顶元素和最后一个元素进行互换,然后再进行堆”筛选“,进过一次的筛选之后,堆顶上的元素又是最小的,除去最后一个,这是再和倒数第二个元素交换,这样就得到了倒数第二小的了,依次就可以进行排序。
这里有一幅图,我觉得很生动,可以提高理解:
初始进行 最大堆堆化:
对最大堆进行排序:
我写这个主要是为了阅读golang源码中sort包的排序,sort包中也使用了堆排序,不过它实现不同。原理是相同。
参考的博文: