數據結構與算法---二叉堆和二叉堆排序(python實現)

一、什麼是二叉堆

1. 堆的定義:

堆(heap),這裏指的堆是一種數據結構,不是內存模型中的堆。堆通常可以看作爲一棵樹,但這棵樹得滿足以下條件:

a.  堆中任意節點的值總是不大於(不小於)其子節點的值;

b.  堆總是一顆完全樹。

將任意節點不大於其子節點的堆叫做最小堆或小根堆,而將任意節點不小於其子節點的堆叫做最大堆或者大根堆。常見的堆有二叉堆,左傾堆,斜堆,二項堆,斐波那契堆等等。

2. 二叉堆:

二叉堆是完全二叉樹,它分爲兩種:最大堆最小堆

最大堆:父結點的鍵值總是大於或等於任何一個子節點的鍵值;最小堆:父結點的鍵值總是小於或等於任何一個子節點的鍵值。示意圖如下:


二、二叉堆的存儲

二叉堆是一顆二叉樹,因此我們很容易想到使用鏈式存儲,但是二叉堆是一顆完全二叉樹,因此我們可以使用數組這種更簡單高效的存儲方式。

我們將二叉堆的第一個元素放在數組索引的0的位置,也可以放在索引爲1的位置。當然,它們的本質是一樣的。

當第一個元素放在索引爲0的位置上時:

1. 索引爲 i 的左孩子的索引爲(2*i + 1)

2. 索引爲 i 的右孩子的索引爲 (2*i + 2)

3. 索引爲 i 的父節點的索引爲 (i - 1)/ 2(計算機裏取整)

二叉堆及其數組存儲方式如下:


當第一個元素放在索引爲1的位置上時:

1. 索引爲 i 的左孩子的索引爲(2*i )

2. 索引爲 i 的右孩子的索引爲 (2*i + 1)

3. 索引爲 i 的父節點的索引爲 (i )/ 2(計算機裏取整)

二叉堆及其數組存儲方式如下:

三、二叉堆的基本操作:shift_up與shift_down

我們以最大堆來演示二叉堆的插入與刪除對應的shift_up與shift_down操作

1. 插入數據---shift_up:

例如,在最大堆[90,80,70,60,40,30,20,10,50]中添加85,需要執行的步驟如下:


插入數據基本過程如下:

a.  將數據加入到最大堆的末尾,即數組最後

b. 然後通過shift_up操作把數據儘可能的往上挪,直到挪不動爲止

因此,插入的最關鍵步驟爲shift_up,最大堆插入的代碼如下:

class MaxHeap:
    heap = []

    @staticmethod
    def insert(num):
        MaxHeap.heap.append(num)
        MaxHeap.shift_up()

    @staticmethod
    def shift_up():
        current_id = len(MaxHeap.heap) - 1
        parent_id = (current_id - 1)//2
        while current_id > 0:
            if MaxHeap.heap[parent_id] >= MaxHeap.heap[current_id]:
                break
            else:
                MaxHeap.heap[parent_id], MaxHeap.heap[current_id] = MaxHeap.heap[current_id], MaxHeap.heap[parent_id]
                current_id = parent_id
                parent_id = (current_id -1)//2

2. 刪除數據---shift_down:

如例,從最大堆[90,85,70,60,80,30,20,10,50,40]中刪除90,需要執行的步驟如下:


刪除數據的步驟如下:

a. 刪除該數據m,但數組結構不變,即其他數據位置不發生移動

b. 將數組最後一個數據n移動到剛纔刪除的數據m的索引處

c. 通過shift_down操作,把數據n,儘量往下挪,直到生於的數組重新成爲最大堆

因此,刪除的最關鍵步驟爲shift_down,最大堆刪除的代碼如下:

