堆的定义
简单来说,堆就是一种有特定约束的完全二叉树。堆要求堆中所有父节点的值大于等于其左右孩子的值,或者小于等于左右孩子的值。前一种被称为大顶堆,后一种被称为小顶堆。
建堆和调整
因为是完全二叉树,可以用数组存储堆这种数据结构,那么我们要怎么样将一个数组中的元素成一个堆呢?其实很简单,只要对每个非叶子节点进行调整,调整的方法如下:
1. 如果该节点的值满足堆的定义(大于等于或小于等于其左右节点的值),则不调整
2. 若不满足,则需要将它和对应的节点互换(如果两个节点都不满足,则选择更大或者更小的一个)
3. 交换以后,若子树根节点不是叶子节点,进一步对子树进行1,2步骤操作
调整的顺序是从后往前的,即从最后一个非叶子节点开始调整,一直到根节点调整完毕为止。
下面我们举一个具体的例子,看一下堆排序的调整过程(假设一个堆是大顶堆),初始数组为45,35,78,28,69,44,17,17,98:
1. 将一个初始的数组转换为完全二叉树的形式
2. 从第一个非叶子节点可以调整,也就是28,将它和98交换
3. 继续调整下一个非叶子节点,该节点值为78,大于其左右节点的值,满足大顶堆的定义,所以不用调整。
4. 调整下一个非叶子节点,35<98且98>69,所以和98的节点交换,交换以后该子树也满足大顶堆的要求。不用继续交换。
5. 调整根节点,该节点小于左孩子98,与之交换,交换以后发现左子树根节点的值依然小于其右孩子的值,再与之交换。
至此建堆完成。
堆排序
从上面的代码可以看出,在一个建好的堆中,堆顶元素肯定是数组中最大的元素,所以我们拿堆顶元素和堆的最后一个元素交换,该元素的位置随即被确定下来。堆中元素数量减一。然后因为刚刚的交换,堆的性质被破坏,要进行调整。调整时只需要调整根节点即可,因为其他节点没有被替换过,必然符合堆的定义。
1. 交换
2. 调整根节点
由此可见,堆排序每次确定一个元素在序列中的位置,然后调整堆,循环这个过程,直到所有元素的位置都被确定下来。
实现堆排序的代码如下:
1 void heapAdjust(int a[],int i,int length) 2 { 3 int tmp = a[i]; 4 for(int k=2*i+1;k<length;k = 2*k+1) 5 { 6 if(k+1<length&&a[k+1]>a[k]) 7 k++; 8 if(tmp>a[k]) 9 break; 10 a[i] = a[k]; 11 i = k; 12 } 13 a[i] = tmp; 14 } 15 void heapSort(int a[],int length) 16 { 17 for(int i=(length-1)/2;i>=0;i--) 18 { 19 heapAdjust(a,i,length); 20 } 21 for(int i=length-1;i>=0;i--) 22 { 23 swap(a[0],a[i]); 24 heapAdjust(a,0,i); 25 } 26 }
堆排序的应用
堆常被应用于实现优先队列,C++ STL中的priority_queue就是通过堆实现的。
时间复杂度
堆排序也是一种O(nlogn)时间复杂的的算法
稳定性
堆排序是一种不稳定的排序方法。