堆排序是很有難度的算法。搞懂之后就覺得,"還行吧"。
先講個故事: 周日學校有開個實習的招聘會,沒有拿到大公司offer的我,當然約上舍友走起啦。第一家,有人在面試了,那我就在旁邊聽下,只記得,"你會快排嗎? 堆排序呢? 現在你能寫出堆排序的算法??" 同為大三的面試者: "......"。
第二家,看了下,有招后台,好極了。
招python開發的嗎? 用啥框架?? 我用django的。很好,公司也有用django。我心里那個高興啊。后來,聊着聊着,不對勁。面試官話里透露着一股ds的氣息……我懷疑他還是學生……
他還說了公司的老板,竟然是我學校的老師,卧擦。我學校的老師竟然“兼職”去當老板了,心理多少不爽啊!! 畢竟,平時上課,教得太水了,上課浪費我時間,還老是點名。(當然少部分還是很好的)。后來換位思考,也就想通了。其實我也想當老板……誰都這樣吧。
-------------------------華麗分割線-------------------------
堆分為最大堆和最小堆,其實就是完全二叉樹。最大堆要求節點的元素都要不小於其孩子,最小堆要求節點元素都不大於其左右孩子,兩者對左右孩子的大小關系不做任何要求,其實很好理解。有了上面的定義,我們可以得知,處於最大堆的根節點的元素一定是這個堆中的最大值。
其實我們的堆排序算法就是抓住了堆的這一特點,每次都取堆頂的元素,將其放在序列最后面,然后將剩余的元素重新調整為最大堆,依次類推,最終得到排序的序列。
其基本思想為(大頂堆)
- 將初始待排序關鍵字序列(R1,R2....Rn)構建成大頂堆,此堆為初始的無序區
- 將堆頂元素R[1]與最后一個元素R[n]交換,此時得到新的無序區(R1,R2,......Rn-1)和新的有序區(Rn)
- 由於交換后新的堆頂R[1]可能違反堆的性質,因此需要對當前無序區(R1,R2,......Rn-1)調整為新堆,然后再次將R[1]與無序區最后一個元素交換,得到新的無序區(R1,R2....Rn-2)和新的有序區(Rn-1,Rn)。不斷重復此過程直到有序區的元素個數為n-1,則整個排序過程完成
下圖來張教材的圖,是整個堆排序的過程: 整個過程的核心就是先初始化大頂堆,將最大數(堆頂)的放到堆的最后一個, 堆長度-1, 繼續調整成大頂堆,直至有序序列為len(array_list)-1.

堆排序前42是在42后面,排序后42在42前面,因此堆排序是不穩定的。
下面舉例說明:
給定一個列表array=[16,7,3,20,17,8],對其進行堆排序。
首先根據該數組元素構建一個完全二叉樹,得到




20和16交換后導致16不滿足堆的性質,因此需重新調整
這樣就得到了初始堆。
第二步: 堆頂元素R[1]與最后一個元素R[n]交換,交換后堆長度減一

第三步: 重新調整堆。此時3位於堆頂不滿堆的性質,則需調整繼續調整(從頂點開始往下調整)


重復上面的步驟:








