堆是一棵徹底二叉樹。堆分爲大根堆和小根堆,大根堆是父節點大於左右子節點,而且左右子樹也知足該性質的徹底二叉樹。小根堆相反。能夠利用堆來實現優先隊列。python
因爲是徹底二叉樹,因此可使用數組來表示堆,索引從0開始[0:length-1]。結點i的左右子節點分別爲2i+1,2i+2。長度爲length的樹的最後一個非葉子節點爲length//2-1。當前節點i的父節點爲(i-1)//2。其中//表示向下取整。數組
以大根堆舉例。當每次插入或者刪除的時候,爲了保證堆的結構特徵不被破壞,須要進行調整。調整分爲兩種,一種是從上往下,將小的數下沉。一種是從下往上,令大的數上浮。app
具體實現以下:函數
首先編寫幾個魔術方法。包括構造函數,能夠直接調用len來返回data數組長度的函數,一個打印data內容的函數code
def __init__(self, data=[]): self.data = data self.construct_heap() def __len__(self): return len(self.data) def __str__(self): return str(self.data)
定義一個swap函數,來方便的交換數組中兩個索引處的值。索引
def swap(self, i, j): self.data[i], self.data[j] = self.data[j], self.data[i]
定義float_up方法,使堆中大的數能浮上來。當前節點不爲根節點,而且當前節點數據大小大於父節點時,上浮。隊列
def float_up(self, i): while i > 0 and self.data[i] > self.data[(i - 1) // 2]: self.swap(i, (i - 1) // 2) i = (i - 1) // 2
定義sink_down方法,使堆中小的數沉下去。當前節點不爲葉子節點時,若是小於左孩子或右孩子的數據,則和左右孩子中較大的換一下位置。get
def sink_down(self, i): while i < len(self) // 2: l, r = 2 * i + 1, 2 * i + 2 if r < len(self) and self.data[l] < self.data[r]: l = r if self.data[i] < self.data[l]: self.swap(i, l) i = l
實現append方法,可以動態地添加數據。在數據數組尾部添加數據,而後將數據上浮。it
def append(self, data): self.data.append(data) self.float_up(len(self) - 1)
實現pop_left方法,取堆中最大元素,即優先隊列中第一個元素。將數組中第一個元素與最後一個元素換位置,刪除最後一個元素,而後將第一個元素下沉到合適的位置。date
def pop_left(self): self.swap(0, len(self) - 1) r = self.data.pop() self.sink_down(0) return r
若是想在初始化堆的時候,向構造函數中傳入數據參數,則須要一次性將整個堆構建完畢,而不能一個一個加入。實現也很簡單,從最後一個非葉節點開始,逐個執行sink_down操做。
def construct_heap(self): for i in range(len(self) // 2 - 1, -1, -1): self.sink_down(i)
這樣一個基本的堆的代碼就編寫完畢了。
可是若是咱們想要動態的改變數據,當前的堆就不能知足咱們的需求了,由於索引不能老是標識同一個數據,由於堆的結構是不斷調整的。咱們須要使用索引堆。
在索引堆中,咱們不在堆中直接保存數據,而是用在堆中存放數據的索引。
若是咱們輸入的數據arr是 45 20 12 5 35。則arr[0]一直指向45,arr[1]一直指向20,由於咱們在調整堆結構中實際調整的是索引數組,而不會改變真實存放數據的數組。
所以咱們的代碼須要調整,首先在構造函數中加入一個索引數組。下標從0開始,與存放數據的數組的下標相對應。
def __init__(self, data=[]): self.data = data self.index_arr = list(range(len(self.data))) self.construct_heap()
而後將返回堆長度的魔術函數也修改一下。
def __len__(self): return len(self.index_arr)
調整一下以前定義的swap方法,原來是直接交換數據,如今交換索引。
def swap(self, i, j): self.index_arr[i], self.index_arr[j] = self.index_arr[j], self.index_arr[i]
調整float_up以及sink_down中的相應位置
def float_up(self, i): while i > 0 and self.data[self.index_arr[i]] > self.data[self.index_arr[(i - 1) // 2]]: self.swap(i, (i - 1) // 2) i = (i - 1) // 2 def sink_down(self, i): while i < len(self) // 2: l, r = 2 * i + 1, 2 * i + 2 if r < len(self) and self.data[self.index_arr[l]] < self.data[self.index_arr[r]]: l = r if self.data[self.index_arr[i]] < self.data[self.index_arr[l]]: self.swap(i, l) i = l
當append數據的時候,要相應的更新index_arr
def append(self, data): self.data.append(data) self.index_arr.append(len(self)) self.float_up(len(self) - 1)
當移出數據的時候,以前已經提到過存放數據的數組,是按照append的順序進行存儲的,平時操做只是對index_arr的順序進行調整。
若是data_arr爲 42 30 74 60 相應的index_arr應該爲2 3 0 1
這時,當咱們popleft出最大元素時,data_arr中的74被移出後變成了42 30 60,數組中最大索引由3變成了2,若是索引數組中仍然用3這個索引來索引30會形成index溢出。74的索引爲2,須要咱們將索引數在2以後的都減1。
綜上,在刪除元素時,咱們原先是將data_arr中的首尾元素互換,再刪除尾部元素,再對頭部元素進行sink_down操做。如今咱們先換索引數組中首尾元素,再刪除索引數組尾部元素,此時還沒有操做存放data的data_arr,所以索引數組剩餘元素與data_arr的元素還是一一對應的。進行sink_down操做,操做完成以後再刪除data_arr相應位置元素。最後將index_arr中值大於原index_arr頭部元素值的減一。
def pop_left(self): self.swap(0, len(self) - 1) r = self.index_arr.pop() self.sink_down(0) self.data.pop(r) for i, index in enumerate(self.index_arr): if index > r: self.index_arr[i] -= 1 return r
索引堆增長了一個更新操做,能夠隨時更新索引堆中的數據。更新時,先直接更新data_arr中相應索引處的數據,而後在index_arr中,找到存放了data_arr中,剛被更新的數據的索引的索引位置,與刪除時同樣須要進行一次遍歷。找到這個位置以後,因爲沒法肯定與先後元素的大小關係,所以須要進行一次float_up操做再進行一次sink_down操做。
def update(self, i, data): self.data[i] = data for index_index, index in enumerate(self.index_arr): if index == i: target = index_index self.float_up(target) self.sink_down(target)
能夠很明顯看出,這個索引堆在插入元素時是比較快的,可是在刪除元素和更新元素時,爲了查找相應位置索引,都進行了一次遍歷,這是很耗時的操做。爲了能更快的找到index_arr中值爲要更新的data_arr的相應索引值得索引位置,咱們再次開闢一個新的數組to_index,來對index_arr進行索引。
例如對於數組75 54 65 90
此時它的index_arr爲3 0 2 1。當要更新data[3],即90這個元素時,如今要遍歷一遍index_arr來找到3這個位置,這個位置是0。咱們要創建一個to_index,to_index[3]中存放的元素爲0。
index_arr存放的元素分別爲: 1 3 2 0。
先改變swap數組,在交換index_arr中元素時,也交換存放在to_index中的index_arr的索引。
def swap(self, i, j): self.index_arr[i], self.index_arr[j] = self.index_arr[j], self.index_arr[i] self.to_index[self.index_arr[i]], self.to_index[self.index_arr[j]] = self.to_index[self.index_arr[j]], \ self.to_index[self.index_arr[i]]
而後在update中,當要更新位置爲i的元素時,咱們就不須要經過一次遍歷才能找到index_arr中該元素的索引,而是直接經過訪問index_arr[i]便可訪問index_arr中相應索引
def update(self, i, data): self.data[i] = data target = self.to_index[i] self.float_up(target) self.sink_down(target)
最後改變pop_left中相應代碼,這時咱們須要維護三個數組,data_arr,index_arr以及to_index。
仍然是首先將index_arr首位元素交換,並pop出尾部元素存放到i中。而後將頭部元素sink_down到相應位置,而後將pop出data_arr索引i處的元素。而後pop出to_index中索引爲i的元素,再將index_arr中索引溢出的元素進行調整。
def pop_left(self): self.swap(0, len(self) - 1) r = self.index_arr.pop() self.sink_down(0) self.data.pop(r) self.to_index.pop(r) for i in range(r, len(self)): self.index_arr[self.to_index[i]] -= 1 return r
以上就是python實現對和索引堆的具體方式。