堆排序
其他排序方法:選擇排序、冒泡排序、歸並排序、快速排序、插入排序、希爾排序、堆排序
概念
完全二叉樹
在講完全二叉樹之前,先引入完美二叉樹/滿二叉樹的概念。
每一個層的結點數都達到最大值的二叉樹就叫完美二叉樹。就像這樣:

而完全二叉樹的結點也像上圖的滿二叉樹那樣從上往下、從左到右進行編號的話,每個結點的位置都與滿二叉樹對應編號結點的位置相同。
也就是說,
如果最后一個葉子結點是其父親的右兒子,則除了葉子結點,每個結點必定有兩個兒子。
如果最后一個葉子結點是其父親的左兒子,則除了其父親與其它葉子結點,每個結點必定有兩個兒子。


堆
堆是一個數組。它滿足兩個特點:
- 完全二叉樹
- 任一結點的值都是其子樹所有結點的最大值①或最小值②
情況①為最大堆

②為最小堆。

我們這里主要講最大堆
存儲結構
堆和完全二叉樹我們通常用數組來存儲,

| 元素 | a | b | c | d | e | f | g | h | i |
| 下標 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
下標公式:
設父結點的下標為parent,左兒子的下標為leftChild,右兒子的下標為rightChild
\(parent = (leftChild-1)/2\) 或 \(\lfloor (rightChild-1)/2 \rfloor\)
即,$$parent = \lfloor (child-1)/2 \rfloor$$
\[leftChild = parent*2+1 \]
\[rightChild = parent*2+2 \]
思想
堆排序其實就是利用堆的第二個特點:任一結點的值都是其子樹所有結點的最大值或最小值。
只要將需要排序的數組建立成堆,然后每次取出根結點,就把剩下的結點調整成堆;再取出根結點,如此下去,最后便能得到排好序的數據。
性能
堆排序的性能比較復雜,我們先看堆的建立,堆的建立有兩種方法:
- 將元素一個一個地插入到空堆里,時間復雜度為O(NlogN)
- 將元素按照完全二叉樹的結構存放到數組里,然后再調整各結點的位置,時間復雜度為O(N)
建好堆之后,開始排序,排序也有兩種方法:
- 取出根結點的元素,把元素放進臨時的數組里,然后把剩下的結點調整成堆;重復前面的操作,最后臨時數組里的數據便排好序了
- 直接在堆內部排序。先將根結點與最后一個結點的元素互換,然后將最后一個結點排除在外,進行堆調整;重復前面的操作,最后便排好序了
兩種方法時間復雜度均為O(NlogN),但第一種方法需要額外O(N)空間來進行輔助排序。
代碼
# 建立最大堆
def buildMaxHeap(heap):
# 最后一個結點的下標
lastChild = len(heap) - 1
# 最后一個結點的父結點的下標
parent = lastChild - 1 >> 1
# 從最后一個結點的父結點開始往前遍歷結點
# 並將以所遍歷到的結點為根結點的堆調整為最大堆
while parent >= 0:
percDown(heap, parent, lastChild)
parent -= 1
# 將堆調整為最大堆
# 需要調整的堆的最后一個結點下標為lastChild
# 需要調整的堆的根結點下標為root
# percolate:過濾、滲透
def percDown(heap, root, lastChild):
if root < 0 or root >= lastChild: return
key = heap[root]
parent = root
# 只要parent有兒子,就繼續循環
while parent << 1 < lastChild:
# child指向parent的左兒子(parent*2+1)
child = parent << 1 | 1
# 如果右兒子比左兒子大,則child指向右兒子
if child < lastChild and heap[child + 1] > heap[child]: child += 1
# 如果根結點的值比兒子結點要小,則下濾
if key < heap[child]:
heap[parent] = heap[child]
# 否則,位置適合,結束循環
else:
break
# parent指向子結點,,調整以child為根的子堆
parent = child
# 如果發生了下濾,則將根結點的值移到合適的位置
if parent != root:
heap[parent] = key
# 返回最大堆的根結點元素,並將剩下結點調整為堆
def maxHeapPop(heap):
# 最后一個結點的下標
lastChild = len(heap) - 1
if lastChild < 0: return
# 取出根結點元素,將最后一個結點元素移到根結點
maxItem, heap[0] = heap[0], heap[lastChild]
# 堆元素少了一個
lastChild -= 1
heap.pop()
# 將剩下結點調整為堆
percDown(heap, 0, lastChild)
return maxItem
方法一排序
# 堆排序
def heapSort1(heap):
tmpHeap = heap.copy()
# 建立最大堆
buildMaxHeap(tmpHeap)
# 取出根結點的元素,把元素放進臨時的數組里,然后把剩下的結點調整成堆
for i in range(len(heap) - 1, -1, -1):
heap[i] = maxHeapPop(tmpHeap)
方法二排序
# 堆排序
def heapSort2(heap):
# 建立最大堆
buildMaxHeap(heap)
# 最后一個結點的下標
lastChild = len(heap) - 1
# 將最大值與最后一個結點的元素位置互換,然后將最后一個結點排除在外,進行堆調整;一直重復這一步,直到只剩一個根結點
while lastChild > 0:
heap[0], heap[lastChild] = heap[lastChild], heap[0]
lastChild -= 1
percDown(heap, 0, lastChild)
