堆的定义

简单来说,堆就是一种有特定约束的完全二叉树。堆要求堆中所有父节点的值大于等于其左右孩子的值,或者小于等于左右孩子的值。前一种被称为大顶堆,后一种被称为小顶堆。

 

建堆和调整

  因为是完全二叉树,可以用数组存储堆这种数据结构,那么我们要怎么样将一个数组中的元素成一个堆呢?其实很简单,只要对每个非叶子节点进行调整,调整的方法如下:

  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)时间复杂的的算法

稳定性

堆排序是一种不稳定的排序方法。