堆排序的Python實現(附詳細過程圖和講解)

Candy_GL發表於2019-02-18

轉自:https://www.jianshu.com/p/d174f1862601

正文前的扯淡

之前電話面試一個公司時,面試官讓寫一個堆排序,遺憾的是我忘了堆排序的思想了,所以直接說不會寫,這次電面也以失敗告終...知恥後勇,這幾天在網上找了很多寫堆排序的帖子,但是帖子質量不好,堆排序是什麼不介紹,程式碼也非常不詳細,看了半天沒整明白,不過好在今天找出了資料結構課的課本,系統複習後,嘗試用Python寫出了一個堆排序。
 

目錄

  • 堆排序介紹
  • 堆排序演算法詳解+Python實現
     

堆排序涉及到的概念

  • 堆排序是利用 進行排序的
  • 是一種完全二叉樹
  • 有兩種型別: 大根堆 小根堆
  • 兩種型別的概念如下:
    大根堆:每個結點的值都大於或等於左右孩子結點
    小根堆:每個結點的值都小於或等於左右孩子結點
    因為比較抽象,所以專門花了兩個圖表示

     

    大根堆

     

    小根堆

那麼,什麼是完全二叉樹呢?

完全二叉樹 是 一種除了最後一層之外的其他每一層都被完全填充,並且所有結點都保持向左對齊的樹,向左對齊指的是:

向左對齊的完全二叉樹


像這樣的樹就不是完全二叉樹:

image.png


如果給上面的大小根堆的根節點從1開始編號,則滿足下面關係(下圖就滿足這個關係):

滿足關係


如果把這些數字放入陣列中,則如下圖所示:其中,上面的數字是陣列下標值,第一個元素佔位用。

陣列中的大根堆

 

堆排序演算法詳解+Python實現

瞭解了堆。下面我們來看下堆排序的思想是怎樣的(以大根堆為例):

  • 首先將待排序的陣列構造出一個大根堆
  • 取出這個大根堆的堆頂節點(最大值),與堆的最下最右的元素進行交換,然後把剩下的元素再構造出一個大根堆
  • 重複第二步,直到這個大根堆的長度為1,此時完成排序。

下面通過圖片來看下,第二個步驟是如何進行的:

首先把2和9的位置互換

互換位置後把2的位置進行調整,重新構造出一個大根堆

 

構造結果如下,這就選出了一個元素,然後再把10和80的位置互換,繼續進行上面的步驟

這就是構建大根堆的思想,瞭解了之後就可以進行編碼,編碼主要解決兩個問題:

  • 如何把一個序列構造出一個大根堆
  • 輸出堆頂元素後,如何使剩下的元素構造出一個大根堆

根據問題進行編碼,由於陣列下標是從0開始的,而樹的節點從1開始,我們還需要引入一個輔助位置,Python提供的原始資料型別list實際上是一個線性表(Array),由於我們需要在序列最左邊追加一個輔助位,線性表這樣做的話開銷很大,需要把陣列整體向右移動,所以list型別沒有提供形如appendleft的函式,但是在一個連結串列裡做這種操作就很簡單了,Python的collections庫裡提供了連結串列結構deque,我們先使用它初始化一個無序序列:

from collections import deque
L = deque([50, 16, 30, 10, 60,  90,  2, 80, 70])
L.appendleft(0)

此時L如下:

In [2]: L
Out[2]: deque([0, 50, 16, 30, 10, 60, 90, 2, 80, 70])

根據我們上面找出的兩個難點,可以先編出heap_sort函式:

def heap_sort(L):
    L_length = len(L) - 1

    first_sort_count = L_length / 2
    for i in range(first_sort_count):
        heap_adjust(L, first_sort_count - i, L_length)

    for i in range(L_length - 1):
        L = swap_param(L, 1, L_length - i)
        heap_adjust(L, 1, L_length - i - 1)

    return [L[i] for i in range(1, len(L))]

講解:

  • 因為引入了一個輔助空間,所以使L_length = len(L) - 1
  • 第一個迴圈做的事情是把序列調整為一個大根堆(heap_adjust函式)
  • 第二個迴圈是把堆頂元素和堆末尾的元素交換(swap_param函式),然後把剩下的元素調整為一個大根堆(heap_adjust函式)

