堆排序詳解


堆排序是很有難度的算法。搞懂之后就覺得,"還行吧"。

先講個故事: 周日學校有開個實習的招聘會,沒有拿到大公司offer的我,當然約上舍友走起啦。第一家,有人在面試了,那我就在旁邊聽下,只記得,"你會快排嗎? 堆排序呢? 現在你能寫出堆排序的算法??" 同為大三的面試者: "......"。

第二家,看了下,有招后台,好極了。

招python開發的嗎? 用啥框架?? 我用django的。很好,公司也有用django。我心里那個高興啊。后來,聊着聊着,不對勁。面試官話里透露着一股ds的氣息……我懷疑他還是學生……

他還說了公司的老板,竟然是我學校的老師,卧擦。我學校的老師竟然“兼職”去當老板了,心理多少不爽啊!! 畢竟,平時上課,教得太水了,上課浪費我時間,還老是點名。(當然少部分還是很好的)。后來換位思考,也就想通了。其實我也想當老板……誰都這樣吧。

-------------------------華麗分割線-------------------------

 

堆分為最大堆和最小堆,其實就是完全二叉樹。最大堆要求節點的元素都要不小於其孩子,最小堆要求節點元素都不大於其左右孩子,兩者對左右孩子的大小關系不做任何要求,其實很好理解。有了上面的定義,我們可以得知,處於最大堆的根節點的元素一定是這個堆中的最大值。

其實我們的堆排序算法就是抓住了堆的這一特點,每次都取堆頂的元素,將其放在序列最后面,然后將剩余的元素重新調整為最大堆,依次類推,最終得到排序的序列

 

其基本思想為(大頂堆)

  1. 將初始待排序關鍵字序列(R1,R2....Rn)構建成大頂堆,此堆為初始的無序區
  2. 堆頂元素R[1]與最后一個元素R[n]交換,此時得到新的無序區(R1,R2,......Rn-1)和新的有序區(Rn)
  3. 由於交換后新的堆頂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)
View Code

但是運行下,出錯了,下標越界!

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)
View Code

輸出:

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
View Code

 

初始化大頂堆后,已經要接近成功了。

此時需要交換堆頂與堆尾,但是問題來了,堆頂肯定是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)
View Code

運行結果:

[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]

 

時間復雜度:

空間復雜度:堆排序數據交換時需要一個輔助空間,故空間復雜度是O(1)
 
在構建堆(初始化大頂堆)的過程中,完全二叉樹從最下層最右邊的非終端結點開始構建,將它與其孩子進行比較和必要的互換,對於每個非終端結點來說,其實最多進行兩次比較和一次互換操作,因此整個構建堆的時間復雜度為: O(n)。大概需進行n/2 * 2 = n次比較和n/2次交換。
 
在正式排序時,n個結點的完全二叉樹的深度為⌊log2n⌋+1,並且有n個數據則需要取n-1次調整成大頂堆的操作,每次調整成大頂堆的時間復雜度為O(log2n)。因此,重建堆的時間復雜度可近似看做: O(nlogn)。
 
 
堆排序效果圖:
各種排序的穩定性,時間復雜度和空間復雜度總結:

上圖來自: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 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM