1. 程式人生 > >排序算法(四)堆排序的Python實現及算法詳解

排序算法(四)堆排序的Python實現及算法詳解

python 堆排序


一、前言


如果需要Java版本的堆排序或者堆排序的基礎知識——樹的概念,請參看本人博文《排序算法(二)堆排序》


關於選擇排序的問題


選擇排序最大的問題,就是不能知道待排序數據是否已經有序,比較了所有數據也沒有在比較中確定數據的順序。

堆排序對簡單選擇排序進行了改進。


二、準備知識


:它是一個完全二叉樹

大頂堆:每個非葉子結點都要大於或者等於其左右孩子結點的值稱為大頂堆


技術分享


小頂堆每個非葉子結點都要小於或者等於其左右孩子結點的值稱為小頂堆

技術分享



三、算法思路

堆排序大致可以分為下面幾個步驟:

1、構建完全二叉樹

將原始數據放入完全二叉樹中

2、構建大頂堆

需要選擇起點結點,選擇下一個結點,以及如何調整堆

3、排序

將堆頂數據依次拿走,生成排序的樹,最後用層序遍歷就可以難道所有的排序元素



四、算法實現


(一)構建完全二叉樹


待排序數字為 30,20,80,40,50,10,60,70,90

構建一個完全二叉樹存放數據,並根據性質5對元素編號,放入順序的數據結構中

構造一個列表為[0,30,20,80,40,50,10,60,70,90],用它來描述完全二叉樹

技術分享



(二)打印樹(輔助函數)


為了方便觀察,生成一個打印列表為樹結構的函數,方便觀察樹結點的變動,不屬於算法函數

為了適應不同的完全二叉樹,這個打印函數還需要特殊處理一下。


思路:

第一行取1個,第二行取2個,第三行取3個,以此類推

投影來思考一個類柵格系統,就可以很好的打印這個樹了


import math
def print_tree(array):
    ‘‘‘
        前空格元素間
    170
    237
    313
    4   01
    ‘‘‘
    index = 1
    depth = math.ceil(math.log2(len(array))) # 因為補0了,不然應該是math.ceil(math.log2(len(array)+1))
    sep = ‘  ‘
    for i in range(depth):
        offset = 2 ** i
        print(sep * (2 ** (depth - i - 1) - 1), end=‘‘)
        line = array[index:index + offset]
        for j, x in enumerate(line):
            print("{:>{}}".format(x, len(sep)), end=‘‘)
            interval = 0 if i == 0 else 2 ** (depth - i) - 1
            if j < len(line) - 1:
                print(sep * interval, end=‘‘)
        index += offset
        print()
print_tree([0, 30, 20, 80, 40, 50, 10, 60, 70, 90, 22])
print_tree([0, 30, 20, 80, 40, 50, 10, 60, 70, 90, 22, 33, 44, 55, 66, 77])
print_tree([0, 30, 20, 80, 40, 50, 10, 60, 70, 90, 22, 33, 44, 55, 66, 77, 88, 99, 11])



(三)構建大頂堆


核心算法

對於堆排序的核心算法就是堆結點的調整

1. 度數為2的結點A,如果它的左右孩子結點的最大值比它大的,將這個最大值和該結點交換

2. 度數為1的結點A,如果它的左孩子的值大於它,則交換

3. 如果結點A被交換到新的位置,還需要和其孩子結點重復上面的過程


核心算法實現如下:

# 為了和編碼對應,增加一個無用的0在首位
origin = [0, 30, 20, 80, 40, 50, 10, 60, 70, 90]
total = len(origin) - 1  # 初始待排序元素個數,即n
print(origin)
print_tree(origin)
def heap_adjust(n, i, array: list):
    ‘‘‘
    調整當前結點(核心算法)
    調整的結點的起點在n//2,保證所有調整的結點都有孩子結點
    :param n: 待比較數個數
    :param i: 當前結點的下標
    :param array: 待排序數據
    :return: None
    ‘‘‘
    while 2 * i <= n:
        # 孩子結點判斷 2i為左孩子,2i+1為右孩子
        lchile_index = 2 * i
        max_child_index = lchile_index  # n=2i
        if n > lchile_index and array[lchile_index + 1] > array[lchile_index]:  # n>2i說明還有右孩子
            max_child_index = lchile_index + 1  # n=2i+1
        # 和子樹的根結點比較
        if array[max_child_index] > array[i]:
            array[i], array[max_child_index] = array[max_child_index], array[i]
            i = max_child_index  # 被交換後,需要判斷是否還需要調整
        else:
            break
        # print_tree(array)