class MaxHeap:
    heap = [90,85,70,60,80,30,20,10,50,40]

    @staticmethod
    def insert(num):
        MaxHeap.heap.append(num)
        MaxHeap.shift_up()

    @staticmethod
    def shift_up():
        current_id = len(MaxHeap.heap) - 1
        parent_id = (current_id - 1)//2
        while current_id > 0:
            if MaxHeap.heap[parent_id] >= MaxHeap.heap[current_id]:
                break
            else:
                MaxHeap.heap[parent_id], MaxHeap.heap[current_id] = MaxHeap.heap[current_id], MaxHeap.heap[parent_id]
                current_id = parent_id
                parent_id = (current_id -1)//2

    @staticmethod
    def delate(num):
        temp = MaxHeap.heap.pop()
        ind = MaxHeap.heap.index(num)
        MaxHeap.heap[ind] = temp
        MaxHeap.shift_down(ind)

    @staticmethod
    def shift_down(ind):
        current_id = ind
        child_id_left = current_id * 2 + 1
        child_id_right = current_id * 2 + 2
        while current_id < len(MaxHeap.heap) - 1:
            #如果當前節點爲葉子節點,shift_down完成
            if current_id * 2 + 1 > len(MaxHeap.heap) - 1:
                break
            #如果當前節點只有左孩子沒有右孩子
            if current_id * 2 + 1 == len(MaxHeap.heap) - 1:
                if MaxHeap.heap[current_id] > MaxHeap.heap[-1]:
                    break
                else:
                    MaxHeap.heap[current_id], MaxHeap.heap[-1] = MaxHeap.heap[-1], MaxHeap.heap[current_id]
                    break
            #如果當前節點既有左孩子又有右孩子
            if MaxHeap.heap[current_id] > max(MaxHeap.heap[child_id_left], MaxHeap.heap[child_id_right]):
                break
            else:
                if MaxHeap.heap[child_id_right] > MaxHeap.heap[child_id_left]:
                    MaxHeap.heap[child_id_right], MaxHeap.heap[current_id] = MaxHeap.heap[current_id], MaxHeap.heap[child_id_right]
                    current_id = child_id_right
                    child_id_left = current_id * 2 + 1
                    child_id_right = current_id * 2 + 2
                else:
                    MaxHeap.heap[child_id_left], MaxHeap.heap[current_id] = MaxHeap.heap[current_id], MaxHeap.heap[child_id_left]
                    current_id = child_id_left
                    child_id_left = current_id * 2 + 1
                    child_id_right = current_id * 2 + 2

四、基礎堆排序和Heapify

1. 基礎排序

有了堆的基本操作,實現堆的排序就比較簡單了,用最大堆實現升序排序步驟如下:

a. 將待排序列表依次插入

b. 依次取出堆頂元素並放進原列表對應位置

代碼實現如下:

class MaxHeap:
    heap = []

    @staticmethod
    def insert(num):
        MaxHeap.heap.append(num)
        MaxHeap.shift_up()

    @staticmethod
    def shift_up():
        current_id = len(MaxHeap.heap) - 1
        parent_id = (current_id - 1)//2
        while current_id > 0:
            if MaxHeap.heap[parent_id] >= MaxHeap.heap[current_id]:
                break
            else:
                MaxHeap.heap[parent_id], MaxHeap.heap[current_id] = MaxHeap.heap[current_id], MaxHeap.heap[parent_id]
                current_id = parent_id
                parent_id = (current_id -1)//2

    @staticmethod
    def delate(num):
        temp = MaxHeap.heap.pop()

        ind = MaxHeap.heap.index(num)
        MaxHeap.heap[ind] = temp
        MaxHeap.shift_down(ind)



    @staticmethod
    def shift_down(ind):
        current_id = ind
        child_id_left = current_id * 2 + 1
        child_id_right = current_id * 2 + 2
        while current_id < len(MaxHeap.heap) - 1:
            #如果當前節點爲葉子節點,shift_down完成
            if current_id * 2 + 1 > len(MaxHeap.heap) - 1:
                break
            #如果當前節點只有左孩子沒有右孩子
            if current_id * 2 + 1 == len(MaxHeap.heap) - 1:
                if MaxHeap.heap[current_id] > MaxHeap.heap[-1]:
                    break
                else:
                    MaxHeap.heap[current_id], MaxHeap.heap[-1] = MaxHeap.heap[-1], MaxHeap.heap[current_id]
                    break
            #如果當前節點既有左孩子又有右孩子
            if MaxHeap.heap[current_id] > max(MaxHeap.heap[child_id_left], MaxHeap.heap[child_id_right]):
                break
            else:
                if MaxHeap.heap[child_id_right] > MaxHeap.heap[child_id_left]:
                    MaxHeap.heap[child_id_right], MaxHeap.heap[current_id] = MaxHeap.heap[current_id], MaxHeap.heap[child_id_right]
                    current_id = child_id_right
                    child_id_left = current_id * 2 + 1
                    child_id_right = current_id * 2 + 2
                else:
                    MaxHeap.heap[child_id_left], MaxHeap.heap[current_id] = MaxHeap.heap[current_id], MaxHeap.heap[child_id_left]
                    current_id = child_id_left
                    child_id_left = current_id * 2 + 1
                    child_id_right = current_id * 2 + 2

    @staticmethod
    def extract_max():
        num = MaxHeap.heap[0]
        try:
            MaxHeap.delate(num)
            return num
        except:
            return num

    @staticmethod
    def heap_sort(arr):
        for n in arr:
            MaxHeap.insert(n)
        for i in range(len(arr)):
            arr[i] = MaxHeap.extract_max()

