樹形結構和堆
技術標籤:python develop
樹形結構和堆
可以改善優先佇列的操作效能
一.線性和樹形結構
首先分析效率低的原因:
-
按序插入操作低效,其根源是需要沿著表順序檢索插入位置,對於順序表,需要移動O(n)個元素,對於連結表,需要順著連結爬行O(n)步.
-
如果不改變資料的線性順序儲存方式,就不可能突破O(n)的複雜度限制, 要做出操作效率更高的優先佇列,必須考慮其他資料結構組織方式
-
利用樹形結構的祖先/子孫序,有可能得到更好的操作效率
一般而言,確定最優先元素並不需要與所有其他元素比較,以體育比賽中的淘汰賽為例,假設有n名選手參加,首先需要進行N-1場比賽確定冠軍,每個選手只需進行約log2
二. 堆及其性質
採用樹形結構實現優先佇列的一種有效技術稱為堆,從結構上看,堆就是結點裡儲存資料的完全二叉樹,但堆中資料的儲存要滿足一種特殊的堆序:任一個結點裡所存的資料(按所考慮的序)先於或等於其子結點(如果存在)裡的資料
- 在一個堆中從樹根到任何一個葉結點的路徑上,各結點裡所存的資料按規定的優先關係(非嚴格)遞減
- 堆中最優先的元素必定位於二叉樹的根結點裡(堆頂), O(1)時間就能得到
- 位於樹中不同路徑上的元素,這裡不關心其順序關係
如果所要求的序是最小元素優先,構造出來的堆就是小頂堆,反之就是大頂堆.
上圖是一個堆的形狀(也就是完全二叉樹的形狀),以及堆中一條路徑的情況,除最下一層右邊可能有所欠缺,堆裡各層結點全滿.圖中從根到葉的路徑上越小的圓圈
一棵完全二叉樹可以自然而且資訊完全地存入一個連續的線性結構(例如連續表),一個堆也可以自然地存入一個連續表,通過下標就方便地找到樹中任一結點的父結點/子結點
堆和二叉樹有以下性質:
Q1:在一個堆的最後加上一個元素(在相應連續表的最後加一個元素),整個結構還是可以看作一棵完全二叉樹,但它未必是堆(最後元素未必滿足堆序)
Q2:一個堆去掉堆頂(表中位置0的元素), 其餘元素形成兩個"子堆", 完全二叉樹的子結點/父結點下標計算規則仍然適用,堆序在路徑上仍然成立
Q3:給由Q2得到的表(兩個子堆)加入一個根元素(存入位置0),得到的結點序列又可看作完全二叉樹,但它未必是堆(根結點未必滿足堆序)
Q4: 去掉一個堆中最後的元素(最下層的最右結點,也就是對應的連續表裡的最後一個元素), 剩下的元素仍構成一個堆
堆實現優先佇列
- 用堆作為優先佇列,可以直接得到堆中的最優先元素,O(1)
- 插入元素的操作(向上篩選):想堆(優先佇列)中加入一個新元素, 必須能得到一個包含了所有原有元素和剛加入的新元素的堆, O(logn)
- 從堆中彈出最小元素操作(向下篩選),從堆中彈出最小元素後,必須能把剩下的元素重新做成堆,O(logn)
三. 優先佇列的堆實現
(一) 插入元素和向上篩選
根據Q1, 在一個堆的最後加入一個元素,得到的結果還是完全二叉樹,但未必是堆,為了把這樣的完全二叉樹恢復為堆,只需做一次向上篩選
向上篩選的方法:
不斷用新加入的元素(設成e)與其父結點的資料進行比較,如果e較小就交換兩個元素的位置,通過這樣的比較和交換,元素e不斷上移,這一操作一直做到e的父結點的資料小於等於e時,或者e已經到達根結點時停止,這時經過e的所有路徑上的元素滿足所需順序,其餘路徑仍保持有序,因此這棵完全二叉樹滿足堆序
- 把新加入的元素放在(連續表裡)已有元素之後,執行一次向上篩選操作
- 向上篩選操作中比較和交換的次數不會超過二叉樹中最長路徑的長度,因此插入元素操作可以在O(logn)時間完成
(二)彈出元素和向下篩選
彈出堆頂元素,從原堆的最後取下一個元素,放在堆頂就得到了一棵完全二叉樹,現在除了堆頂元素可能不滿足堆序外,其餘元素都滿足堆序, 現在需要設法把結構重新恢復為一個堆
- 用e與A,B兩個"子堆"的頂元素(根)比較,最小者作為整個堆的頂
- 若e不是最小,最小的比為A或B的根,設A的根最小,將其移到堆頂,相當於刪掉了A的頂元素
- 將e放入去掉堆頂的A,這是規模更小的同一問題
- B的根最小的情況同樣處理
- 如果某次比較重e最小,以它為頂的區域性樹已經成為堆,整個結構也成為堆
- 或者e已經落到底,這時它自身就是一個堆,整個結構也成為堆
總結:
- 彈出堆頂 O(1)
- 從堆最後取一個元素作為完全二叉樹的根 O(1)
- 執行一次向下篩選 O (logn), 操作次數不超過樹中路徑的長度
下面用python實現基於堆的優先佇列類,用list儲存元素,應該在表尾端加入元素,以首端作為堆頂
class PrioQueue:
"""
implementing priority queues using heaps
"""
def __init__(self, elist=[]):
self._elems = list(elist)
if elist:
self.buildheap()
def is_empty(self):
return not self._elems
def peek(self):
if self.is_empty():
raise PrioQueueError("in peek")
return self._elems[0]
def enqueue(self, e):
self._elems.append(None) # add a dummy element
self.siftup(e,len(self._elems)-1)
def shiftup(self, e, last):
elems, i, j = self._elems, last, (last-1) // 2
while i > 0 and e < elems[j]:
elems[i] = elems[j]
i, j = j, (j-1)//2
elems[i] = e
def dequeue(self):
if self.is_empty():
raise PrioQueueError("in dequeue")
elems = self._elems
e0 = elems[0]
e = elems.pop()
if len(elems) > 0:
self.shiftdown(e, 0, len(elems))
return e0
def shiftdown(self, e, begin, end):
elems, i, j = self._elems, begin, begin*2+1
while j < end:
if j+1 < end and elems[j+1] < elems[j]:
j += 1
if e < elems[j]:
break
elems[i] = elems[j]
i, j = j, 2*j+1
elems[i] = e
def buildheap(self):
end = len(self._elems)
for i in range(end//2, -1, -1):
self.shiftdowm(sele._elems[i], i, end)
list(elist)從elist出發做出一個表拷貝的意義:
- 做拷貝使內部的表脫離原來的表,排除共享
- 對預設情況也建立一個新的空表,避免了以可變物件作為預設值的python程式設計陷阱
在shiftup的實現裡,並沒有先存入元素後再考慮交換,而是"拿著它"去查詢正確插入的位置,迴圈條件保證跳出的元素都是優先度較低的元素,在檢查過程中把它們逐個下移
總結:
- 基於堆的概念實現優先佇列,建立操作的時間複雜度是O(n), 這件事只需要做一次,
- 插入和彈出操作的複雜度是O(logn).插入操作的第一步是在表的最後加入一個元素,可能導致list物件替換元素儲存區,因此可能出現O(n)的最壞情況
- 所有操作中都只用了一個簡單變數,沒用其他結構,所以空間複雜度都是O(1)
區,因此可能出現O(n)的最壞情況
3. 所有操作中都只用了一個簡單變數,沒用其他結構,所以空間複雜度都是O(1)