上篇博客主要講了冒泡排序、插入排序、希爾排序以及選擇排序。本篇博客就來講一下堆排序(Heap Sort)。看到堆排序這個名字我們就應該知道這種排序方式的特點,就是利用堆來講我們的序列進行排序。“堆”其實就是一種有着特定結構的完全二叉樹,下方將會詳細的介紹一下堆。本篇博客講的就是堆排序,首先我們先對大頂堆,小丁堆進行介紹,然后構建堆,最后利用堆的特性對我們的數據序列進行排序。
下方我們依然是先給出相應內容的示意圖,然后給出相應的代碼實現,最后就是測試用例了。還是那句話,廢話少說,進入今天博客的主題。
一、堆
在本篇博客的第一部分,我們先聊一下什么什么是“堆”。在數據結構中的堆其實就是一顆“完全二叉樹”,不過此完全二叉樹有着一些特殊的規則,根據這些特殊的規則又可以將“堆”分為“大頂堆”和“小頂堆”。大頂堆的特點是該“完全二叉樹”的根節點比其左右節點都要大,而小頂堆與其相反,在“小頂堆”中根節點要比左右子節點的值都要小。下方詳細的介紹了“大頂堆”和“小頂堆”。
1、大頂堆
下方這示意圖就是大頂堆的規則示意圖,其根節點比起左右子節點都大。如果將“堆”的節點按照層次進行編號的話,假設根節點的編號為i(i > 0)的話,那么該根節點的左孩子的編號就為2i, 其右孩子的編號就為2i + 1。那么根據大頂堆的特點,我們很容易就得出k(i) >= k(2i)和k(i) >= k(2i + 1)。根據此特點我們又很容易得出在大頂堆中的根節點是完全二叉樹中最大的那個節點。
根據上述特點,下方是我們構建的“大頂堆”,如下所示。在大頂堆中,如果我們隊大頂堆進行層次遍歷的話,層次遍歷序列的第一個值肯定是所有序列中最大的那個值。
2、小頂堆
與大頂堆相反,小頂堆則是左右孩子都比根節點大的完全二叉樹。與大頂堆規則類似,在小頂堆中k(i)<=k(2i), k(i) <=k(2i+1)(i > 0)。
根據上述特點,我們很容易的就給出了小頂堆的結構如下所示。如果我們隊小頂堆進行層次遍歷的話,層次遍歷序列的第一個值肯定是所有序列中最小的那個值。
二、大頂堆的構建
接下來我們要對[62, 88, 58, 47, 62, 35, 73, 51, 99, 37, 93]進行堆排序,在排序之前,我們需要將該序列構建成大頂堆。更確切的說是根據k(i) >= k(2i)和k(i) >= k(2i + 1)這個規則把該序列轉換成大頂堆層次遍歷的序列。進一步說,假如大頂堆層次遍歷的序列為list, 如果下標是從1開始的話,那么肯定有list[i] > list[2i], list[i]>list[2i + 1](i > 0)這個規則。我們就可以通過這個規則將[62, 88, 58, 47, 62, 35, 73, 51, 99, 37, 93]此序列轉換成大頂堆的層次遍歷的序列。下方我們會詳細的給出方案。
1.大頂堆構建的示意圖
接下來我們將通過示意圖的方式來聊一下如何將[62, 88, 58, 47, 62, 35, 73, 51, 99, 37, 93]轉換成大頂堆的層次遍歷的序列。首先我們先將上述序列從左往右存入完全二叉樹中,如下所示。換一種方法來說,上述要排序的序列,也就是下方完全二叉樹層次遍歷的結果。
2、“大頂堆”的轉換
大頂堆的構建是從下往上進行調整的,確切的說是從局部到整體的來進行大頂堆的創建。在構建大頂堆的過程中,我們先從最小的子樹開始調整,然后慢慢的往外擴充。下方是整個過程的示意圖,下方會給出詳細的介紹。
-
(1)、位於“ 完全二叉樹” 最下方最小的子樹是以62為根節點的子樹,我們先對此子樹進行調整,將其調整成大頂堆。 我們先比較62的兩個子節點,比較后我們知道93是子節點中較大的那個。 然后62再和93進行比較,我們發現 93>62,將62與93交換。該子樹的大頂堆構建完畢。
-
(2)、以同樣的方式我們對以47為根節點的子樹和以58為根節點的子樹進行調整,將其調整為大頂堆。具體步驟如下方(2)、(3)所示。
-
(3)、子樹的范圍繼續擴大,接下來我們要調整根節點為88的子樹。 88的左右子樹都是大頂堆,但是88為根節點的子樹不是大頂堆,我們需要從下方的子樹中找到88應該在的位置,使其成為大頂堆。 88與其較大的子節點99比較, 因為99>88將其進行交換。交換完畢后, 88的子節點為51和47。88>51,不需要交換,此刻該子樹的大頂堆構建完畢。
-
(4)、同上一步,我們對整棵樹進行調整,最終大頂堆構建完畢。
3.代碼實現
上述步驟如果理解后,在再給出相應的代碼實現並不困難。雖然上面是使用的完全二叉樹進行表示的,但是我們在真正進行堆排序的時候並不會用到上述的完全二叉樹的結構。僅僅用到了大頂堆層次遍歷的序列。所以我們只需要將需要排序的數組根據k(i) >= k(2i)和k(i) >= k(2i + 1)這個規則把該序列轉換成大頂堆層次遍歷的序列即可。下方就是相應的代碼實現。
下方截圖中的兩個函數就是構建大頂堆層次遍歷序列的函數。heapCreate()函數就負責將傳入的數組轉換成大頂堆層次遍歷的結構。heapAdjast()方法就負責對子樹進行調整。具體代碼如下所示:
三、堆排序的實現
上面我們將無序的序列轉換成了“大頂堆”的層次遍歷的結果。接下來我們就要利用大頂堆來進行排序了。本部分將會給出堆排序的詳細示意圖,然后再根據這些示意圖給出相應的代碼實現和運行結果。詳細內容如下所示:
1、堆排示意圖
下方是對“大頂堆”進行的排序,排序后,我們的大頂堆會變成小頂堆,而這個“小頂堆”的層次遍歷就是有序的。下方這個示意圖就是堆排完整的過程。其實下方的步驟可以總結為下方的兩步:
-
將大頂堆的第一個值(整個序列中最大的那個值)與大頂堆最后一個值進行交換。
-
交換后,最后一個值為整個序列中最大值,將此值從大頂堆中剔除。然后將剩余的元素再次進行調整,將其調整為大頂堆。
下方這些示意圖其實就是上述兩個步驟的不斷循環,具體如下所示。
2、調整大頂堆的代碼實現
因為將大頂堆第一個值與最后一個值交換后,大頂堆的規則將會被打破,將不再是大頂堆。需要我們從上往下進行調整,上述示意圖的方框中的第二部分就是調整的過程。調整后,將會又成為一個新的大頂堆。下方就是調整的具體代碼實現,如下所示。
下方代碼的核心就是將新的根節點與子節點進行比較,若根節點比子節點中較大的那個節點要小,就要將兩者進行交換。重復這個過程,直到成為大頂堆為止。具體做法如下所示。下方這段代碼就是上面我們創建大頂堆的那段代碼,我們在堆排序的過程中,依然是調用下方的方法來進行大頂堆的調整。
3.堆排序的代碼實現
“大頂堆”的創建以及調整上面我們已經給出了相應的代碼實現。在上述代碼的基礎上,給出堆排序的代碼並不困難,下方就是堆排序的具體代碼實現。
在下方代碼中,首先我們將需要排序的序列調用heapCreate()方法將其轉換成“大頂堆”的層次遍歷的序列。然后將大頂堆的根節點與尾結點進行交換,交換后將大頂堆的長度減一,然后將縮減后的堆調用heapAdjust()進行調整,使其再次成為一個“大頂堆”。使用while不斷的循環交換和調整這個過程,知道“大頂堆”中的元素個數為零。具體代碼如下所示:
4、輸出結果
接下來我們就來看看上述代碼的運行結果,下方截圖中就是相應的運行結果。從下方結果中我們也能清楚的看到,堆排序其實就是不斷交換和調整的過程。
本篇博客對堆排序的介紹就先到這兒,下篇博客我們將會介紹“歸並排序”以及“快速排序”的詳細內容。本篇博客的相關代碼依然會在github上進行分享,下方是github分享地址,如下所示:
github代碼分享地址:https://github.com/lizelu/DataStruct-Swift/tree/master/AllKindsOfSort