堆排序的Python實現(附詳細過程圖和講解)
轉自: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]為80,i = j
執行後i=8,j = 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
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。
相關文章
- 超詳細講解頁面載入過程
- 入門 | Tensorflow實戰講解神經網路搭建詳細過程神經網路
- vue原始碼解析-圖解diff詳細過程Vue原始碼圖解
- 詳細分析棧和佇列的資料結構的實現過程(Java 實現)佇列資料結構Java
- 【Node】詳解模組的實現過程
- DeFi和CeFi的區別詳細講解
- 詳細分析連結串列的資料結構的實現過程(Java 實現)資料結構Java
- 幾大排序演算法的理解和程式碼實現(超級詳細的過程)排序演算法
- python協程詳細解釋以及例子Python
- 指標的詳細講解指標
- 超詳細的ArrayList擴容過程(配合原始碼詳解)原始碼
- 詳細瞭解 synchronized 鎖升級過程synchronized
- Java 動態代理原理圖解 (附:2種實現方式詳細對比)Java圖解
- EventBus 3.0+ 原始碼詳解(史上最詳細圖文講解)原始碼
- 泊松過程的詳細理解
- dart類詳細講解Dart
- [轉載] Python中協程的詳細用法和例子Python
- 堆排序詳解排序
- react的詳細知識講解!React
- Java中的static詳細講解Java
- Python實現快遞分揀小程式(附原始碼和超詳細註釋)Python原始碼
- nginx配置https詳細過程NginxHTTP
- MySQL MHA詳細搭建過程MySql
- 詳解布隆過濾器的原理和實現過濾器
- 第二十節:詳細講解String和StringBuffer和StringBuilder的使用UI
- Go Struct超詳細講解GoStruct
- Q-Q圖原理詳解及Python實現Python
- Python 內建logging 使用詳細講Python
- Linux基本命令詳細講解和擴充套件Linux套件
- centos7安裝的詳細過程CentOS
- 2、超詳細的域滲透過程
- 使用JavaScript和Python實現Oracle資料庫的儲存過程?JavaScriptPythonOracle資料庫儲存過程
- MapReduce過程詳解
- 這是我見過的最詳細的Linux系統結構講解!Linux
- linux下安裝zsh和p10k的詳細過程Linux
- MyBatis-Plus詳細講解(一)MyBatis
- 詳細講解函式呼叫原理函式
- GPIO口工作原理的超詳細解釋(附電路圖)