python最大堆_用Python实现最大堆
本文的內(nèi)容是如何通過二叉樹實(shí)現(xiàn)一個(gè)最大堆, 實(shí)現(xiàn)原理方面參考了Python的heap模塊. 此外, 在正式項(xiàng)目上, 我還是建議你使用python自帶的heap完成, 它只提供最小堆, 但是可以通過對所有元素取反或者重寫__lt__方法實(shí)現(xiàn)最大堆.
一. 堆的數(shù)據(jù)結(jié)構(gòu)
1. 數(shù)據(jù)結(jié)構(gòu)分析
堆的本質(zhì)就是一顆二叉樹, 這顆二叉樹必須具備以下兩個(gè)性質(zhì):
1). 對于最大堆來說, 二叉樹根節(jié)點(diǎn)的值不小于任何子節(jié)點(diǎn), 其所有子樹也符合這一特征, 最小堆則相反;
2). 堆是一顆完全二叉樹, 除了底層外, 所有層都盡可能地填滿, 底層元素從左到右排列.
上圖就是一個(gè)最大堆的二叉樹, 基于特性1我們可以得知, 這顆二叉樹從任意葉子節(jié)點(diǎn)到根節(jié)點(diǎn)的路徑一定是一個(gè)遞增序列, 最大值為根節(jié)點(diǎn). 因此, 當(dāng)我們需要最大值時(shí), 取出根節(jié)點(diǎn)的值就行了. 當(dāng)我們新添加了一個(gè)葉子節(jié)點(diǎn)之后, 為了維護(hù)二叉樹的有序性, 我們可以讓這個(gè)葉子節(jié)點(diǎn)向頂端移動(dòng), 如下圖所示:
->
->
我們插入節(jié)點(diǎn)16后, 將這個(gè)節(jié)點(diǎn)的值與其父節(jié)點(diǎn)進(jìn)行比較, 大于父節(jié)點(diǎn)則二者交換, 持續(xù)這個(gè)操作直到不大于父節(jié)點(diǎn)或沒有父節(jié)點(diǎn)為止, 這樣, 我們就在插入元素之后, 仍然保持了二叉樹的有序性. 彈出節(jié)點(diǎn)同理, 將底層最后一個(gè)葉子節(jié)點(diǎn)取出填入空缺, 然后根據(jù)值的大小讓這個(gè)節(jié)點(diǎn)往下移動(dòng)就行.
因此, 堆在保證內(nèi)部有序性的前提下, 可以做到在O(k)的時(shí)間內(nèi)插入和彈出元素, k為二叉樹的高度. 這也就是為什么堆的二叉樹必須是完全二叉樹: 在這種情況下k最小, 為log n. 因此, 堆的插入和彈出都只需要O(log n)的時(shí)間復(fù)雜度, 可以高效地獲取最大值/最小值.
2. 通過列表實(shí)現(xiàn)二叉樹
由于堆是一顆完全二叉樹, 因此我們可以用一個(gè)列表來儲(chǔ)存這顆二叉樹的值:
如上圖所示, 我們用列表從上到下, 從左到右記錄了二叉樹的所有節(jié)點(diǎn). 二叉樹節(jié)點(diǎn)右邊的藍(lán)色數(shù)字是它在列表中的索引. 因此我們可以得知, 對于一個(gè)在列表中索引為n的節(jié)點(diǎn), 它的父節(jié)點(diǎn)索引為(n-1)//2, 它的左右子節(jié)點(diǎn)索引為n*2+1和n*2+2, 如果索引值溢出, 說明沒有對應(yīng)的父節(jié)點(diǎn)或子節(jié)點(diǎn). 這樣, 我們就通過列表儲(chǔ)存了這顆完全二叉樹的信息.
基于以上的分析, 我們先定義一個(gè)Heap類:
classHeap:def __init__(self, nums: [int] = None) ->None:
self.cache= nums or[]
self._heapify()def __len__(self) ->int:returnlen(self.cache)def __bool__(self) ->bool:return len(self) >0def __repr__(self) ->str:return f'heap({self.cache})'@propertydef largest(self) ->int:if notself.cache:raise Exception('Empty heap')returnself.cache[0]def show(self) ->None:#調(diào)用這個(gè)函數(shù)繪制一顆二叉樹出來,DEBUG用
height = int(math.log2(len(self))) + 1
for i inrange(height):
width= 2 ** (height - i) - 2
print(' ' * width, end='')
blank= ' ' * (width * 2 + 2)print(
blank.join(['{: >2d}'.format(num) for num in self.cache[2 ** i - 1:min(2 ** (i + 1) - 1, len(self))]]))print()def _swap(self, i: int, j: int) ->None:#這個(gè)方法交換二叉樹的兩個(gè)節(jié)點(diǎn)
self.cache[i], self.cache[j] = self.cache[j], self.cache[i]
二. 插入元素
這部分好像太簡單了, 我實(shí)在講不出來什么:
def push(self, num: int) ->None:
self.cache.append(num)
self._siftup(self.size- 1)def _siftup(self, i: int) ->None:while i >0:
parent= (i - 1) >> 1
if self.cache[i] <=self.cache[parent]:breakself._swap(i, parent)
i= parent
說白了, 當(dāng)我們push一個(gè)元素時(shí), 首先把這個(gè)元素放到列表的末端, 這相當(dāng)于在完全二叉樹上新建了一個(gè)葉子節(jié)點(diǎn). 然后, 調(diào)用siftup方法讓這個(gè)節(jié)點(diǎn)一直和父節(jié)點(diǎn)比較, 大于父節(jié)點(diǎn)就上浮, 直到它到達(dá)合適的位置. 這樣就維護(hù)了二叉樹的有序性.
三. 彈出元素
彈出元素的原理和插入元素大同小異: 我們將根節(jié)點(diǎn)的元素彈出后, 取出最后一個(gè)葉子節(jié)點(diǎn)作為根節(jié)點(diǎn)(避免破壞完全二叉樹的結(jié)構(gòu)), 然后讓這個(gè)節(jié)點(diǎn)與子節(jié)點(diǎn)比較, 下沉到合適的位置就行. 有兩點(diǎn)需要注意一下: 首先, 最大元素處在列表的頭部, 彈出的時(shí)間復(fù)雜度是O(n), 因此我們可以把頭部元素和尾部元素交換后, 刪除尾部元素. 然后, 大部分節(jié)點(diǎn)都有兩個(gè)子節(jié)點(diǎn), 我們應(yīng)該讓更大的那個(gè)節(jié)點(diǎn)上浮, 這樣才能保證二叉樹的有序性.
基于以上兩點(diǎn), 彈出元素的代碼如下:
def pop(self) ->int:
largest=self.largest
self._swap(0, len(self)- 1)
self.cache.pop()
self._siftdown(0)returnlargestdef _siftdown(self, i: int) ->None:while i * 2 + 1
smaller=iif self.cache[i * 2 + 1] >self.cache[smaller]:
smaller= i * 2 + 1
if i * 2 + 2 < len(self) and self.cache[i * 2 + 2] >self.cache[smaller]:
smaller= i * 2 + 2
if smaller ==i:returnself._swap(i, smaller)
i= smaller
四. 列表的堆化
我們在創(chuàng)建Heap對象時(shí)傳入了一個(gè)列表作為堆的原始數(shù)據(jù), 但是, 這個(gè)列表并不一定是顆有序的二叉樹, 因此我們需要將其堆化.
最容易想到的方式是, 首先創(chuàng)建一個(gè)空堆, 然后將列表的所有元素依次推入堆中, 通過_siftup方法保持有序:
如上圖所示, 如果我們通過_siftup來堆化所有元素, 則時(shí)間復(fù)雜度為O(n/2*log n+n/4*log n/2+...+1*1)=O(nlog n), 這和排序的時(shí)間復(fù)雜度差不多, 因此不是很理想.
另外一種方案是, 首先按照列表的原有順序構(gòu)建二叉樹, 然后從二叉樹的倒數(shù)第二層開始, 依次通過_siftdown下沉, 這樣依次為k-1層, k-2層直到頂層排序:
這種堆化方式的時(shí)間復(fù)雜度為O(n), 計(jì)算過程如下:
T(n)=O(n/4)+O(n/8*2)+(n/16*3)+O(log n)2*T(n)=O(n/2)+O(n/4*2)+(n/8*3)+O(2*log n)2*T(n)-T(n)=O(n/2)+O(n/4)+O(n/8)+...+O(log n)=O(n)
因此, 我們的堆化方法可以這么寫:
def _heapify(self) ->None:for i in reversed(range(len(self) // 2)):
self._siftdown(i)
五. 總結(jié)
簡單對我們創(chuàng)建的Heap類進(jìn)行測試:
nums = list(range(14))
random.shuffle(nums)
heap=Heap(nums[:])
heap.show()
heap.push(100)print('插入100')
heap.show()
heap.pop()print('彈出堆頂元素')
heap.show()for _ in range(100):
num= random.randrange(100)
nums.append(num)
heap.push(num)assert max(nums) ==heap.largest
nums.remove(heap.pop())print('所有測試通過!!!')
結(jié)果如下:
總結(jié)
以上是生活随笔為你收集整理的python最大堆_用Python实现最大堆的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 医疗投资公司经营业务范围
- 下一篇: mysql utf8 乱码_MySql