python算法系列-堆隊列算法(heapq)

在這裏插入圖片描述
python版本:3.7.0
系統版本:win10專業版(1909)

heapq模塊提供了堆隊列算法的實現,也稱爲優先隊列算法。

一、堆簡介

堆是一個二叉樹,它的每個父節點的值都只會小於或大於所有孩子節點。它使用了數組來實現:從零開始計數,對於所有的 k ,都有heap[k]<=heap[2k+1]heap[k] <= heap[2*k+1]heap[k]<=heap[2k+2]heap[k] <= heap[2*k+2] 。爲了便於比較,不存在的元素被認爲是無限大。堆最有趣的特性在於最小的元素總是在根結點:heap[0] 。

二、實現方法簡介

  1. 實現方法的不同:
  2. 這個堆算法的實現和常規的不按天一樣不太一樣,在於兩方面:
  • (a)使用了基於零開始的索引。這使得節點和其孩子節點之間的索引關係不太直觀,但是由於Python使用了從零開始的索引,所以這樣做更加合適。
  • (b)我們的 pop 方法返回了最小的元素,而不是最大的(這在教材中叫做 “最小堆”;而“最大堆”在課本中更加常見,因爲它更加適用於原地排序)。

基於這兩方面,我們可以把堆看作原生的Python list: heap[0] 表示最小的元素,同時 heap.sort() 維護了堆的不變性!

  1. 創建一個堆.
    要創建一個堆,可以使用list來初始化爲 [] ,或者你可以通過一個函數 heapify() ,來把一個list轉換成堆。

3.模塊中定義了以下函數:

  • heapq.heappush(heap, item)
    將 item 的值加入 heap 中,保持堆的不變性。

  • heapq.heappop(heap)
    彈出並返回 heap 的最小的元素,保持堆的不變性。如果堆爲空,拋出 IndexError 。使用 heap[0] ,可以只訪問最小的元素而不彈出它。

  • heapq.heappushpop(heap, item)
    將 item 放入堆中,然後彈出並返回 heap 的最小元素。該組合操作比先調用 heappush() 再調用 heappop() 運行起來更有效率。

  • heapq.heapify(x)
    將list x 轉換成堆,原地,線性時間內。

  • heapq.heapreplace(heap, item)
    彈出並返回 heap 中最小的一項,同時推入新的 item。 堆的大小不變。 如果堆爲空則引發 IndexError。

這個單步驟操作比 heappop() 加 heappush() 更高效,並且在使用固定大小的堆時更爲適宜。 pop/push 組合總是會從堆中返回一個元素並將其替換爲 item。

返回的值可能會比添加的 item 更大。 如果不希望如此,可考慮改用 heappushpop()。 它的 push/pop 組合會返回兩個值中較小的一個,將較大的值留在堆中。

三、通用功能函數

  1. heapq.merge(*iterables, key=None, reverse=False)
    將多個已排序的輸入合併爲一個已排序的輸出(例如,合併來自多個日誌文件的帶時間戳的條目)。 返回已排序值的 iterator。

類似於 sorted(itertools.chain(*iterables)) 但返回一個可迭代對象,不會一次性地將數據全部放入內存,並假定每個輸入流都是已排序的(從小到大)。

具有兩個可選參數,它們都必須指定爲關鍵字參數。

key 指定帶有單個參數的 key function,用於從每個輸入元素中提取比較鍵。 默認值爲 None (直接比較元素)。

reverse 爲一個布爾值。 如果設爲 True,則輸入元素將按比較結果逆序進行合併。 要達成與 sorted(itertools.chain(*iterables), reverse=True) 類似的行爲,所有可迭代對象必須是已從大到小排序的。

  1. heapq.nlargest(n, iterable, key=None)
    從 iterable 所定義的數據集中返回前 n 個最大的元素。 如果提供了 key 則其應指定一個單參數的函數,用於從 that is used to extract a comparison key from each element in iterable 的每個元素中提取比較鍵 (例如 key=str.lower)。 等價於: sorted(iterable, key=key, reverse=True)[:n]。

  2. heapq.nsmallest(n, iterable, key=None)
    從 iterable 所定義的數據集中返回前 n 個最小元素組成的列表。 如果提供了 key 則其應指定一個單參數的函數,用於從 iterable 的每個元素中提取比較鍵 (例如 key=str.lower)。 等價於: sorted(iterable, key=key)[:n]。

注意:
後兩個函數在 n 值較小時性能最好。 對於更大的值,使用 sorted() 函數會更有效率。 此外,當 n==1 時,使用內置的 min() 和 max() 函數會更有效率。 如果需要重複使用這些函數,請考慮將可迭代對象轉爲真正的堆。

