前言:
這篇文章是基於我看過的一篇論文,主要是關於函數式數據結構,函數式堆(優先級隊列),
我會以自己的理解寫下來,然後論文中出現的代碼將會使用scala這們語言。
論文鏈接: Optimal Purely Functional Priority Queues,另外一個鏈接: 論文。
這裏有個好網站介紹:coursera,全球在線課程,各種課程都有。
scala這們語言的一些學習資料:
scala的教程: scala turorials(文檔和更高階的教程這個網站上都有),
這裏有一個有趣的(講的很有趣值的一看)介紹scala的視頻:
這裏還有scala作者Martin Odersky在youtube上的一個視頻,
主要是介紹scala這個語言是如何應對和處理並行計算所帶來的挑戰(youtube需翻牆):
Martin’s talk at OSCON 2011: Working Hard to Keep it Simple。
正文:
好了,我們回到正文。
我們知道堆可用來實現優先級隊列,或者說堆就是一種優先級隊列。論文中的優先級隊列
(priority queue),其實也就是堆(heap),反正都是差不多的東西,我還是寫成堆吧。
論文中的討論的堆支持以下4個操作:
1、findMin(h) 返回堆h中最小的元素;
2、insert(x,h) 往堆h中插入元素x;
3、meld(h1,h2) 將堆h1和h2融合成一個新堆;
4、deleteMin(h) 從堆h中刪除最小的元素;
論文中首先介紹了二項堆(binomial queue),二項堆對於上述4個操作的時間複雜度是O(log n),
接着我們會用3步來優化該堆,最終的效果是,1、2、3操作的時間複雜度會變爲O(1)。
第一步,引入二項堆的一個變種,斜二項堆(skew binomial queue),該堆通過消除級聯鏈接?
(cascading links)使插入的時間複雜度爲O(1);
第二步,增加一個全局root保存最小的元素,這樣查找最小的元素時間複雜度變爲O(1),後面會看到其實
第三步就包含了第二步,所以可以說只要兩步;
第三步,通過應用一種技術data-structural bootstrapping,通過允許堆中包含另外一個堆從而使合併 (meld操作)兩個堆的時間複雜度變爲 O(1)。
下面會詳細解釋。
堆的抽象定義:
首先來看堆的抽象定義(引用自coursera的課程Principles of Reactive Programming的week 1的作業 ):
//對應論文第三頁,Figure 1
trait Heap {
type H // 代表堆的類型
type A // 代表每個元素的類型
def ord: Ordering[A] // 對元素的排序方式
def empty: H // 空堆
def isEmpty( h: H ): Boolean // 判斷堆h是否爲空
def insert( x: A, h: H ): H // 向堆h中插入元素x
def meld( h1: H, h2: H ): H // 合併堆h1和h2
def findMin( h: H ): A // 堆h中的最小元素
def deleteMin( h: H ): H // 刪除堆中的最小元素
}
//當應用到其他元素類型時,只需要類似以下定義多一個trait
trait IntHeap extends Heap {
override type A = Int
override def ord = scala.math.Ordering.Int
}
二項堆(binomial queue):
由於二項堆由二項樹組成,所以我們先來看看二項樹的定義。
二項樹(binomial tree):
1、rank爲0的二項樹只有一個節點;
2、rank爲r+1的二項樹由兩個rank爲r的二項樹鏈接而成,鏈接的方法是一個二項樹作爲另外一個二項樹的
最左子樹。
下面用圖片舉例:
以rank3的二項樹爲例,我們可以看到藍色虛線框和藍色實線框都是rank爲2的二項樹,其中一個
是另外一個的最左子樹。然後從二項樹的定義可以知道,rank爲r的二項樹包含2^r(2的r次方)個
節點。
二項樹的另外一個等價定義:
一個rank爲r的二項樹相當於是一個節點,該節點有r個子節點t1 ... tr,對於ti(1 <= i <= r)
是一個rank爲r - i的二項樹。對照上圖很容易明白。
然後假設一棵二項樹是堆有序的,當該樹的每個節點都比自己的子節點要小。爲了保存堆順序,
鏈接兩棵堆有序二項樹時,選擇根較大的二項樹作爲根較小的二項樹的最左子樹。
然後我們回到二項堆,一個二項堆就是一個堆有序的二項樹集合,該集合中每棵二項樹的rank都
不一樣,而且堆中的二項樹以升序排列。
由於我們知道一棵rank爲r的二項樹 包含2^r(2的r次方)個節點,而結合二項堆的定義,我們可以
推出,一個包含n個節點的二項堆所包含的二項樹就對應n的二進制表達形式中的的1。
比如:我們來看看21個節點的二項堆,由於21的二進制表達形式爲:10101,所以該堆包含rank爲
0、2、4 ,三棵二項樹(對應的節點數分別爲1、4、16,加起來就是21)。
而一個節點數爲n的二項堆最多包含 [ log(n + 1) ] 棵二項樹。
現在可以來描述一下二項堆的操作,因爲堆中的所有二項樹都是堆有序的,所以可以得出堆中的
最小元(findMin)是某棵二項樹的樹根。所以只要把堆中的二項樹的根都遍歷一遍就知道最小的元素了,
所以時間複雜度爲O(log n)。對於插入一個元素(insert)的操作,首先創建一個只有一個節點的樹也就是
rank爲0的二項樹,然後相當於向一個二進制數加一一樣,如果堆中已經存在rank爲0的二項樹,則將這
兩個二項樹鏈接起來,然後繼續將rank相同的二項樹鏈接,相當於加一之後的進位,如果沒有rank爲0的
二項樹,則直接將該節點放到二項樹列表的頭部。最差的時間復雜度爲 O(log n),即是一個節點數爲n的
二項堆,n = 2^k - 1,相當於二進制位全部爲一,這時加一會導致k次進位,也就是要做k次鏈接,
這也是後面要講到的斜二項堆會優化的方面,通過消除這種情況,使插入的時間複雜度爲O(1)。
容易推出,合併(meld)兩個二項堆就相當於兩個二進制數相加,升序遍歷兩個堆的樹然後將rank
相同的樹鏈接在一起,一次鏈接就相當於一次進位,時間複雜度也是(log n)。
最後到deleteMin操作,首先遍歷堆中的樹根,找到根最小的二項樹,然後刪除該節點,返回該樹
的子樹,由於子樹是以降序排列的,所以要反轉順序,然後該被刪除的樹的子樹也組成一個二項堆,
於是剩下的操作就是將該堆和原來的堆合併,找樹和合並都需要O(log n)的時間,所以總共需要
O(log n)的時間複雜度。
上圖解釋插入操作:
這個圖解釋了插入操作的過程,還有上面的說到的,k次鏈接問題(當n=2^k-1)。
合併兩個堆和刪除最小的情況參照上圖讀者自己能畫出來。
二項堆的定義:
二項堆的定義(引用自coursera的課程Principles of Reactive Programming的week 1的作業 ):
// 對應論文Figure 3, 第七頁 7
trait BinomialHeap extends Heap {
type Rank = Int
case class Node( x: A, r: Rank, c: List[Node] ) //定義節點,也就是二項樹
override type H = List[Node] //堆的定義,是由二項樹組成的列表
protected def root( t: Node ) = t.x //取得樹根的值
protected def rank( t: Node ) = t.r //取得樹的rank
//鏈接兩棵rank相同的樹,樹根值大的元素作爲樹根值小的元素的最左子樹
protected def link( t1: Node, t2: Node ): Node = // t1.r==t2.r
if ( ord.lteq( t1.x, t2.x ) )
Node( t1.x, t1.r + 1, t2 :: t1.c )
else
Node( t2.x, t2.r + 1, t1 :: t2.c )
//往堆中插入一棵樹
protected def ins( t: Node, ts: H ): H = ts match {
case Nil => List(t) //若堆爲空,直接返回只有一棵樹的堆
case tp :: ts => //其實認真想一下只有t.r <= tp.r的情況出現,所以如果t.r不小於tp.r,
//則t.r == tp.r,所以將之鏈接起來,然後插入到堆ts中
if ( t.r < tp.r ) t :: tp :: ts else ins( link( t, tp ), ts )
}
override def empty = Nil //空堆
override def isEmpty(ts: H) = ts.isEmpty //判斷堆是否爲空
//往堆中插入一個元素,insert函數和ins函數有點令人困惑,論文中說了,這幾乎是
//所有的二項樹實現都有的問題
override def insert( x: A, ts: H ) = ins( Node( x, 0, Nil ), ts )
//合併兩棵樹
override def meld( ts1: H, ts2: H ) = ( ts1, ts2 ) match {
case ( Nil, ts ) => ts
case ( ts, Nil ) => ts
case ( t1 :: ts1, t2 :: ts2 ) =>
if ( t1.r < t2.r ) t1 :: meld( ts1, t2 :: ts2 ) //二進制數相加一樣
else if ( t2.r < t1.r ) t2 :: meld( t1 :: ts1, ts2 )
else ins( link( t1, t2 ), meld( ts1, ts2 ) )
//當找到兩個rank相同的樹,則先鏈接兩棵樹,然後
//將鏈接後得到的樹插入到剩下已經合併的堆中。
}
//找最小元素
override def findMin( ts: H ) = ts match {
case Nil => throw new NoSuchElementException( "min of empty heap" )
case t :: Nil => root(t)
case t :: ts =>
val x = findMin(ts)
if ( ord.lteq( root(t), x ) ) root(t) else x
}
//刪除最小元素
override def deleteMin( ts: H ) = ts match {
case Nil => throw new NoSuchElementException( "delete min of empty heap" )
case t :: ts =>
def getMin( t: Node, ts: H ): ( Node, H ) = ts match {
//輔助函數,找到堆中最小的樹,返回值
case Nil => ( t, Nil ) //Node代表根最小的樹,H是已經刪除Node
case tp :: tsp => //的堆,雖然是遞歸看的有點頭暈,但其實就是
//從列表的最後一棵樹開始遍歷到頭,相鄰兩
//棵樹互相比較,每次比較最小的樹就是tq
//最後回溯到頭就找到根最小的樹了
val ( tq, tsq ) = getMin( tp, tsp )
if ( ord.lteq( root(t), root(tq) ) ) (t, ts)
else ( tq, t :: tsq )
}
val ( Node( _, _, c ), tsq ) = getMin( t, ts )
meld( c.reverse, tsq ) //將被刪除節點的子樹和剩下的樹合併
}
}
好,二項樹就介紹完了,part two的內容是介紹斜二項堆,斜二項樹(解決k次鏈接,使得插入操作的時間複雜度
爲O(log n))等內容。