堆(heap)這種資料結構對我們來說是極為有用的。因為它可以非常方便地幫我們完成高效的動態的排序。接下來,我將給出堆的原理以及實現堆的具體操作。
先看一下堆的結構,在此,只需要牢記兩點:
1. 堆是一棵完全二叉樹
2. 堆中每個節點都大於等於其任何子節點
所謂完全二叉樹,一定要滿足這個特徵:這棵樹除最後一層外,每一層都是滿的,且最後一層如果不滿,所有節點都位於最左邊。看下圖就明白了:
其中,左圖是堆,而右圖則不是。
所以,如果比較形象地描述堆的話,就是「不留空隙地將元素放入二叉樹中」。
正因為堆跟普通的二叉樹比有這樣的特點,所以,我們可以以按層遍歷的順序將這個二叉樹的所有節點儲存在乙個列表當中。比如上面的左圖,用列表儲存就是:[a, b, c, d, e, f],為什麼可以這樣做呢,因為堆的性質保證了在給出乙個節點索引的前提下,我們能通過公式推出其左孩子,右孩子,以及其父節點(如果有的話)的索引。公式如下:
left_child_index = (2 * cur) + 1
right_child_index = (2 * cur) + 2
par = (cur - 1) // 2
其中,cur為當前節點的索引。驗證一下就知道是沒問題的,比如現在掃瞄到節點a,索引為0,左右孩子b, c的索引分別是1, 2,正好1 = 2 * 0 + 1;2 = 2 * 0 + 2,而b, c的父節點為a:(1 - 1) // 2 = 0, (2 - 1) // 2 = 0
而且,陣列儲存堆肯定比用節點 + 指標的方式要更高效,無論在從空間還是時間上。
現在,我們來看堆的第二個特點:所有節點都大於他的子節點。這個特點保證了在堆的同一棵子樹當中,越大的節點越在上層。顯然,這也保證了堆的最大節點就是二叉樹的根節點。我們把這樣的堆成為「最大堆」,若是所有節點的值都比它的孩子小,我們則稱之為「最小堆」,當然,
一般情況下,不加特別說明的堆都指最大堆。本文當中也是如此,因為最大最小並不影響對堆的原理的分析。
此時,用堆排序的邏輯已經浮現了:假設現在有乙個已經建好的堆,堆中一共有n個節點,我們可以先將堆的根節點(放到列表裡面來看,就是列表的首元素)刪除,這當然就是n個節點當中,值最大的。然後按照堆的規律重新對剩下的節點構建成新的堆(此時新堆中只有n - 1個節點了),再將這個新堆的根節點刪除,得到之前n個節點中值第二大的。。。依次類推,最終可以按照值從大到小的順序輸出這n個節點。
具體的刪除方法如下所示:顯然,刪除堆的根節點是由節點刪除和堆重構兩部分組成的。
1. 將堆的根節點與最後一層葉子節點中的最右端的節點交換位置
2. 將最後這個節點(也就是之前的根節點)刪除
3. 比較現在的新的根節點與他的兩個孩子的大小,若比他的兩個孩子中最大的孩子小,則將這個節點與最大的孩子交換。
4. 反覆執行第3步,直到不能執行為止,新堆構建完畢
3, 4兩步是重構堆的操作,保證了父親節點始終比孩子要大。當然,若你建立的是最小堆,那麼反過來,保證父親比孩子節點小就行。
以上操作也可以用下圖表示:
仿照這種上下交換的原理,也可以設計出將節點新增到堆中的演算法:
1. 將需要新增的節點放置在堆的最末端。如果不滿,則放在最後一層的最右端;如果滿了,則放置在最後一層下一層的最左端(相當於是位二叉樹新建了一層)
2. 若這個節點的值比它的父親大,則與其父親交換,直到不能交換為止(交換到了根節點或者比他的父親小了)
有了新增和刪除兩個演算法,堆的構建就完整了。前面我們已經說過堆的特點使得它方便用列表來表示,接下來就看看如何具體使用列表實現堆。
我是建立了乙個堆的類,乙個資料成員:vector為列表,儲存堆中節點;兩個方法:pop()刪除根節點(也就是列表首元素),add()為堆新增元素
**如下:我設計的方法是在建立堆類時,將乙個列表作為引數
class heap(object):
def __init__(self, elements):
self.vector =
for element in elements:
self.add(element)
def add(self, element):
# 新增新元素到列表末尾
# 找到新新增節點的父親
cur = len(self.vector) - 1
par = (cur - 1) // 2
# 逐層交換
while cur != 0 and self.vector[cur] > self.vector[par]:
self.vector[par], self.vector[cur] = self.vector[cur], self.vector[par]
cur = par
par = (cur - 1) // 2
def pop(self):
# next決定是否還需要進行下一步交換
next = true
# 首尾交換
self.vector[0], self.vector[-1] = self.vector[-1], self.vector[0]
# 將尾元素(其實是根節點)刪除出來
result = self.vector.pop()
cur = 0
# 我們認為只要這一步進行交換了,且還能交換(當前節點還有孩子),就繼續交換
while cur < len(self.vector) and next:
next = false
# 找左右孩子的索引
left_child, right_child = (2 * cur) + 1, (2 * cur) + 2
# 左孩子索引越界,交換終止
if left_child >= len(self.vector):
break
# 右孩子存在
if right_child < len(self.vector):
# max_index:較大孩子的索引
max_index = right_child if self.vector[left_child] < self.vector[right_child] else left_child
if self.vector[cur] < self.vector[max_index]:
self.vector[cur], self.vector[max_index] = self.vector[max_index], self.vector[cur]
cur = max_index
next = true
# 右孩子不存在
elif self.vector[cur] < self.vector[left_child]:
self.vector[cur], self.vector[left_child] = self.vector[left_child], self.vector[cur]
cur = left_child
next = true
# 返回被刪除的值
return result
既然堆有這種最大先出的性質,那麼利用堆,就可以構建優先佇列。大家知道普通的佇列是「先進先出」的原則,而優先佇列則是「最大先出」的原則,也就是說,佇列中的每乙個元素都有乙個權重,權重大的最先出隊。這種思路最常見的應用是醫院排隊,一種原則是讓病情更嚴重的患者最先就診,那如果按照這個原則設計乙個就醫系統的話,優先佇列就是最恰當的選擇。
一樣的,我嘗試建立乙個優先佇列的類,**如下:這裡稍微改動一下,讓優先佇列的建構函式__init__()沒有引數,只能通過新增元素往佇列中新增
import heap
class priorityqueue(object):
def __init__(self):
self.record = heap.heap()
def add_queue(self, element):
self.record.add(element)
def pop_queue(self):
return self.record.pop()
def size(self):
return len(self.record.vector)
資料成員record是乙個堆,三個函式,分別用於新增,刪除和返回佇列大小。 優先佇列與堆
二 堆 1.1 定義 在很多應用中,我們通常需要按照優先順序情況對待處理物件進行處理,比如首先處理優先順序最高的物件,然後處理次高的物件。最簡單的乙個例子就是,在手機上玩遊戲的時候,如果有來電,那麼系統應該優先處理打進來的 在這種情況下,我們的資料結構應該提供兩個最基本的操作,乙個是返回最高優先順序...
堆與優先佇列
分析與思考 陣列是完全二叉樹的儲存結構,完全二叉樹是陣列的邏輯結構,這樣我們就可以使用樹形結構來解決線性問題。堆的插入與刪除 尾部插入,頭部彈出 聯想到了佇列 不同程式語言在實現優先佇列時底層90 是由堆構成的。通過 本身來提高程式設計能力是錯誤的,應注重思維邏輯結構的提公升 資料結構 結構定義 結...
用堆實現優先佇列
堆的性質 1.乙個是他是乙個陣列 當然你也可以真的用鍊錶來做。2.他可以看做乙個完全二叉樹。注意是完全二叉樹。所以他的葉子個數剛好是nsize 2個。3.我使用的下標從1開始,這樣好算,如果節點的位置為i,他的父節點就是i 2,他的左孩子結點就是i 2,右孩子結點就是i 2 1,如果下標從0開始,要...