《大話資料結構》第9章 排序 9 7 堆排序(下)

2021-07-03 22:00:46 字數 3488 閱讀 1790

堆排序(heap sort)就是利用堆(假設利用大頂堆)進行排序的方法。它的基本思想是,將待排序的序列構造成乙個大頂堆。此時,整個序列的最大值就是堆頂的根結點。將它移走(其實就是將其與堆陣列的末尾元素交換,此時末尾元素就是最大值),然後將剩餘的n-1個序列重新構造成乙個堆,這樣就會得到n個元素中的次小值。如此反覆執行,便能得到乙個有序序列了。

例如圖9-7-4,左圖是乙個大頂堆,90為最大值,將90與20(末尾元素)互換,如中圖所示,此時90就成了整個堆序列的最後乙個元素,將20經過調整,使得除90以外的結點繼續滿足大頂堆定義(所有結點都大於等於其子孩子),見右圖。然後再考慮將30與80互換……

相信大家有些明白堆排序的基本思想了,不過要實現它還需要解決兩個問題:(1)如何由乙個無序序列構建成乙個堆?(2)如果在輸出堆頂元素後,調整剩餘元素成為乙個新的堆?要解釋清楚它們,讓我們來看**。

/* 對順序表l進行堆排序 */

void heapsort(sqlist *l)

}

從**中也可以看出,整個排序過程分為兩個for迴圈。第乙個迴圈要完成的就是將現在的待排序序列構建成乙個大頂堆。第二個迴圈要完成的就是逐步將每個最大值的根結點與末尾元素交換,並且再調整其成為大頂堆。

假設我們要排序的序列是 ,那麼l.length=9,第乙個for迴圈,**第4行,i是從⌊9/2⌋=4開始,4→3→2→1的變數變化。為什麼不是從1到9,或者從9到1,而是從4到1呢?其實我們看了圖9-7-5就明白了,它們都有什麼規律?它們都是有孩子的結點。注意灰色結點的下標編號就是1、2、3、4。

我們所謂的將待排序的序列構建成為乙個大頂堆,其實就是從下往上,從右到左,將每個非終端結點(非葉結點)當作根結點,將其和其子樹調整成大頂堆。i的4→3→2→1的變數變化,其實也就是30,90,10、50的結點調整過程。

既然已經弄清楚i的變化是在調整哪些元素了,現在我們來看關鍵的heapadjust(堆調整)函式是如何實現的。

/* 已知l->r[s..m]中記錄的關鍵字除l->r[s]之外均滿足堆的定義, */

/* 本函式調整l->r[s]的關鍵字,使l->r[s..m]成為乙個大頂堆 */

void heapadjust(sqlist *l,int s,int m)

l->r[s]=temp; /* 插入 */

}

1) 函式被第一次呼叫時,s=4,m=9,傳入的sqlist引數的值為length=9,r[10]=

2) 第4行,將l.r[s]=l.r[4]=30賦值給temp。如圖9-7-6

3) 第5~13行,迴圈遍歷其結點的孩子。這裡j變數為什麼是從2*s開始呢?又為什麼是j*=2遞增呢?原因還是二叉樹的性質5,因為我們這棵是完全二叉樹,當前結點序號是s,其左孩子的序號一定是2s,右孩子的序號一定是2s+1,它們的孩子當然也是以2的位數序號增加,因此j變數才是這樣迴圈。

4) 第7~8行,此時j=2*4=8, jm,因此跳出迴圈

8) 第14行,將temp=30賦值給l.r[s]=l.r[8],完成30與60的交換工作。如圖9-7-7。本次函式呼叫完成。

9) 再次呼叫heapadjust,此時s=3,m=9。第4行,temp=l.r[3]=90,第7~8行,由於40<80得到j+1=2*s+1=7。9~10行,由於90>80,因此退出迴圈,最終本次呼叫,整個序列未發什麼改變。

10) 再次呼叫heapadjust,此時s=2,m=9。第4行,temp=l.r[2]=10,第7~8行,60<70,使得j=5。最終本次呼叫使得10與70進行了互換。

11) 再次呼叫heapadjust,此時s=1,m=9。第4行,temp=l.r[1]=50,第7~8行,70<90,使得j=3。第11~12行,l.r[1]被賦值了90,並且s=3,再迴圈,由於2j=6並未大於m,因此再次執行迴圈體,使得l.r[3]被賦值了80,完成迴圈後,l.[7]被賦值為50,最終本次呼叫使得50、90、80進行了輪換。

接下來heapsort函式的第6~11行就是正式的排序過程,由於有了前面的充分準備,其實這個排序就比較輕鬆了。下面是這部分**。

6 for(i=l->length;i>1;i--)

7

1) 當i=9時,第8行,交換20與90,第9行,將當前的根結點20進行大頂堆的調整,調整過程和剛才流程一樣,找到它左右子結點的較大值,互換,再找到其子結點的較大值互換。此時序列變為,如圖9-7-10

2) 當i=8時,交換30與80,並將30與70交換,再與60交換,此時序列變為,如圖9-7-10

3) 後面的變化完全類似,不解釋,只看圖。

最終就得到乙個完全有序的序列了。

9.7.3 堆排序複雜度分析

堆排序的效率到底有多好呢?我們來分析一下。

它的執行時間主要是消耗在初始構建堆和在重建堆時的反覆篩選上。

在構建堆的過程中,因為我們是完全二叉樹從最下層最右邊的非終端結點開始構建,將它與其孩子進行比較和若有必要的互換,對於每個非終端結點來說,其實最多進行兩次比較和互換操作,因此整個構建堆的時間複雜度為o(n)。

在正式排序時,第i次取堆頂記錄重建堆需要用o(logi)的時間(完全二叉樹的某個結點到根結點的距離為⌊log2i⌋+1),並且需要取n-1次堆頂記錄,因此,重建堆的時間複雜度為o(nlogn)。

所以總體來說,堆排序的時間複雜度為o(nlogn)。由於堆排序對原始記錄的排序狀態並不敏感,因此它無論是最好、最壞和平均時間複雜度均為o(nlogn)。這在效能上顯然要遠遠好過於冒泡、簡單選擇、直接插入的o(n2)的時間複雜度了。 

空間複雜度上,它只有乙個用來交換的暫存單元,也算是非常的不錯。不過由於記錄的比較與交換是跳躍式進行,因此堆排序也是一種不穩定的排序方法。

另外,由於初始構建堆所需的比較次數較多,因此,它並不適合待排序序列個數較少的情況。 

出處:

《大話資料結構》第9章 排序 9 7 堆排序(上)

我們前面講到簡單選擇排序,它在待排序的n個記錄中選擇乙個最小的記錄需要比較n 1次。本來這也可以理解,查詢第乙個資料需要比較這麼多次正常的,否則如何知道它是最小的記錄。可惜的是,這樣的操作並沒有把每一趟的比較結果儲存下來,在後一趟的比較中,有許多比較在前一趟已經做過了,但由於前一趟排序時未儲存這些比...

第9章 資料結構

第9章 資料結構 my frames,button my sub of 日誌查詢 push button2 溫金簡訊查詢 push button3 中均簡訊查詢 push button4 機器資訊查詢 push button5,ip資訊查詢 push button6,裝置資訊維護 push butt...

第9章 資料結構

9.1.1 建立和訪問乙個兩維陣列 root wx03 4 cat a1.pl aoa fred barney george jane elroy homer marge bart print aoa 0 1 print n root wx03 4 perl a1.pl barney aoa 0 1...