Python算法之『 神祕的堆排序』

一、爲什麼說堆排序很“神祕”?

堆排序相較於其他常見的線性排序(冒泡、選擇、插入、快排等),它比較不一樣(我們不一樣,不一樣)。堆排序使用“堆”結構進行非線性排序,這裏的堆就是數據結構裏的二叉樹,確切的說應該是【完全二叉樹】。說到樹結構,可能會想到二分法的“分治”思想,難道跟快排有些許關係?不!快排是使用了二分法的思想,但是沒有使用二叉樹的組織結構,數據還是線性列表的;而堆排序則不同。

另外,在我們剛開始接觸算法的時候,可能大部分都是從前面說的幾種簡單的排序算法開始的,好理解,寫起來又簡單。堆排序可能就被冷落了一些。並且堆排序的邏輯比那幾種算法稍微複雜一點,從下面的代碼量就可以看出,比前面說的幾種算法多很多代碼。

 

二、堆排序的特性?

堆排序的時間複雜度:O(n) = n*log(n)

堆排序的穩定性:不穩定(假設在數列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;並且排序之後,a[i]仍然在a[j]前面。則這個排序算法是穩定的!)

 

三、堆排序的原理是怎樣的?

首先,我們需要理解什麼大根堆小根堆、父節點和子節點之間的索引關係

1、大根堆(大頂堆):在完全二叉樹的條件下,任意父節點的值都大於等於其左右子節點。

2、小根堆(小頂堆):在完全二叉樹的條件下,任意父節點的值都小於等於其左右子節點。

3、索引關係:

    若某一節點A的索引爲x時:

        A的左子節點索引爲:2*x+1

        A的右子節點索引爲:2*x+2

        A的父節點的索引爲:(x-1)//2  ('//'爲除法向下取整符號)

下圖是是數列到堆的映射關係:

 

本程序示例中使用的是大根堆方法,基本邏輯是:

1、把數列初始化成一個大根堆(或者小根堆)。

2、取出最大值(根節點值)放到數列最後。

3、把剩餘的數列重新調整爲一個大根堆。

4、重複第2、3步驟。

import numpy

def heap_sort_asc(arr):
    """
    堆排序(升序)。
    時間複雜度:n*log(n)
    穩定性:不穩定(假設在數列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;並且排序之後,a[i]仍然在a[j]前面。則這個排序算法是穩定的!)
    此處使用大根堆(即:對於任意節點,其孩子節點都不大於該節點)
    
    :param arr: 待排序的數列
    :return: 排好序的數列(升序)
    """
    arr_size = len(arr)
    # 把數列排列成大根堆形式
    set_big_heap(arr, arr_size)
    for i in range(arr_size): # 循環n次
        heap_size = arr_size - i
        # 交換。[0]處的數值是最大值,交換放到數列最後
        arr[0], arr[heap_size - 1] = arr[heap_size - 1], arr[0]
        # 把剩餘的數列元素重新調整成大根堆,最多循環log(n)次,即二叉樹的深度
        adjust_to_big_heap(arr, heap_size - 1)
    return arr
    

def set_big_heap(arr, size):
    """
    把一個數列處理成一個大根堆
    注意:若當前節點索引爲x,則:其父節點索引爲:(x-1)//2
    
    :param arr: 數列
    :param size: 要處理的數列元素數量
    :return:
    """
    # 遍歷每一個數據
    for i in range(size):
        this_index = i
        parent_index = (this_index - 1) // 2 # 父節點索引
        if parent_index < 0: # 沒有父節點了
            continue
        # 循環查找,直到當前節點不大於父節點
        while arr[this_index] > arr[parent_index]:
            # 交換當前節點和父節點
            arr[this_index], arr[parent_index] = arr[parent_index], arr[this_index]
            this_index = parent_index # 當前節點指向交換之前的父節點位置
            parent_index = (this_index - 1) // 2 # 計算父節點
            if parent_index < 0: # 找不到父節點,則退出循環
                break
    return arr

    
def adjust_to_big_heap(arr, size):
    """
    調整爲大根堆.
    該函數處理的堆有一個“異常值”,那就是根節點上的值,它是由堆的最大值(原根節點)和堆的最後一個節點值交換而來。
    所以這個函數的任務就是給這個“異常值”,尋找一個合適的家(位置)。
    注意:若當前節點索引爲x,則:
        其左孩子索引爲:2*x+1
        其右孩子索引爲:2*x+2
    
    :param arr: 是一個被替換掉跟節點的大根堆(此時,除了根節點,其他及節點都滿足大根堆的要求)。正因爲如此,所以需要重新調整爲大根堆
    :param size: 堆的大小,即在數組中的索引範圍[0, size)
    :return: 調整後的原數組
    """
    current_index = 0 # 最大的值的索引(因爲是大根堆,所以索引0的值最大)
    while current_index * 2 + 1 < size: # 有左孩子
        left_node_index = current_index*2+1
        
        # 尋找最大孩子節點
        max_child_index = left_node_index
        if left_node_index+1 < size: # 有右孩子
            if arr[left_node_index] < arr[left_node_index+1]:
                max_child_index = left_node_index+1
        # 孩子節點大於父節點(當前節點),則交換
        if arr[max_child_index] > arr[current_index]:
            arr[max_child_index], arr[current_index] = arr[current_index], arr[max_child_index]
            # 更新當前索引指向(當前索引一直指向最初那個根節點上的值,因爲只有這個值是“異常值”,需要給它尋找合適的位置)
            current_index = max_child_index
        else:
            break

    
if __name__ == '__main__':

    array = list(numpy.random.randint(0, 100, 20))
    print(array)
    a = heap_sort_asc(array)
    print(a)
排序前:[28, 93, 13, 51, 9, 97, 42, 8, 1, 57, 89, 10, 24, 73, 97, 24, 13, 41, 11, 1]
排序後:[1, 1, 8, 9, 10, 11, 13, 13, 24, 24, 28, 41, 42, 51, 57, 73, 89, 93, 97, 97]

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章