優先順序佇列之堆的分析與實現

2022-07-12 08:15:10 字數 3861 閱讀 8554

在現實應用中,我們有這樣一種需求,就是選取出當前佇列中優先順序最高的元素,比如作業系統中的執行緒排程,當前執行緒時間片用完的時候,需要從就緒佇列中選出優先順序最高的執行緒,對於乙個無序佇列,我們需要遍歷所有的元素,那麼時間複雜度就是o(n)。研究優先順序佇列的目的就是找到一種資料結構和對應的演算法,實現高效的動態和靜態操作。這裡的動態操作指的是插入和刪除元素,靜態操作指的是查詢。需要注意的是,刪除操作和查詢操作都只是針對優先順序最高的元素。於是我們可以給出乙個優先順序佇列的虛基類的定義。

template struct pq//其實這只是一種adt(abstract data type),需要根據不同的應用場景具體實現,對應的效率也不相同
優先順序佇列只是一種概念上的範圍,我們可以有多種實現方法,這裡介紹幾個最簡單的方法,分別用向量、有序向量、bbst(balanced binary search tree,平衡二叉查詢樹)來實現pq(priority queue,下面簡稱pq),下表列出了插入、刪除和查詢優先順序最高元素的時間複雜度。

實現型別\抽象介面

insert()

getmax()

delmax()

向量o(1)

o(n)

o(n)

有序向量

o(n)

o(1)

o(1)

bbst

o(logn)

o(logn)

o(logn)

這裡簡單說一下為什麼時間複雜度如上表所示。對於無序向量來說,插入元素很簡單,直接插到末尾就行,所以是o(1),getmax和delmax都需要遍歷所有元素,其中delmax在刪除元素之後還要移動後續元素,可以說,delmax在最壞情況下需要做接近2n次操作,但是在近似意義下都是o(n)。對於有序向量來說,我們可以按從小到大排序,這時候插入元素在最壞情況下需要移動接近n個元素,所以是o(n),而getmax和delmax都可以直接對最後乙個元素操作(按照從小到大排序,最後乙個元素優先順序最高),所以是o(1)。bbst比較複雜,我們知道對於bbst來說,我們是search,insert還是remove介面(bbst的三個標準介面,在實現insert,getmax和delmax時可以直接呼叫)的時間複雜度都是o(logn),但是bbst本身實現起來過於複雜,而且它相對於pq來說功能過於強大,比方說,對於insert介面來說兩者功能類似,但是對於getmax和delmax來說,pq只需要一種偏序關係,即pq只需要對優先順序最高的元素進行操作,但是bbst可以對任何元素操作。我們希望找到一種簡單的資料結構,它既可以容易實現與維護,又能高效實現上述三個功能介面,下面我們介紹完全二叉堆。

完全二叉堆實際上就是在上述需求背景下產生的,事實上它同時結合了向量的形和樹的神,向量的形是指它的儲存結構和向量一樣,樹的神是指從邏輯上我們把它理解成樹。乙個向量如果想理解成樹,事實上只有在完全二叉樹的情況下才有可能,這時樹是按層次遍歷的順序存在向量裡的,完全二叉堆的乙個重要特徵是堆序性,簡單來說就是父節點一定不小於子節點,因此整個堆中的最大節點就一定是根節點,所以getmax介面非常容易實現,只需要返回_elem[0]即可,時間複雜度是o(1),接下來我們分析框架類裡實現的另兩種介面的實現方案和實現效率。

對於insert(),實際上是乙個上濾的過程,新插入的節點首先放在向量的末尾,相當於在完全二叉樹的最後乙個節點,如果它有父親節點的話,就和父親節點比較,如果大於父親節點就互換,然後繼續和新的父親節點比較直至小於新的父節點或者抵達根節點,下面是對應**,首先我們先給出用完全二叉堆的類定義。

template class pq_complhead:public pq,public vector;

t delmax();

}

接下來我們來看insert介面的實現

template void pq_comlheap::insert(t e)

template int pq_complheap::percolateup(int i)

return i;

}

接下來分析刪除操作,刪除操作只是針對最大元素,所以只需要刪除掉_elem[0]即可,問題的關鍵是,在刪除掉_elem[0]之後如何保持堆序性。原理是:首先將最後乙個節點放在根節點的位置,然後執行下濾操作。無論是上濾操作還是下濾操作,雖然不是一次就能完成,但是在操作的過程中能保證單調性,即問題始終在逐漸簡化,下面給出**。

