本章开始介绍了堆的基本概念,然后引入最大堆和最小堆的概念。全章采用最大堆来介绍堆的操作,两个重要的操作是调整最大堆和创建最大堆,接着着两个操作引进了堆排序,最后介绍了采用堆实现优先级队列。
1、堆
堆给人的感觉是一个二叉树,但是其本质是一种数组对象,因为对堆进行操作的时候将堆视为一颗完全二叉树,树种每个节点与数组中的存放该节点值的那个元素对应。所以堆又称为二叉堆,堆与完全二叉树的对应关系如下图所示:
通常给定节点i,可以根据其在数组中的位置求出该节点的父亲节点、左右孩子节点,这三个过程一般采用宏或者内联函数实现。书上介绍的时候,数组的下标是从1开始的,所有可到:PARENT(i)=i/2 LEFT(i) = 2*i RIGHT(i) = 2*i+1。
根据节点数值满足的条件,可以将分为最大堆和最小堆。最大堆的特性是:除了根节点以外的每个节点i,有A[PARENT(i)] >= A[i],最小堆的特性是:除了根节点以外的每个节点i,有A[PARENT(i)] >=A[i]。
把堆看成一个棵树,有如下的特性:
(1)含有n个元素的堆的高度是lgn。
(2)当用数组表示存储了n个元素的堆时,叶子节点的下标是n/2+1,n/2+2,……,n。
(3)在最大堆中,最大元素该子树的根上;在最小堆中,最小元素在该子树的根上。
2、保持堆的性质
堆个关键操作过程是如何保持堆的特有性质,给定一个节点i,要保证以i为根的子树满足堆性质。书中以最大堆作为例子进行讲解,并给出了递归形式的保持最大堆性的操作过程MAX-HEAPIFY。先从看一个例子,操作过程如下图所示:
从图中可以看出,在节点i=2时,不满足最大堆的要求,需要进行调整,选择节点2的左右孩子中最大一个进行交换,然后检查交换后的节点i=4是否满足最大堆的要求,从图看出不满足,接着进行调整,直到没有交换为止。书中给出了递归形式的为代码,我用C语言实现如下所示:
1 void adjust_max_heap_recursive(int *datas,int length,int i) 2 { 3 int left,right,largest; 4 int temp; 5 left = LEFT(i); //left child 6 right = RIGHT(i); //right child 7 //find the largest value among left and rihgt and i. 8 if(left<=length && datas[left] > datas[i]) 9 largest = left;10 else11 largest = i;12 if(right <= length && datas[right] > datas[largest])13 largest = right;14 //exchange i and largest15 if(largest != i)16 {17 temp = datas[i];18 datas[i] = datas[largest];19 datas[largest] = temp;20 //recursive call the function,adjust from largest21 adjust_max_heap(datas,length,largest);22 }23 }
课后习题要求给出其非递归的形式,我想了半天,才搞出来,领悟能力有限啊。非递归就要考虑要循环进行实现,需要考虑的是循环结束条件是什么。对一个给定的节点i,要对其进行调整使其满足最大堆的性质。总的思想是先找出节点i的左右孩子节点,然后从三者中找到最大的节点,如果找到的最大节点就是i,说明i节点满足堆的性质,此时循环就结束了。如果找到的最大节点不是节点i,那么这个时候就要将最大的节点(设为largest)与节点i进行交换,然后从largest节点开始循环进行调整,直到满足条件为止。给出非递归的调整堆程序如下:
1 void adjust_max_heap(int *datas,int length,int i) 2 { 3 int left,right,largest; 4 int temp; 5 while(1) 6 { 7 left = LEFT(i); //left child 8 right = RIGHT(i); //right child 9 //find the largest value among left and rihgt and i.10 if(left <= length && datas[left] > datas[i])11 largest = left;12 else13 largest = i;14 if(right <= length && datas[right] > datas[largest])15 largest = right;16 //exchange i and largest17 if(largest != i)18 {19 temp = datas[i];20 datas[i] = datas[largest];21 datas[largest] = temp;22 i = largest;23 continue;24 }25 else26 break;27 }28 }
3、建堆
建立最大堆的过程是自底向上地调用最大堆调整程序将一个数组A[1.....N]变成一个最大堆。将数组视为一颗完全二叉树,从其最后一个非叶子节点(n/2)开始调整。调整过程如下图所示:
书中给出了创建堆的为代码,我用C语言实现如下:
1 void build_max_heap(int *datas,int length)2 {3 int i;4 //build max heap from the last parent node5 for(i=length/2;i>0;i--)6 adjust_max_heap(datas,length,i);7 }
4、堆排序算法
堆排序算法过程为:先调用创建堆函数将输入数组A[1...n]造成一个最大堆,使得最大的值存放在数组第一个位置A[1],然后用数组最后一个位置元素与第一个位置进行交换,并将堆的大小减少1,并调用最大堆调整函数从第一个位置调整最大堆。给出堆数组A={4,1,3,16,9,10,14,8,7}进行堆排序简单的过程如下:(1)创建最大堆,数组第一个元素最大,执行后结果下图:
(2)进行循环,从length(a)到2,并不断的调整最大堆,给出一个简单过程如下:
书中给出了对排序为代码,我用C语言实现如下所示:
1 void heap_sort(int *datas,int length) 2 { 3 int i,temp; 4 //bulid max heap 5 build_max_heap(datas,length); 6 i=length; 7 //exchange the first value to the last unitl i=1 8 while(i>1) 9 {10 temp = datas[i];11 datas[i] = datas[1];12 datas[1] =temp;13 i--;14 //adjust max heap,make sure the fisrt value is the largest15 adjust_max_heap(datas,i,1);16 }17 }
结合上面的调整堆和创建堆 的过程,写个简单测试程序连续堆排序,程序如下所示:
程序测试结果如下所示:
从结果可以看出按照最大堆进行堆排序最终使得结果是从小到大排序(非递减的)。堆排序算法时间复杂度:调整堆过程满足递归式T(n)<=T(2n/3)+θ(1),有master定义可以知道T(n) = O(lgn),堆排序过程中执行一个循环,调用最大堆调整函数,总的时间复杂度为O(nlgn)。
5、问题
(1)在创建最大堆的过程中,为什么从最后一个非叶子节点(n/2)开始到第一个非叶子(1)结束,而不是从第一个非叶子节点(1)到最后一个非叶子节点(n/2)结束呢?
我的想法是:如果是从第一个非叶子节点开始创建堆,有可能导致创建的堆不满足堆的性质,使得第一个元素不是最大的。这样做只是使得该节点的和其左右孩子节点满足堆性质,不能确保整个树满足堆的性质。如果最大的节点在叶子节点,那么将可能不会出现在根节点中。例如下面的例子:
从图中可以看出,从第一个非叶子节点开始创建最大堆,最后得到的结果并不是最大堆。而从最后一个非叶子节点开始创建堆时候,能够保证该节点的子树都满足堆的性质,从而自底向上进行调整堆,最终使得满足最大堆的性质。