注意了,現在你應該了解堆排序的思想了,給你一串列表,你也能寫出&說出堆排序的過程。
在寫算法的過程中,剛開始我是很懵比。后來終於看懂了。請特別特別注意: 初始化大頂堆時 是從最后一個有子節點開始往上調整最大堆。而堆頂元素(最大數)與堆最后一個數交換后,需再次調整成大頂堆,此時是從上往下調整的。
不管是初始大頂堆的從下往上調整,還是堆頂堆尾元素交換,每次調整都是從父節點、左孩子節點、右孩子節點三者中選擇最大者跟父節點進行交換,交換之后都可能造成被交換的孩子節點不滿足堆的性質,因此每次交換之后要重新對被交換的孩子節點進行調整。我在算法中是用一個while循環來解決的
開始寫算法:
首先,我先初始化大頂堆:
1 def sift_down(array, start, end): 2 """
3 調整成大頂堆,初始堆時,從下往上;交換堆頂與堆尾后,從上往下調整 4 :param array: 列表的引用 5 :param start: 父結點 6 :param end: 結束的下標 7 :return: 無 8 """
9 # 當列表第一個是以下標0開始,結點下標為i,左孩子則為2*i+1,右孩子下標則為2*i+2;
10 # 若下標以1開始,左孩子則為2*i,右孩子則為2*i+1
11 left_child = 2*start + 1 # 左孩子的結點下標
12 # 當結點的右孩子存在,且大於結點的左孩子時
13 if left_child+1 <= end and array[left_child+1] > array[left_child]: 14 left_child += 1
15 if array[left_child] > array[start]: # 當左右孩子的最大值大於父結點時,則交換
16 temp = array[left_child] 17 array[left_child] = array[start] 18 array[start] = temp 19
20 print(">>", array) 21
22
23 def heap_sort(array): # 堆排序
24 # 先初始化大頂堆
25 first = len(array)//2 -1 # 最后一個有孩子的節點(//表示取整的意思)
26 # 第一個結點的下標為0,很多博客&課本教材是從下標1開始,無所謂吧,你隨意
27 for i in range(first, -1, -1): # 從最后一個有孩子的節點開始往上調整
28 print(array[i]) 29 sift_down(array, i, len(array)-1) # 初始化大頂堆
30
31 print("初始化大頂堆結果:", array) 32
33 if __name__ == "__main__": 34 array = [16, 7, 3, 20, 17, 8] 35 print(array) 36 heap_sort(array) 37 print(array)
看下運行結果,發現有問題:
[16, 7, 3, 20, 17, 8] 3 >> [16, 7, 8, 20, 17, 3] 7 >> [16, 20, 8, 7, 17, 3] 16 >> [20, 16, 8, 7, 17, 3] 初始化大頂堆結果: [20, 16, 8, 7, 17, 3] [20, 16, 8, 7, 17, 3]
上面代碼的過程如下面4張圖所示,但問題是初始化的大頂堆並不正確,當20與16交換后,算法並沒有繼續對以16為根結點的堆進行調整,導致17的右孩子,大於父結點16.




於是我改進了算法,每次子結點與父結點交換后,需將以子結點為根的完全二叉樹調整為大頂堆,當然,如果父結點大與左右孩子,就不需交換,當然與無須再調整為大頂堆。改進后算法如下:
1 def sift_down(array, start, end): 2 """ 3 調整成大頂堆,初始堆時,從下往上;交換堆頂與堆尾后,從上往下調整 4 :param array: 列表的引用 5 :param start: 父結點 6 :param end: 結束的下標 7 :return: 無 8 """ 9 while True: 10 # 當列表第一個是以下標0開始,結點下標為i,左孩子則為2*i+1,右孩子下標則為2*i+2; 11 # 若下標以1開始,左孩子則為2*i,右孩子則為2*i+1 12 left_child = 2*start + 1 # 左孩子的結點下標 13 # 當結點的右孩子存在,且大於結點的左孩子時 14 if left_child+1 <= end and array[left_child+1] > array[left_child]: 15 left_child += 1 16 if array[left_child] > array[start]: # 當左右孩子的最大值大於父結點時,則交換 17 temp = array[left_child] 18 array[left_child] = array[start] 19 array[start] = temp 20 21 start = left_child # 交換之后以交換子結點為根的堆可能不是大頂堆,需重新調整 22 else: # 若父結點大於左右孩子,則退出循環 23 break 24 25 print(">>", array) 26 27 28 def heap_sort(array): # 堆排序 29 # 先初始化大頂堆 30 first = len(array)//2 -1 # 最后一個有孩子的節點(//表示取整的意思) 31 # 第一個結點的下標為0,很多博客&課本教材是從下標1開始,無所謂吧,你隨意 32 for i in range(first, -1, -1): # 從最后一個有孩子的節點開始往上調整 33 print(array[i]) 34 sift_down(array, i, len(array)-1) # 初始化大頂堆 35 36 print("初始化大頂堆結果:", array) 37 38 if __name__ == "__main__": 39 array = [16, 7, 3, 20, 17, 8] 40 print(array) 41 heap_sort(array) 42 print(array)
但是運行下,出錯了,下標越界!
Traceback (most recent call last):
File "C:/Users/Administrator/PycharmProjects/laonanhai/編程/我的堆排序.py", line 42, in <module>
heap_sort(array)
File "C:/Users/Administrator/PycharmProjects/laonanhai/編程/我的堆排序.py", line 35, in heap_sort
[16, 7, 3, 20, 17, 8]
sift_down(array, i, len(array)-1) # 初始化大頂堆
3
File "C:/Users/Administrator/PycharmProjects/laonanhai/編程/我的堆排序.py", line 17, in sift_down
if array[left_child] > array[start]: # 當左右孩子的最大值大於父結點時,則交換
IndexError: list index out of range
>> [16, 7, 8, 20, 17, 3]
通過Debug知道為啥越界了