template t pq_complheap::delmax()
template int pq_complhead::percolatedown(int n,int i)

return i;

}

ok,到這裡完全二叉堆的基本框架和演算法時間都完成了,下面我們分析效能。

這裡我們和之前的三種實現方式做以對比,這裡先給出時間複雜度,再介紹這個時間複雜度是如何算出來的。

實現型別\抽象介面

insert()

getmax()

delmax()

向量o(1)

o(n)

o(n)

有序向量

o(n)

o(1)

o(1)

bbst

o(logn)

o(logn)

o(logn)

完全二叉堆

o(logn)

o(1)

o(logn)

對於getmax來說,時間複雜度是o(1)應該很好理解,因為只需要取_elem[0]就行了,insert操作其實最主要的就是乙個上濾過程,這個過程每次都會至少上公升一層,每次上公升只需要做常數次比較操作,而完全二叉堆是嚴格意義上最多只有logn層,所以它的時間複雜度是o(logn)。其實delmax()操作和insert類似,需要做乙個下濾操作,在每層也需要做常數次比較操作,然後下降一層,在最壞情況下也不過是下降logn層,所以也是o(logn)。

由堆衍生而來的堆排序演算法思路也非常簡單,不過是每次取出最大的元素然後輸出就可以了,每次取出最大元素都要執行一次getmax()和delmax(),時間複雜度為o(logn+1),也就是o(logn),一共執行n次,所以總體的時間複雜度是o(n*logn),這也是全排序演算法中效率最高的情況了,當然我們可以對它做常數意義下的優化。

接下來我們介紹另一種堆,它是為了彌補二叉完全堆在某些特定應用場景下的不足應運而生的。

左式堆是為了高效的實現兩個堆的合併(merge)操作,設想一下,如果用完全二叉堆我們如何實現兩個堆的合併呢?假設有兩個堆,堆a和堆b,我們可以首先判對一下它們的長度,不失一般性,假設size(a)>size(b),一種很簡單的演算法就是不斷輸出b堆的資料插入到a堆中去,每次插入的時間複雜度都是logn,假設b堆有m個元素,那就是mlogn,一般我們認為n和m是同階的,那就是o(nlogn),這種效能開銷我們是無法接受的,因為我們把堆a和堆b中的元素混搭在一起,重新排序也就是o(n*logn)的時間複雜度,而且堆是一種偏序關係(偏序關係體現在只要能找到最大或最小元素即可),那麼它的開銷應該小於全排序才行,所以我們引入左式堆。

這裡我們引入乙個npl的概念,npl是null path length的縮寫,它是指對應節點到外部節點的最近距離,這裡引入兩條規則:

npl(null)=0;

npl(x)=1+min(npl(lc(x)),npl(rc(x)));

我們首先給出左式堆的類定義,**如下。

template class pq_leftheap:public pq,public bintree

t delmax();

}

在滿足左傾性的情況下,我們給出一種遞迴的合併演算法的實現,**如下。

template static binnodeposi(t) merge(binnodeposi(t) a,binnodeposi(t) b)
先介紹到這兒吧,有機會我在詳細展開,讀者可以自己畫一些圖幫助理解一下。

優先順序佇列之堆實現

優先順序佇列 priority queue 是0個或多個元素的集合,每個元素都有乙個優先權或值。優先順序佇列中,元素的出佇列順序不是同普通佇列一樣fifo 先進先出 而是按照優先順序遞增或遞減順序,但不是元素進入佇列的順序。堆 heap 陣列表示 是實現優先順序佇列效率很高的資料結構。堆即使完全二叉...

優先順序佇列(堆實現)

一 優先順序佇列定義 二 方法實現 獲得最大元素方法 去掉最大元素方法 修改優先順序方法 新增節點 三 實現 用堆實現乙個優先順序佇列 主要是新增 修改 刪除節點 節點具有唯一性 author hhf 2014年11月28日 public class priorityqueue 返回優先佇列中優先順...

優先順序佇列(堆實現)

優先順序佇列 概念 一般來說我們會根據事情的重要程度優先處理某事,比如完成學習任務和刷微博,我們會認為完成學習任務比較重要,因此會先執行它,因此在這種情況下,資料結構就應提供兩個基本的操作,一是返回最高優先順序物件,二是新增新的物件,這種資料結構就叫做優先順序佇列。二叉樹的順序儲存 儲存方式 使用陣...