heap_adjust(total, total // 2, origin)
print(origin)
print_tree(origin)


到目前為止也只是解決了單個結點的調整,下面要使用循環來依次解決解決比起始結點編號小的結點。



起點的選擇

從最下層最右邊葉子結點的父結點開始

由於構造了一個前置的0,所以編號和列表的索引正好重合

但是,元素個數等於長度減1



下一個結點

按照二叉樹性質5編號的結點,從起點開始找編號逐個遞減的結點,直到編號1

# 構建大頂堆、大根堆
def max_heap(total,array:list):
    for i in range(total//2,0,-1):
        heap_adjust(total,i,array)
    return array
print_tree(max_heap(total,origin))



(四)排序


思路

1. 每次都要讓堆頂的元素和最後一個結點交換,然後排除最後一個元素,形成一個新的被破壞的堆。

2. 讓它重新調整,調整後,堆頂一定是最大的元素。

3. 再次重復第1、2步直至剩余一個元素

def sort(total, array:list):
    while total > 1:
        array[1], array[total] = array[total], array[1] # 堆頂和最後一個結點交換
        total -= 1
        heap_adjust(total,1,array)
    return array
print_tree(sort(total,origin))



改進

如果最後剩余2個元素的時候,如果後一個結點比堆頂大,就不用調整了。

def sort(total, array:list):
    while total > 1:
        array[1], array[total] = array[total], array[1] # 堆頂和最後一個結點交換
        total -= 1
        if total == 2 and array[total] >= array[total-1]:
            break
        heap_adjust(total,1,array)
    return array
print_tree(sort(total,origin))



五、算法分析


1、利用堆性質的一種選擇排序,在堆頂選出最大值或者最小值

2、時間復雜度

堆排序的時間復雜度為O(nlogn)

由於堆排序對原始記錄的排序狀態並不敏感,因此它無論是最好、最壞和平均時間復雜度均為O(nlogn)

3、空間復雜度

只是使用了一個交換用的空間,空間復雜度就是O(1)

4、穩定性

不穩定的排序算法



六、完整代碼


如果有需要,請自行將算法函數封裝成類。


import math
def print_tree(array):
    ‘‘‘
        前空格元素間
    170
    237
    313
    4   01
    ‘‘‘
    index = 1
    depth = math.ceil(math.log2(len(array))) # 因為補0了,不然應該是math.ceil(math.log2(len(array)+1))
    sep = ‘  ‘
    for i in range(depth):
        offset = 2 ** i
        print(sep * (2 ** (depth - i - 1) - 1), end=‘‘)
        line = array[index:index + offset]
        for j, x in enumerate(line):
            print("{:>{}}".format(x, len(sep)), end=‘‘)
            interval = 0 if i == 0 else 2 ** (depth - i) - 1
            if j < len(line) - 1:
                print(sep * interval, end=‘‘)
        index += offset
        print()
# Heap Sort
# 為了和編碼對應,增加一個無用的0在首位
origin = [0, 30, 20, 80, 40, 50, 10, 60, 70, 90]
total = len(origin) - 1  # 初始待排序元素個數,即n
print(origin)
print_tree(origin)
print("="*50)
def heap_adjust(n, i, array: list):
    ‘‘‘
    調整當前結點(核心算法)
    調整的結點的起點在n//2,保證所有調整的結點都有孩子結點
    :param n: 待比較數個數
    :param i: 當前結點的下標
    :param array: 待排序數據
    :return: None
    ‘‘‘
    while 2 * i <= n:
        # 孩子結點判斷 2i為左孩子,2i+1為右孩子
        lchile_index = 2 * i
        max_child_index = lchile_index  # n=2i
        if n > lchile_index and array[lchile_index + 1] > array[lchile_index]:  # n>2i說明還有右孩子
            max_child_index = lchile_index + 1  # n=2i+1
        # 和子樹的根結點比較
        if array[max_child_index] > array[i]:
            array[i], array[max_child_index] = array[max_child_index], array[i]
            i = max_child_index  # 被交換後,需要判斷是否還需要調整
        else:
            break
    # print_tree(array)
# 構建大頂堆、大根堆
def max_heap(total,array:list):
    for i in range(total//2,0,-1):
        heap_adjust(total,i,array)
    return array
print_tree(max_heap(total,origin))
print("="*50)
# 排序
def sort(total, array:list):
    while total > 1:
        array[1], array[total] = array[total], array[1] # 堆頂和最後一個結點交換
        total -= 1
        if total == 2 and array[total] >= array[total-1]:
            break
        heap_adjust(total,1,array)
    return array
print_tree(sort(total,origin))
print(origin)






本文出自 “終南山下” 博客,謝絕轉載!

排序算法(四)堆排序的Python實現及算法詳解