我們要排序的序列為deque([50, 16, 30, 10, 60, 90, 2, 80, 70]),但是在第一個迴圈中,我們用了一個輔助變數first_sort_count,迴圈時,這個值變化的順序是4->3->2->1,這是為什麼呢。實際上,這些數字代表的是有孩子的節點,從下圖可以看出,而我們所謂的調整大根堆,其實就是按照從右往左,從下到上的順序,把每顆小樹調整為一個大根堆。4->3->2->1的調整,其實就是10->30->16->50的調整。

節點含義

swap_param函式很簡單,我們根據Python的特點,無需引入中間變數,直接交換堆頂元素和最後元素即可,程式碼如下:

def swap_param(L, i, j):
    L[i], L[j] = L[j], L[i]
    return L



下面讓我們看下最關鍵的堆調整函式heap_adjust

def heap_adjust(L, start, end):
    temp = L[start]

    i = start
    j = 2 * i

    while j <= end:
        if (j < end) and (L[j] < L[j + 1]):
            j += 1
        if temp < L[j]:
            L[i] = L[j]
            i = j
            j = 2 * i
        else:
            break
    L[i] = temp

這段程式碼比較抽象,我們結合實際例子把自己想象成一個直譯器來看一下:

 

處理過程

  • 第一個迴圈在第一個呼叫這個函式時,start=4, end=9,L=[0, 50, 16, 30, 10, 60, 90, 2, 80, 70],進行temp = L[start],實際就是temp=L[4]=10,i=start, i此時為4,拿到我們要處理的樹節點,j = 2*i,j此時得到第四個節點的左子樹座標,接著開始迴圈,迴圈條件j <= end代表在調整完整棵樹樹之前一直進行迴圈。第一個條件if (j < end) and (L[j] < L[j + 1])是要保證 j 取到較大子樹的座標,由於左子樹大於右子樹,所以這個if表示式不進行。


    第二個if 表示式,要做的如果根節點小於子樹的值,就把根節點和較大的子樹的值進行交換,temp<L[j]10<80,所以執行if內的語句:L[i] = L[j] 執行後L[i]為80i = j 執行後i=8j = 2 * i,執行後j為16,此時不滿足迴圈條件,退出迴圈,然後執行L[i] = temp,執行後L[i] = 10
    這個函式其實就是把每個子樹的根節點和較大的子節點進行值交換。而且如果在左子樹 依然是根節點的情況下繼續進行調整。 讀者可以自己照著圖調整幾次就可以很好的理解程式碼的含義了。

這樣調整4次後,這棵樹就變成了一個大根堆,此時序列變成了這樣:

 

第一個迴圈之後的序列

接下來進行第二個迴圈。

for i in range(L_length - 1):
    L = swap_param(L, 1, L_length - i)
    heap_adjust(L, 1, L_length - i - 1)

首先L = swap_param(L, 1, L_length - i)交換第一個節點和最後一個節點的值(因為我們引入了一個輔助空間,所以序列長度減1),此時序列變成了[16, 80, 50, 70, 60, 30, 2, 10, 90] 接下來對[16, 80, 50, 70, 60, 30, 2, 10]進行調整,由於我們之前已經把序列調整為了大根堆,所以此時迴圈條件變為從堆頂進行小範圍調整就可以。
這次調整後,堆變為:

調整的過程

 

調整的結果


然後繼續把10和80進行交換,繼續調整,直到遍歷完整個序列為止。

 

完整程式碼如下:


from collections import deque


def swap_param(L, i, j):
    L[i], L[j] = L[j], L[i]
    return L


def heap_adjust(L, start, end):
    temp = L[start]

    i = start
    j = 2 * i

    while j <= end:
        if (j < end) and (L[j] < L[j + 1]):
            j += 1
        if temp < L[j]:
            L[i] = L[j]
            i = j
            j = 2 * i
        else:
            break
    L[i] = temp


def heap_sort(L):
    L_length = len(L) - 1

    first_sort_count = L_length / 2
    for i in range(first_sort_count):
        heap_adjust(L, first_sort_count - i, L_length)

    for i in range(L_length - 1):
        L = swap_param(L, 1, L_length - i)
        heap_adjust(L, 1, L_length - i - 1)

    return [L[i] for i in range(1, len(L))]


def main():
    L = deque([50, 16, 30, 10, 60,  90,  2, 80, 70])
    L.appendleft(0)
    print heap_sort(L)


if __name__ == '__main__':
    main()

執行結果如下:

python heap_sort2.py
[2, 10, 16, 30, 50, 60, 70, 80, 90]



作者:一根薯條
連結:https://www.jianshu.com/p/d174f1862601
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。

相關文章