在很多数据结构和算法的书上,“堆排序”的实现都是建立在数组上,数组能够通过下标访问其元素,其这一特性在堆排序的实现上,使得其编码实现比链式结构简单,下面我利用链表实现堆排序。 在“堆”这种数据结构中,分为“大根堆”和“小根堆”,“大根堆”中其每一个双亲节点大于等于其子女节点,“小根堆”的定义与其相
反, 当然实现最大堆之前必须要建一个堆,一个高度为h的堆,它的前h-1层时满的,如下图所示:
通过对上图的直观的感受,其构成是一课完全二叉树,在完全二叉树中,每一个节点时按照层数来放置的,只有每一层放满了之后才会进入下一层,所以建完全二叉树的时候就需要按层建节点,下图为其建完全二叉树的过程:
但是由于建树时只能通过双亲访问左右子女,不能通过子女访问双亲,所以在解决这个问题需要储存每个节点的地址,所以分析其建树过程,我们采用了队列来存储每个节点的过程,其实现过程如图:
在建完全二叉树时每挂一个节点时,该节点就要入队,但是什么时候出队时个关键的问题,观察每次的挂节点会发现,当双亲节点把右节点挂完之后(即双亲节点的左右子女都不为空),双亲节点会改变,但也会出现最后一个节点出现只有一个左子女的情况,但是这种情况会随着循环而结束,因为这个节点的位置就是挂的最后一个节点。
下面是其代码的具体实现过程:
- Node *BuildCompleteBTree(int *a, int heap_size, Node **pQueue)
- {
- int i;
- Node *newNode,*root,*p = NULL;
- for(i = 0; i < heap_size; ++i){
- newNode = (Node*)malloc(sizeof(Node));
- newNode -> data = a[i];
- newNode -> left = newNode -> right = NULL;
- if(i == 0) pQueue[++rear] = root = newNode; //根节点入队
- else{ //不是建根节点时
- if(!p) p = pQueue[++front];
- if(!p -> left){
- p -> left = newNode; //挂左儿子
- pQueue[++rear] = newNode; //左儿子入队
- }
- else{
- p -> right = newNode;
- pQueue[++rear] = newNode;
- p =NULL;
- }
- }
- }
- return root;
- }
其中front 和 rear 为全局变量,初始值为1。
完全二叉树建完后只是完成了第一步工作,要使这棵完全二叉树变为堆,还需要对这棵树进行调整,使其保持每一个双亲节点大于等于其子女节点的属性,其中根节点为这个堆中的最大值。
在调整中会发现从根节点向下调整时无法一次将最大值调整到根节点上去,所以我们由下往上将最大值向上推,如图是调整一次后的结果:
从上面的一次调整可以看出,由下向上调整时虽然可以将当前所有数据的最大值推到根上但是并不能完成建最大堆的工作,所以我们需要反复调整才能达到目的,其代码实现的过程如下:
- void Max_heapify(Node **pQueue)
- {
- int temp;
- Node *largest;
- while(1){
- int k = front,flag = 0;
- while(k > 0){ //向前跑双亲
- largest = pQueue[k];
- if(largest -> data < largest -> left -> data) largest = largest -> left; //判断左儿子
- if(largest->right && largest->data < largest -> right -> data) largest = largest->right; //判断右儿子
- if(largest != pQueue[k]) { //调整保持最大堆
- temp = pQueue[k] -> data;
- pQueue[k] -> data = largest -> data;
- largest -> data = temp;
- flag = 1;
- }
- --k;
- }
- if(!flag) break;
- }
- }
其中值得注意的问题是,要从下往上调整,需要最后一个双亲的位置,那么如何确定位置了,答案就是front,数组中front中的值就是当前的双亲,而front前面的值都是双亲,注意到这一点的话后面的问题就迎刃而解,注意在调整的过程中front的是不能改变的,我们可以设置另一个值等于front,这里我们用变量k,所以每次跑循环的时候是k在跑,而front一直标识着最后一个双亲节点的位置;但是在一次调整后如何判断本次调整是不是将堆调成了最大堆呢?这里我们想想当当前的堆是最大堆时再进行调整时是不会出现交换过程的,所以当堆变为最大堆时,交换过程是不会不出现的,所以我设置了一个标志flag来标记是否进行了交换,若出现了交换flag = 1,若没有交换flag = 0,最后通过判断flag的值来判断当前堆的状态。
在建好最大堆后后面的工作就简单了,首先将根节点的值和最后一个节点交换,然后删除最后一个节点,然后再调整改变了大小的堆使其保持最大堆,然后循环这个过程,直至只剩下一个节点,下图为删除一个节点的过程:
循环执行这个过程直至剩下一个根节点即可,但是在删除过程中front的位置是会变的,当删除左儿子时其front就要跑到上一个双亲,这是front要减一,下面是其代码的实现:
- void getSequence(Node **pQueue)
- {
- Node *root = pQueue[1];
- int temp;
- while(root ->left || root ->right){
- temp = root ->data;
- root -> data = pQueue[rear] -> data;
- pQueue[rear--] -> data = temp;
- if(!pQueue[front] -> right) { //右节点为空时,最后一个双亲只有一个左儿子
- pQueue[front] -> left = NULL;
- --front;
- }
- else
- pQueue[front] -> right = NULL;
- Max_heapify(pQueue);
- }
- }
然后输出的时候只要遍历队列输出就可以输出有序的序列了!堆排序是一种稳定的排序算法,其算法的时间复杂度为O(NLgN)。