鏈式結構實現堆排序
在很多數據結構和算法的書上,“堆排序”的實現都是建立在數組上,數組能夠通過下標訪問其元素,其這一特性在堆排序的實現上,使得其編碼實現比鏈式結構簡單,下面我利用鏈表實現堆排序。 在“堆”這種數據結構中,分為“大根堆”和“小根堆”,“大根堆”中其每一個雙親節點大於等於其子女節點,“小根堆”的定義與其相
反, 當然實現最大堆之前必須要建一個堆,一個高度為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)。
鏈式結構實現堆排序