純函數式堆(純函數式優先級隊列)part one ----二項堆 原

前言:

這篇文章是基於我看過的一篇論文,主要是關於函數式數據結構,函數式堆(優先級隊列),

我會以自己的理解寫下來,然後論文中出現的代碼將會使用scala這們語言。

論文鏈接:   Optimal Purely Functional Priority Queues,另外一個鏈接:   論文。    

 

這裏有個好網站介紹:coursera,全球在線課程,各種課程都有。    

 

scala這們語言的一些學習資料:    

scala的教程:   scala turorials(文檔和更高階的教程這個網站上都有),

這裏有一個有趣的(講的很有趣值的一看)介紹scala的視頻:

Scala for the Intrigued

 

這裏還有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))等內容。

 

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