四、基本使用示例子

  1. 堆排序 可以通過將所有值推入堆中然後每次彈出一個最小值項來實現。
import heapq
def  heapsort(iterable):
 	h = []
 	for value in iterable:
 		heapq.heappush(h, value)
 	return [heapq.heappop(h) for i in range(len(h))]
a = heapsort([1,3,2,4,6,5,7,9,8])
print(a)
#輸出結果:[1, 2, 3, 4, 5, 6, 7, 8, 9]

這類似於 sorted(iterable),

2. 堆元素可以爲元組。 這適用於將比較值(例如任務優先級)與跟蹤的主記錄進行賦值的場景:
```python
import heapq
h = []
heapq.heappush(h, (5, 'write code'))
heapq.heappush(h, (7, 'release product'))
heapq.heappush(h, (1, 'write spec'))
heapq.heappush(h, (3, 'create tests'))
heapq.heappop(h)
print(h)
#輸出結果:[(3, 'create tests'), (7, 'release product'), (5, 'write code')]

五、理論補充

堆是通過數組來實現的,其中的元素從 0 開始計數,對於所有的 k 都有 a[k] <= a[2k+1] 且 a[k] <= a[2k+2]。 爲了便於比較,不存在的元素被視爲無窮大。 堆最有趣的特性在於 a[0] 總是其中最小的元素。

上面的特殊不變量是用來作爲一場錦標賽的高效內存表示。 下面的數字是 k 而不是 a[k]:
在這裏插入圖片描述
在上面的樹中,每個 k 單元都位於 2k+1 和 2k+2 之上。 體育運動中我們經常見到二元錦標賽模式,每個勝者單元都位於另兩個單元之上,並且我們可以沿着樹形圖向下追溯勝者所遇到的所有對手。 但是,在許多采用這種錦標賽模式的計算機應用程序中,我們並不需要追溯勝者的歷史。 爲了獲得更高的內存利用效率,當一個勝者晉級時,我們會用較低層級的另一條目來替代它,因此規則變爲一個單元和它之下的兩個單元包含三個不同條目,上方單元“勝過”了兩個下方單元。

如果此堆的不變量始終受到保護,則序號 0 顯然是最後的贏家。 刪除它並找出“下一個”贏家的最簡單算法方式是家某個輸家(讓我們假定是上圖中的 30 號單元)移至 0 號位置,然後將這個新的 0 號沿樹下行,不斷進行值的交換,直到不變量重新建立。 這顯然會是樹中條目總數的對數。 通過迭代所有條目,你將得到一個 O(n log n) 複雜度的排序。

此排序有一個很好的特性就是你可以在排序進行期間高效地插入新條目,前提是插入的條目不比你最近取出的 0 號元素“更好”。 這在模擬上下文時特別有用,在這種情況下樹保存的是所有傳入事件,“勝出”條件是最小調度時間。 當一個事件將其他事件排入執行計劃時,它們的調試時間向未來方向延長,這樣它們可方便地入堆。 因此,堆結構很適宜用來實現調度器,我的 MIDI 音序器就是用的這個 😃。

用於實現調度器的各種結構都得到了充分的研究,堆是非常適宜的一種,因爲它們的速度相當快,並且幾乎是恆定的,最壞的情況與平均情況沒有太大差別。 雖然還存在其他總體而言更高效的實現方式,但其最壞的情況卻可能非常糟糕。

堆在大磁盤排序中也非常有用。 你應該已經瞭解大規模排序會有多個“運行輪次”(即預排序的序列,其大小通常與 CPU 內存容量相關),隨後這些輪次會進入合併通道,輪次合併的組織往往非常巧妙 1。 非常重要的一點是初始排序應產生儘可能長的運行輪次。 錦標賽模式是達成此目標的好辦法。 如果你使用全部有用內存來進行錦標賽,替換和安排恰好適合當前運行輪次的條目,你將可以對於隨機輸入生成兩倍於內存大小的運行輪次,對於模糊排序的輸入還會有更好的效果。

另外,如果你輸出磁盤上的第 0 個條目並獲得一個可能不適合當前錦標賽的輸入(因爲其值要“勝過”上一個輸出值),它無法被放入堆中,因此堆的尺寸將縮小。 被釋放的內存可以被巧妙地立即重用以逐步構建第二個堆,其增長速度與第一個堆的縮減速度正好相同。 當第一個堆完全消失時,你可以切換新堆並啓動新的運行輪次。 這樣做既聰明又高效!

總之,堆是值得了解的有用內存結構。 我在一些應用中用到了它們,並且認爲保留一個 ‘heap’ 模塊是很有意義的

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