Python數據結構與算法(十二、最大堆)

保證一週更兩篇吧,以此來督促自己好好的學習!代碼的很多地方我都給予了詳細的解釋,幫助理解。好了,幹就完了~加油!
聲明:本python數據結構與算法是imooc上liuyubobobo老師java數據結構的python改寫,並添加了一些自己的理解和新的東西,liuyubobobo老師真的是一位很棒的老師!超級喜歡他~
如有錯誤,還請小夥伴們不吝指出,一起學習~

一、二叉堆

  1. 二叉堆是一棵【完全二叉樹】(不一定是滿樹,除最底層外,其它層一定是滿樹,但是在最底層的節點一定是從左到右連續排列的)
  2. 既然是完全二叉樹,就可以用數組的方式來表示這棵完全二叉樹,數組索引按樹的廣度優先遍歷的順序那樣來設置(即按層序標註,每層從左到右來標註)
    這樣就會有着明顯的索引關係:(元素從索引0開始盛放,相應的若從索引1開始盛放元素需要做出一點更改,也很簡單)
parent(i)      = (i - 1) // 2     # 獲取位於索引i的元素的父親的索引
left_child(i)  = i * 2 + 1        # 獲取位於索引i的元素的左孩子的索引,可以畫圖看一下,很簡單的
right_child(i) = i * 2 + 2        # 獲取位於索引i的元素的右孩子的索引
  1. 注意,雖然叫二叉堆,但是在真正的實現中並沒有Node一說,只有一個數組作爲底層的數據結構,因爲堆一定是一棵完全二叉樹,因此用索引就已經能夠表徵一個二叉堆了。

二、最大堆

堆中某個節點的值總是【不大於】其父節點的值(除非沒有父節點)————最大堆(相應的可以定義最小堆),本節來實現最大堆。

三、實現

# -*- coding: utf-8 -*-
# Author:           Annihilation7
# Data:             2018-10-20    10:58 pm
# Python version:   3.6

from array.Array import Arr    # 用我們以前實現的數組類作爲二叉堆的底層數據結構