為了解決越界的問題,加個下標判定,輕松解決,oh year:
if left_child > end:
break
初始化大頂堆代碼:
1 def sift_down(array, start, end): 2 """ 3 調整成大頂堆,初始堆時,從下往上;交換堆頂與堆尾后,從上往下調整 4 :param array: 列表的引用 5 :param start: 父結點 6 :param end: 結束的下標 7 :return: 無 8 """ 9 while True: 10 11 # 當列表第一個是以下標0開始,結點下標為i,左孩子則為2*i+1,右孩子下標則為2*i+2; 12 # 若下標以1開始,左孩子則為2*i,右孩子則為2*i+1 13 left_child = 2*start + 1 # 左孩子的結點下標 14 # 當結點的右孩子存在,且大於結點的左孩子時 15 if left_child > end: 16 break 17 18 if left_child+1 <= end and array[left_child+1] > array[left_child]: 19 left_child += 1 20 if array[left_child] > array[start]: # 當左右孩子的最大值大於父結點時,則交換 21 temp = array[left_child] 22 array[left_child] = array[start] 23 array[start] = temp 24 25 start = left_child # 交換之后以交換子結點為根的堆可能不是大頂堆,需重新調整 26 else: # 若父結點大於左右孩子,則退出循環 27 break 28 29 print(">>", array) 30 31 32 def heap_sort(array): # 堆排序 33 # 先初始化大頂堆 34 first = len(array)//2 -1 # 最后一個有孩子的節點(//表示取整的意思) 35 # 第一個結點的下標為0,很多博客&課本教材是從下標1開始,無所謂吧,你隨意 36 for i in range(first, -1, -1): # 從最后一個有孩子的節點開始往上調整 37 print(array[i]) 38 sift_down(array, i, len(array)-1) # 初始化大頂堆 39 40 print("初始化大頂堆結果:", array) 41 42 if __name__ == "__main__": 43 array = [16, 7, 3, 20, 17, 8] 44 print(array) 45 heap_sort(array) 46 print(array)
輸出:
C:\Python34\python3.exe C:/Users/Administrator/PycharmProjects/laonanhai/編程/我的堆排序.py [16, 7, 3, 20, 17, 8] 3 >> [16, 7, 8, 20, 17, 3] 7 >> [16, 20, 8, 7, 17, 3] 16 >> [20, 16, 8, 7, 17, 3] >> [20, 17, 8, 7, 16, 3] 初始化大頂堆結果: [20, 17, 8, 7, 16, 3] [20, 17, 8, 7, 16, 3] Process finished with exit code 0
初始化大頂堆后,已經要接近成功了。
此時需要交換堆頂與堆尾,但是問題來了,堆頂肯定是array[0],但堆尾呢? 因為每次交換堆頂與堆尾后,堆尾下標是會變化的啊。
為了每次交換時都能找到堆尾,我用一個循環。
# 交換堆頂與堆尾
for head_end in range(len(array)-1, 0, -1): # start stop step
array[head_end], array[0] = swap(array[head_end], array[0]) # 交換堆頂與堆尾
交換堆頂與堆尾后,堆長度減一,且需從上往下調整成大頂堆。
# 交換堆頂與堆尾
for head_end in range(len(array)-1, 0, -1): # start stop step
array[head_end], array[0] = swap(array[head_end], array[0]) # 交換堆頂與堆尾
sift_down(array, 0, head_end-1) # 堆長度減一(head_end-1),再從上往下調整成大頂堆
自此,堆排序算法ending,你會了嗎? or 你會裝逼了嗎?
堆排序代碼:
1 def swap(a, b): # 將a,b交換 2 temp = a 3 a = b 4 b = temp 5 return a,b 6 7 def sift_down(array, start, end): 8 """ 9 調整成大頂堆,初始堆時,從下往上;交換堆頂與堆尾后,從上往下調整 10 :param array: 列表的引用 11 :param start: 父結點 12 :param end: 結束的下標 13 :return: 無 14 """ 15 while True: 16 17 # 當列表第一個是以下標0開始,結點下標為i,左孩子則為2*i+1,右孩子下標則為2*i+2; 18 # 若下標以1開始,左孩子則為2*i,右孩子則為2*i+1 19 left_child = 2*start + 1 # 左孩子的結點下標 20 # 當結點的右孩子存在,且大於結點的左孩子時 21 if left_child > end: 22 break 23 24 if left_child+1 <= end and array[left_child+1] > array[left_child]: 25 left_child += 1 26 if array[left_child] > array[start]: # 當左右孩子的最大值大於父結點時,則交換 27 array[left_child], array[start] = swap(array[left_child], array[start]) 28 29 start = left_child # 交換之后以交換子結點為根的堆可能不是大頂堆,需重新調整 30 else: # 若父結點大於左右孩子,則退出循環 31 break 32 33 print(">>", array) 34 35 36 def heap_sort(array): # 堆排序 37 # 先初始化大頂堆 38 first = len(array)//2 -1 # 最后一個有孩子的節點(//表示取整的意思) 39 # 第一個結點的下標為0,很多博客&課本教材是從下標1開始,無所謂吧,你隨意 40 for i in range(first, -1, -1): # 從最后一個有孩子的節點開始往上調整 41 print(array[i]) 42 sift_down(array, i, len(array)-1) # 初始化大頂堆 43 44 print("初始化大頂堆結果:", array) 45 # 交換堆頂與堆尾 46 for head_end in range(len(array)-1, 0, -1): # start stop step 47 array[head_end], array[0] = swap(array[head_end], array[0]) # 交換堆頂與堆尾 48 sift_down(array, 0, head_end-1) # 堆長度減一(head_end-1),再從上往下調整成大頂堆 49 50 51 52 if __name__ == "__main__": 53 array = [16, 7, 3, 20, 17, 8] 54 print(array) 55 heap_sort(array) 56 print("堆排序最終結果:", array)
運行結果:
[16, 7, 3, 20, 17, 8] 3 >> [16, 7, 8, 20, 17, 3] 7 >> [16, 20, 8, 7, 17, 3] 16 >> [20, 16, 8, 7, 17, 3] >> [20, 17, 8, 7, 16, 3] 初始化大頂堆結果: [20, 17, 8, 7, 16, 3] >> [17, 3, 8, 7, 16, 20] >> [17, 16, 8, 7, 3, 20] >> [16, 3, 8, 7, 17, 20] >> [16, 7, 8, 3, 17, 20] >> [8, 7, 3, 16, 17, 20] >> [7, 3, 8, 16, 17, 20] 堆排序最終結果: [3, 7, 8, 16, 17, 20]
時間復雜度:
上圖來自:http://blog.csdn.net/hguisu/article/details/7776068
參考博客:http://www.cnblogs.com/dolphin0520/archive/2011/10/06/2199741.html
這篇博客寫了好幾天了。轉發注明出處: http://www.cnblogs.com/0zcl/p/6737944.html
