手把手详解堆排序,堆就这么难懂?没有人看一遍学不会的,如果学不会,那就两遍吧
1. 先驗知識
堆排序,是使用一種 堆 數據結構的排序算法。在了解堆排序前,建議先掌握二叉樹相關知識,不然很費勁,很迷茫。
好了,話不多說。接下來先講一講 堆 這種數據結構的特點。主要有兩點:①堆是一顆完全二叉樹;②堆有大頂堆和小頂堆之分。
1.1 滿二叉樹
在講完全二叉樹之前,需要先了解一下 滿二叉樹,滿二叉樹就是一顆二叉樹,每一層的節點都是“滿”的,如下圖。
?
1.2 完全二叉樹
將樹中的節點從上至下,從左至右順序編號,如果和滿二叉樹相同編號節點的位置相同,那么就是完全二叉樹,如下圖。
?
1.3 大頂堆和小頂堆
大頂堆,很明顯就是頂點數據比較大的堆;小頂堆則是頂點數據比較小的堆。話不多說,看圖。
?
2. 堆排序
好了,第一節費了點時間,如果還不夠詳細的地方,可以評論或者自己網頁搜索一下哈,再不進入主題,黃花菜都涼了。不知道大家根據前面講解的大頂堆和小頂堆得到什么結論沒有?如果沒有結論,那我這里幫你總結一下,主要有兩點:①大頂堆和小頂堆從上至下,從左至右不是完全有序的,如上面大頂堆掃描得到 {34, 18, 23, 12, 8};小頂堆得到 {3, 9, 7, 9, 12};②大頂堆的根節點是最大值,小頂堆的根節點是最小值。
根據大頂堆的根節點是最大值這個特性,我們可以通過得到大頂堆,取出根節點,然后循環調整大頂堆取出根節點即可得到有序數列。同理,小頂堆也可以。一般大頂堆適用于升序排序,小頂堆適用于降序排序,不是很好分析為什么,下面走一遍你就會體會到,如果沒有體會,我們后面再分析;
2.1 堆排序原理
?以大頂堆對數組進行升序排序為例。
① 將待排序數組調整為大頂堆;
② 此時根節點是最大值,交換根節點(數組首元素)和末尾節點(數組待排序元素的最后一個元素,示例會詳細說明);
③ 因為根節點和末尾節點交換了數據,需要重新對根節點開始遞歸調整,其它節點沒有變化不需要調整;
④ 重復第②和③步驟,直到最后剩下一個待排序元素。
到這是不是有點理解堆排序的原理,什么?還是一臉懵!那就只能開始分析示例了,接下來我們實際走一遍堆排序的流程。
?
2.2 堆排序示例
假設初始無序數組arr =?{3, 9, 13, 7, 1, 16, 3, 11},根據完全二叉樹的構建規則,得到數組對應的完全二叉樹形式如下。
?
2.2.1 將數組調整為大頂堆
① 得先將該數組調整為大頂堆形式對應的數組,怎么將完全二叉樹調整為大頂堆呢?我們知道大頂堆的節點值要大于或等于子節點值,所以我們得從樹的最底層開始,慢慢地把數值大的節點往上放,最終最大值放在根節點。
② 我們從哪個節點開始調整呢?對于每一個節點,它要和子節點比較,判斷是否小于子節點,如果小于子節點則交換,否則不調整。所以我們從擁有子節點的節點開始,也就是非葉子節點。
③ 通過完全二叉樹的結構圖,我們知道是從 第三層的7 開始調整。但如果是寫代碼,那我們怎么“確定”是 第三層的7 呢?
④ 得益于完全二叉樹的結構,下標為index的節點,它的父節點下標是 (index - 1) / 2,左子節點下標是 2 * index + 1,右子節點下標是 2 * index + 2;完全二叉樹的最后一個節點,它肯定沒有子節點,那么我們可以有個策略:從完全二叉樹的最后一個節點往前遍歷,每次遍歷一個節點,我們就尋找它的父節點,然后父節點和左右子節點進行比較、調整,直到根節點。這樣最終我們就能得到一個大頂堆。
⑤ 第一次,我們從完全二叉樹最后一個節點開始,它在數組的下標是 arr.length - 1 = 7,它的父節點在數組arr中的下標可推算得?(arr.length - 1 - 1) / 2 = 3,剛好是完全二叉樹 第三層的7 的編號。
⑤ 完全二叉樹 第三層的7 小于左子節點 第四層的11,也就是數組 arr[3]?< arr[2 * 3 + 1];所以交換,得到 arr = {3, 9, 13, 11, 1, 16, 3, 7},對應的完全二叉樹如下
?
⑥ 第二次,我們從完全二叉樹倒數第二個節點分析,也就是數組索引往前,指向 arr.length - 2 = 6,它的父節點在數組下標可推算得 (arr.length - 2 -1) / 2 = 2,指向完全二叉樹?第二層的13。
⑦ 由于完全二叉樹 第二層的13 小于左子節點 第三層的16,也就是 arr[2] < arr[2 * 2 + 1]?,交換得 arr = {3, 9, 16, 11, 1, 13, 3, 7},對應的完全二叉樹如下
?
⑧ 接下來同樣處理即可,就不再具體描述,以下只做簡單分析。
⑨ 第三次,完全二叉樹倒數第三個節點 第三層的13 ,也就是數組索引為 arr.length - 3 = 5,該節點的父節點 第二層16 ,數組的下標是 (arr.length - 3 - 1) / 2 = 2。滿足條件不需要調整。
⑩ 第四次,完全二叉樹倒數第四個節點 第三層的1?,也就是數組索引為 arr.length - 4?= 4,該節點的父節點是 第二層9?,數組的下標是 (arr.length - 4 - 1) / 2 = 1。
11. 由于 第二層的9 小于左子節點 第三層的11, 也就是 arr[1] < arr[1 * 2 + 1],進行交換;交換完之后,得到 arr = {3, 11, 16, 9, 1, 13, 3, 7},對應的完全二叉樹如下。
?
12. 此時還應遞歸判斷完全二叉樹剛交換完得到的 第三層的9 是不是符合大頂堆要求,我們這個示例剛好符合,則結束遞歸,否則要遞歸判斷并調整。
13.?第五次,完全二叉樹倒數第五個節點 第三層的9?,也就是數組索引為 arr.length - 5?= 3,該節點的父節點是 第二層11?,數組的下標是 (arr.length - 5 - 1) / 2 = 1。滿足條件不需要調整。
14.?第六次,完全二叉樹倒數第六個節點 第二層的16?,也就是數組索引為 arr.length - 6?= 2,該節點的父節點是 根節點3?,數組的下標是 (arr.length - 6 - 1) / 2 = 0。
15. 由于 根節點3?小于右子節點 第二層的16, 也就是 arr[0] < arr[0 * 2 + 2],進行交換;交換完之后,得到 arr = {16, 11, 3, 9, 1, 13, 3, 7},對應的完全二叉樹如下。
?
16. 此時還應遞歸判斷完全二叉樹剛交換完得到的 第二層的3 是不是符合大頂堆要求,我們發現節點 第二層的3 小于左子節點 第三層13,也就是 arr[0 * 2 + 2] < arr[(0 * 2 + 2) * 2 + 1],進行交換。
17. 交換后得到 arr = {16, 11, 13, 9, 1, 3, 3, 7},對應的完全二叉樹如下。
?
18. 遞歸判斷剛交換完的 第三層3 是不是符合大頂堆要求,我們發現它沒有子節點,并且推算的子節點下標超出了數組的末尾索引,結束遞歸。
19. 因為我們剛剛調整的就是根節點,所以結束掃描。最終大頂堆結構圖如上圖所示,數組?arr = {16, 11, 13, 9, 1, 3, 3, 7}
至此,我們完成了一次數組調整為大頂堆的流程。
?
但是上面的分析是從最后一個節點慢慢往前掃描、調整,效率有點低。但事實上由于完全二叉樹的特點,我們可以直接從最后一個節點(一定是葉子節點)的父節點(這個節點是完全二叉樹的最后一個非葉子節點,它后面的節點都是葉子節點,不用調整)開始往前掃描、調整??梢宰屑毸伎?、理解一下,我就不詳細解釋了。
?
2.2.2 將大頂堆的根節點與末尾節點進行交換
在進行了2.2.1之后,我們得到了大頂堆,此時我們需要保存大頂堆根節點(最大值),保存之后,我們還需要對剩下的數據進行調整得到大頂堆,再保存大頂堆根節點,周而復始,直到最后只有一個元素,此時不需要再排序,結束。問題來了,每次保存大頂堆根節點很簡單,我們可以新建一個數組,然后依次將數據保存在該數組即可。我們同時需要將該根節點從數組中“刨除”,然后對剩余數據繼續調整為大頂堆。如我們上面得到的數組為{16, 11, 13, 9, 1, 3, 3, 7},如果把數組首元素“刨除”,也就是“刨除”大頂堆根節點。那么我們接下來需要對剩余元素{11, 13, 9, 1, 3, 3, 7}進行排序,我們需要重新構建完全二叉樹,并且調整為大頂堆,然后保存大頂堆根節點,然后再將該元素“刨除”,周而復始對剩余數據構建、調整。
?
但是這樣有點復雜,效率不夠高,因為我們第一次得到的大頂堆是大致有序的,如果后續每次都要“刨除”根節點,重新構建、調整的話,效率大打折扣。所以這里有一個操作:直接將數組首元素和數組末尾元素進行交換。這樣交換之后,我們只對數組下標為?0——arr.length-2 的元素進行調整,并且只需要對大頂堆的根節點遞歸調整即可,因為其它的節點經過第一次調整已經滿足大頂堆的需求,這樣既簡單又高效。
?
2.3 堆排序的Java代碼
分析了這么多,終于來到了碼代碼階段,以下是Java的堆排序代碼、相關代碼解釋以及排序效果,如下圖。
?
總結
以上是生活随笔為你收集整理的手把手详解堆排序,堆就这么难懂?没有人看一遍学不会的,如果学不会,那就两遍吧的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 八皇后问题的Java递归算法
- 下一篇: 线性结构(顺序存储和链式存储)和非线性结