class MaxHeap:
    def __init__(self, capacity_=20):
        """二叉堆的構造函數"""
        self._data = Arr(capacity=capacity_)   # 調用數組的構造函數就可以了,別忘了我們的數組是支持索引操作的

    def isEmpty(self):
        """
        Description: 判空
        Returns:
        bool值,空爲True
        """
        return self._data.isEmpty()   # 直接調用數組的判空函數

    def getSize(self):
        """
        Description: 獲取當前二叉堆中有效元素的個數
        Returns:
        有效元素的個數
        """
        return self._data.getSize()

    def add(self, elem):
        """
        Description: 向堆中添加元素elem
        時間複雜度:O(logn)
        Params:
        - elem: 待添加元素
        """
        self._data.addLast(elem)  # 由於是一棵完全二叉樹,因此直接向數組尾部添加元素就完事了,
        # 但是必須要對新添加的這個元素進行相應的維護操作,使其繼續滿足堆的性質。
        # 空間問題不用考慮,有自動擴容
        self._shiftUp(self.getSize() - 1) # addLast操作中已經維護了self._size,所以此時新添加的元素位於self.getSize()-1索引處
        # 調用self._shiftUp來上移(shiftup)位於這個索引的元素,使其滿足堆的性質

    def findMax(self):
        """
        Description: 看一下堆中的最大值(也就是優先級最高的元素)
        時間複雜度:O(1)
        Returns:
        優先級最高的元素的值
        """
        if self.isEmpty():
            raise Exception('Error! The maxheap is empty!')
        return self._data[0]   # 就是0索引處的元素的值

    def extractMax(self):
        """
        Description: 將堆中最大元素取出來,並返回其相應的值。
        問題:  取出後,爲了維護堆的性質,由誰來代替最大值的位置呢?如果要將兩個子樹進行合併,非常的複雜, 我們這樣來做:
                將數組中的最後一個元素放到索引0的位置,然後對索引0處的元素進行shiftDown操作,顧名思義,與shiftUp的操作是
                處於對立關係的,從而維護堆的性質
        Returns:
        堆中最大的元素的值(索引0處的元素的值)
        """
        ret = self.findMax()   # 先找到最大值,便於返回,這裏不用判空了哈,findMax已經做了。
        self._data.swap(0, self.getSize() - 1)   # 將數組尾部的元素和索引0處的元素交換
        self._data.removeLast()     # 此時將數組尾部的元素刪除(也就是最大值,我們已經記錄過了,刪除就完事了)
        self._shiftDown(0)   # 此時對新來的索引0處的元素進行shiftDown操作,從而滿足堆的性質
        return ret # 將最大值返回

    def replace(self, elem):
        """
        Description: 取出堆頂的元素,然後在添加一個元素elem,返回原先堆頂的元素。
        實現這個方法可以從我們已經實現的方法組合來實現,先extacrmax(),再add()操作,這樣兩側O(logn)的操作,
        在這裏我們通過一個O(logn)的方法來實現。
        時間複雜度:O(logn)
        Params:
        - elem: 待添加元素
        Returns:
        原先堆頂的元素
        """
        ret = self.findMax()       # 找到棧頂元素並用ret記錄,便於返回
        self._data.set(0, elem)    # 直接將堆頂元素設爲elem,調用數組的set函數,時間複雜度爲O(1)
        self._shiftDown(0)         # 新設置的值極有可能破壞了堆的性質,所以將索引0處的元素進行shiftDown操作,使其最後滿足堆的性質
        return ret                 # 將原先堆頂元素返回 

    def printMaxHeap(self):
        """對堆元素進行打印操作"""
        self._data.printArr()      # 直接調用數組的printArr()函數即可       


    
    # private
    def _parent(self, index):
        """
        Description: 獲取index索引處元素的父親節點的index
        Params:
        - index: 傳入的索引值
        Returns:  
        其父親所在的位置
        """
        if index == 0:         # 非法index,這個位置是二叉堆根節點的位置,沒有父親節點。
            raise Exception('index-0 doesn\'t have parent.')
        return (index - 1) // 2   # 根據公式返回就好了,建議畫圖理解下,很簡單

    def _leftChild(self, index):
        """
        Description: 獲取index索引處元素的左孩子節點的index
        Params:
        - index: 傳入的索引值
        Returns:
        其左孩子所在的位置
        """
        return index * 2 + 1 

    def _rightChild(self, index):
        """
        Description: 獲取index索引處元素的右孩子節點的index
        Params:
        - index: 傳入的索引值
        Returns:
        其右孩子所在的位置
        """
        return index * 2 + 2

    def _shiftUp(self, k):
        """
        Description: 對位於索引k的元素進行上移操作,移動到合適的位置,從而滿足堆的性質
        Params:
        - k: 傳入的索引值
        """
        while k > 0 and self._data[k] > self._data[self._parent(k)]:  # 如果k>0(沒到根節點,也就是還有父親節點),並且索引k處的元素大於其父親的元素
            self._data.swap(k, self._parent(k))    # 交換
            k = self._parent(k)                    # k移動到父親節點的索引處,看是否還需要上移,反正只要不到根節點就一直判斷嘛,直到合適的位置

    def _shiftDown(self, k):
        """
        Description: 對位於索引k的元素進行下移操作,移動到合適的位置,從而滿足堆的性質
        Params:
        - k: 傳入的索引值
        """
        while(self._leftChild(k) < self.getSize()):   # 如果左孩子的索引還有效(想一想爲什麼不是右孩子,如果判斷的是右孩子的索引值,就會少判斷一種情況:
            # 就是右孩子的索引無效,但是左孩子的索引還有效)
            j = self._leftChild(k)   # 此時左孩子的索引有效,用j來標記這個索引
            if self._rightChild(k) < self.getSize():    # 還沒有判斷右孩子,肯定是要和兩個孩子的最大值進行交換,這樣在交換後才能滿足堆的定義:孩子節點的值
                # 不大於父親節點的值,所以要找到兩個孩子的最大值,如果右孩子有效:
                if self._data[self._rightChild(k)] > self._data[j]:   # 看一下右孩子的值是否大於左孩子的值
                    j = self._rightChild(k)                        # 如果大於,j就等於右孩子的索引,否則什麼也不做
            # 此時索引j等於位於索引k處元素的兩個孩子的最大值的索引
            if self._data[k] >= self._data[j]:   # 此時索引k的值已經大於兩個孩子的最大值了
                break     # 直接退出循環
            self._data.swap(k, j)   # 否則就和最大值交換
            k = j                   # k繼續往下走,看是否需要繼續進行交換操作來滿足最大堆的定義 


    @staticmethod
    # 獨立的一個實現
    def heapify(alist):
        """
        Description: 傳入一個列表,並自動對列表中的所有元素進行堆排列。實在是因爲python
        沒有構造函數的重載功能,我迫不得已才把這個功能單獨抽出來作爲一個靜態函數,有一點不方便。。。
        分析: 實現這個功能很簡單,新建一個空的MaxHeap,然後將列表中的所有元素一個一個的add
              到MaxHeap中,這樣做時間複雜度爲O(nlogn)。本函數要實現一個時間複雜度爲O(n)的堆
              排列功能,至於爲什麼是O(n)的,我也不知道,感興趣的可以看一下heapify的時間複雜度的
              數學推導,但是要知道O(n)與O(nlogn)之間性能是質的飛躍!
        時間複雜度:O(n)
        Params:
        - alist: 一個列表,裏面的元素是要進行堆排列的全部元素
        Returns:
        一個滿足堆排列(最大堆)的list,注意這個函數是in-place操作哦。
        """
        def shiftDown(alist, index):
            """
            Description: 和MaxHeap的self._shiftDown操作是一樣,只不過我需要在這裏再實現一個,才
            可以調用--!
            Params:
            - alist: 一個列表
            - index: 要shiftDown的索引
            """
            length = len(alist)    # 就不詳細講解了,跟前面的self._shiftDown是一樣的~
            while index * 2 + 1 < length:
                j = index * 2 + 1
                if j + 1 < length:
                    if alist[j + 1] > alist[j]:
                        j += 1
                if alist[index] >= alist[j]:
                    break 
                alist[index], alist[j] = alist[j], alist[index]
                index = j 
        
        length = len(alist)
        # 這裏要記住堆有一個極其重要的性質~就是堆的第一個非葉子節點的索引一定是
        # ((length - 1) - 1) // 2 
        # 其實想一想也很簡單,length - 1這個索引一定是數組的最後一個元素,最後一個元素的父親
        # 節點的索引一定是第一個非葉子節點的索引呀,即parent(length - 1),這裏並沒有parent函數
        # 所以展開就是 ((length - 1) - 1) // 2
        # 從第一個非葉子節點開始,一直到索引0處(二叉堆根的索引),都執行shiftDown操作,最後
        # 這個數組也就變成了一個堆。爲什麼不用管  (((length - 1) - 1) // 2, length - 1]左
        # 開右閉區間內的點呢?原因也很簡單,在非葉子節點shiftDown的過程中自動就將這些點維護了!
        for i in range(((length - 1) - 1) // 2, -1, -1):  # 遍歷第一個非葉子節點開始一直到根節點
            shiftDown(alist, i)         # 都shiftDown一次就完事了
        return alist

四、測試

from Priorityqueue.maxheap import MaxHeap  # 我們的MaxHeap寫在了maxheap.py文件中
import numpy as np 
np.random.seed(7)

test_maxheap = MaxHeap()
nums = 100000  # 操作數

for i in range(nums - 1):
    test_maxheap.add(np.random.randint(1000))  # 99999次添加隨機數操作
test_maxheap.add(6666)  # 再添加一個最大的,便於驗證

print('添加元素操作後的Size:', test_maxheap.getSize())
print('此時堆中最大值:', test_maxheap.findMax())

record = []
for i in range(nums):
    record.append(test_maxheap.extractMax())  # 100000次提取最大元素操作並append到record中
    # 不出意外,record中的元素應該是完全降序排列的

for i in range(1, len(record)):    # 測試record是否真的是降序排列
    if record[i - 1] < record[i]:
        raise Exception('Error, the list is not absolutely a reversed list.')
print('Test MaxHeap completed.')

print('將元素全部抽取後是否爲空堆?', test_maxheap.isEmpty())

print('---------------------------------------------')
print('新建一個堆完成replace的測試,10次添加操作後:')

#######################################################
#                        9                            #
#                   /          \                      #
#                 8             5                     #
#               /   \         /   \                   #
#             6       7      1     4                  #
#           /   \    /                                #
#          0     3  2                                 #
#######################################################

test_maxheap2 = MaxHeap()
for i in range(10):
    test_maxheap2.add(i)
test_maxheap2.printMaxHeap()
print('抽取堆頂元素並添加一個0.7:') # 0.7這裏選的不好,因爲浮點數一旦涉及到判等操作會出現問題,但是字在這裏沒有判等操作
test_maxheap2.replace(0.7)
test_maxheap2.printMaxHeap()

print('測試heapify函數')
test_list = [i for i in range(30)]
print('待堆排列的元素:', test_list)
print('堆排列後:')   
print(MaxHeap.heapify(test_list))  # 可以動手畫一下是滿足堆排列性質的。

五、輸出

添加元素操作後的Size: 100000
此時堆中最大值: 6666
Test MaxHeap completed.
將元素全部抽取後是否爲空堆? True
---------------------------------------------
新建一個堆完成replace的測試,10次添加操作後:
9  8  5  6  7  1  4  0  3  2
Size: 10-----Capacity: 20
抽取堆頂元素並添加一個0.78  7  5  6  2  1  4  0  3  0.7
Size: 10-----Capacity: 20
測試heapify函數
待堆排列的元素: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
堆排列後:
[29, 22, 28, 18, 21, 26, 27, 16, 17, 20, 10, 24, 25, 13, 14, 15, 7, 3, 8, 19, 9, 4, 1, 23, 11, 5, 12, 2, 0, 6]

六、總結

  1. 本節實現的是一個最大堆,相應的也可以更改成一個最小堆,定義就是某個節點的值都【不小於】其父親節點的值,相應的操作大同小異,很簡單。
  2. 根據最大堆特有的性質,最大值一定位於索引0處,因此可以很輕鬆的實現一個全部操作時間複雜度爲O(logn)的優先隊列,下一小節我們一起動手來實現一個基於最大堆的優先隊列,性能非常的好,因爲是O(logn)的時間複雜度!!
  3. 可能基礎知識講解的不夠到位,畢竟沒有圖解神馬的,這方面小夥伴們還是需要去額外的查一下資料呢。因爲怕侵權,還是不貼老師上課ppt的圖了,有需要的小夥伴直接去聽他的課就好,很值得的!講的非常非常好!

若有還可以改進、優化的地方,還請小夥伴們批評指正!

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