堆排序
在堆排序中,我們可以將順序表看成一顆完全的二叉樹
二叉樹知識回顧:
從1開始對二叉樹中的每個節點順序編號
序列 : [ # , C, H, G, E, A, D, I, F, B, K ]
索引 1 2 3 4 5 6 7 8 9 10
所以編號為 i 的 左孩子節點編號為 2*i ; 右孩子節點編號為 2*i+ 1,
除了根節點,如果一個節點編號為 j 那麼���的雙親節點為 j//2
如果將序列R[1,n] 視作一個完全二叉樹,那麼 R[i] 的左孩子是R[2i] 右孩子則為R[2i+1]
堆:
n個值的序列K[1],K[2],K[3]……K[n] 稱之為堆。
該序列滿足一下特性
大根堆: K[i] >= K[2*i] 且 K[i] > = K[2*i +1] i 的範圍 1 ~ n//2
小根堆: K[i] <= K[2*i] 且 K[i] <= K[2*i +1]
如圖所示
小根堆
大根堆
堆排序的簡單過程是:
1、根據序列,構建一個初始堆 (大根堆/小根堆)
2、交換堆的第一個和最後一個元素。
3、排除最後一個元素,將序列第一個到倒數第二個重新整理成 堆。
4、再次交換堆的第一個和最後一個元素
5、重複 第3 步
6 、重複上述過程直至完成排序
再說簡單點就是:構造堆,交換元素、構造堆,交換元素,構造堆。。。。。。
從上述 6步的第三步——交換第一個和最後一個元素,而且該動作會在每次整理完堆之後,都會重複進行。所以我們就知道,序列的最後一個元素總是當前堆的最大/最小元素。所以,第一步,我們究竟是要構建大根堆還是小根堆,是要看我們打算順序排序序列還是要倒序排序序列。
在下面的例子中我們 順序排序 序列,也就說 構建 大根堆。
有這樣一個序列
A = [ –, 1, 0, 3, 6, 2, 8, 4, 9, 7, 5 ]
構建二叉樹,樹節點從 1 開始 編號。所以A[0] 為佔位作用。初始二叉樹如下:
開始構建大根堆。
1、從最後一個非葉子節點開始。比較 節點 和它的 最大 孩子節點,如果 如果節點大,那麼就保持不變。如果孩子節點大,那麼就交換孩子節點和該節點的值。
在本例中,最後一個非葉子節點是 序號為 5,值為 2 的節點。比較它和它的最大孩子節點。發現5 >2 所以交換他和他的孩子。
2、然後查看倒數第二個非葉子節點,也就是 序號為4 值為 6 的節點。同樣比較它和它的最大孩子節點。發現,6<9 也需要交換[下圖中紅色部分]
3、然後繼續查看倒數第三個非葉子節點,也就是 序號為 3 值為3 的節點,同樣比較它和它的最大孩子節點,發現 8>3 ,也需要交換[下圖中灰色部分]
4,然後繼續比較倒數第四個非葉子節點。即 序號為2 值為 0 的 節點。比較它和它的最大孩子節點 ,發現0<9,交換2者。。。。。 莫急,你再往下看會發現即使0 和 9 交換了,0還是比它現在的最大孩子節點(7)小,所以,還要繼續交換 0 和7 最終結果就是下圖:
5,接下來移動序號為1,值為1 的節點。。。重複上述過程,最終構造出的大根堆如下:
這就是完成構造初始堆的過程。函數如下:
def fixDown(a, i, n): # i 是指當前節點 n 為序列中所有元素的最大索引。二叉樹中 最後一個節點的 序號 while 2 * i <= n: # j = 2*i 即 當前節點的左孩子。它必須在所有節點的序號範圍內。如果j > n 了說明 i 是葉子節點。 j = 2 * i if j < n and a[j] < a[j + 1]: # 選出左右孩子節點中更大的那個。注意一:下面詳說。 j += 1 if a[i] < a[j]: # 如果雙親節點小於孩子節點,那麼交換雙親節點和子節點。同時向下移動 i 的位置。i 的範圍是[1-n//2] a[i], a[j] = a[j], a[i] i = j else: #如果雙親節點大於他的最大的孩子,那麼說明這個小的二叉樹構建堆完成。 break
View Code
注意一: 這條語句中使用了短路原則。即:如果 j < n 為假那麼就不再對後面的條件進行判斷。直接跳過該條件陳述式,進行下面的語句。為什麼在本條件陳述式中 要求 j != n,在本例中 j = 10,如果 條件陳述式改成 j<= n ,10 <=10,第一個條件成立了,那麼無疑會繼續驗證第二個條件是否成立,然而對照本例你會發現,根本沒有第11個節點,即a[j+1] 是不存在的。所以這會給程式引發一個超出索引的錯誤。
初始化堆的,同時也是整理堆的函數有了,那麼接下來就是在堆排序的主函數中調用這個函數,進行堆排序了,還是先用圖示範一下,再給代碼。
回顧一下上述的 堆排序的過程 。構造堆已經完成了,那麼就該交換第一個節點和最後一個節點了。然後,將最後一個節點排除,用剩下的節點再次構建堆。
重新構造堆如下:
交換第一個和最後一個如下:
重複上面的過程直至再也沒有可以構建堆的節點了。排序完成。
代碼如下:
def heapSort(A): n = len(A) - 1 # 因為加入了一個佔位元素,所以構建堆的實際元素個數應該 減去 1 for i in range(n // 2, 0, -1): #構建初始堆。 從最後一個非葉子節點開始構建堆。地板除 1是為了防止出現小數,2是取下界。 fixDown(A, i,n) # 調用上面的那個構造堆的函數,構造出初始化堆。 print('A=',A) #這裡可以看一下 初始化堆之後的序列的 各個元素的排列順序 while n > 1: # n 表示 堆的最後一個節點的序號 A[1], A[n] = A[n], A[1] # 交換堆的第一個和最後一個 節點 fixDown(A, 1, n-1) # 重新整理堆剩下的節點,再次構建 根堆 n -= 1 # 每次交換之後,已經排好序的節點就被排除出堆,所以堆的最後一個節點的向前移動一位。 return A[1:] #因為我們使用了佔位元素,所以返回從 所以為 1 及其以後得元素。A = [ '--', 1, 0, 3, 6, 2, 8, 4, 9, 7, 5 ] # 第一個元素不用,佔位print(heapSort(A))
View Code
這裡再說一下:
因為我們是從小到大排列 序列。所以每次都是讓 讓 A[i] 和他子節點中比較大的那個進行比較,然後大的上浮,小的下沉。從而構建大根堆
如果我們想要從大到小 逆序排列的話
那麼就要讓 A[i] 和它 的子節點中 較小的那個比較,小的上浮,大的下沉。
所以 構造堆的代碼如下:
def fixDown(a, i, n): # i 是指當前節點 n 為序列中所有元素的最大索引。二叉樹中 最後一個節點的 序號 while 2 * i <= n: # j = 2*i 即 當前節點的左孩子。它必須在所有節點的序號範圍內。如果j > n 了說明 i 是葉子節點。 j = 2 * i if j < n and a[j] > a[j + 1]: # 選出左右孩子節點中更 。。小。。 的那個 j += 1 if a[i] > a[j]: # 如果雙親節點。。 大於 。。孩子節點,那麼交換雙親節點和子節點。同時向下移動 i 的位置。i 的範圍是[1-n//2] a[i], a[j] = a[j], a[i] i = j else: #如果雙親節點。。小於。。他的。。最小。。的孩子,那麼說明這個小的二叉樹構建堆完成。 break