2. Heapify

基礎堆排序中,將n個元素逐個插入到一個空堆中,算法複雜度是O(nlogn)

而下面介紹的Heapify,對n個元素的建堆,算法複雜度是O(n)

Heapify算法過程如下:

----堆的第一個元素從索引0開始,堆元素個數爲n

a. 找到待建堆的二叉樹最後一個非葉子節點,索引爲 m =(n - 1)/2

b. 從索引m到0,依次執行shift_down 操作

二叉樹的倒數第一層滿足二叉堆性質,因此,從倒數第二層開始,通過shift_down 逐層的將其轉換爲二叉堆。

代碼如下(附帶通過heapify的排序算法):

    @staticmethod
    def heapify(arr):
        MaxHeap.heap = arr
        n = (len(arr) - 1)//2
        while n >= 0:
            MaxHeap.shift_down(n)
            n -= 1

    @staticmethod
    def heap_sort2(arr):
        MaxHeap.heapify(arr)
        res = []
        for i in range(len(arr)):
            res.append(MaxHeap.extract_max())
        return res

五、原地堆排序

在上一節中,無論是堆的基礎排序還是基於heapify的排序,都需要額外的開闢一片空間存放排序。空間複雜度爲O(n),

接下來要講的原地堆排序的空間複雜度爲O(1), 算法過程分析如下:

a. 由heapify對n個元素的列表建堆

b. 將堆頂元素與堆尾元素互換,堆大小減一

c. 對堆頂元素執行shift_down操作

d. 依次循環b,c。當堆中元素個數爲0時爲止

代碼如下:

    @staticmethod
    def heap_sort3(arr):
        MaxHeap.heapify(arr)
        for i in range(len(arr)-1, -1, -1):
            MaxHeap.heap[i], MaxHeap.heap[0] = MaxHeap.heap[0], MaxHeap.heap[i]  #將堆頂元素與堆尾元素互換
            MaxHeap.shift_down(0, i)

六、堆的優勢

若使用堆做靜態數組的排序,它的時間複雜度與快速排序相比並沒有優勢,實際上一般情況下要慢於快速排序。

那堆排序的優勢在哪呢?

堆,在解決動態排序問題時,有較大優勢。

問題1. 動態選擇優先級最高的任務執行

很多情況下,我們需要使用優先隊列來解決實際問題,如操作系統選擇優先級最高的進程使用CPU,而進程隨時都會有新進程產生,也會有老進程死亡,而且各進程的優先級也會動態變化。這種時候,如果每次都用排序算法對所有進程優先級進行排序,可以想象耗時是巨大的。而此時堆來解決優先隊列就顯示出巨大優勢,插入新元素,重建最大堆,刪除元素,這些操作的時間複雜度均爲O(logn)。

問題2. 從N個元素中選出前M個(N巨大而M相對很小,如N=10000000,M=10)

用快速排序算法時間複雜度爲NlogN, 而用堆排序時間複雜度爲NlogM

當然對於問題2,對快排進行改進,也可提高效率,具體實現方法還沒想太清楚。

綜上:堆的最大優勢就在使用堆實現優先隊列。


參考博客:

https://www.cnblogs.com/skywang12345/p/3610187.html

https://coding.imooc.com/class/207.html





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