本文首發於我的公眾號 Linux雲計算網絡(id: cloud_dev) ,專注於干貨分享,號內有 10T 書籍和視頻資源,后台回復 「1024」 即可領取,歡迎大家關注,二維碼文末可以掃。
《算法導論》第二版中在討論斐波那契堆之前還討論了二項堆,但是第三版中已經把這塊的內容放到思考題中,究極原因我想大概是二項堆只是個引子,目的是為了引出斐波那契堆,便於理解,而且許多經典的算法實現都是基於斐波那契堆,譬如計算最小生成樹問題和尋找單源最短路徑問題等,此時再把二項堆單獨作為一章來講顯然沒有必要。類似的堆結構還有很多,如左傾堆,斜堆,二項堆等,下次我打算開一篇博客來記錄下它們的異同點。
一、攤還分析(第十七章)
這些高級的數據結構的性能分析一般是基於一個技術——攤還分析,可以理解成一種時間復雜度的分析方法。它的提出是基於這樣一個事實:並不能總以最壞情況來衡量一個算法的性能,因為最壞情況並不總會發生,而且在絕大多數應用中最壞情況出現的次數是極小的。從字面意思上理解這種分析方法,就是將一系列操作所需要的時間平均分攤到每一個操作上,使得每一個操作,在最壞情況下,具有平均性能。這樣的分析方法看上去有點像平均情況分析,但其實有着本質的不同,其一,攤還分析是通過求一系列操作的平均時間來評價某一操作的性能,即使其中某一操作的代價很高,都能保證它們具有較低的平均性能,而平均情況分析則是單獨針對某一操作來評價;其二,攤還分析不涉及概率計算,只是對一個整體求平均值,而平均情況則不然,代價高的操作自然占總開銷的概率就大。
要理解的是,攤還分析強調的是一個“操作序列”,一般低級的數據結構操作都用不到這種分析方法,而高級的數據結構,說白了,就是結合多種低級數據結構形成的,因此在對這些數據結構進行操作時,涉及到一個序列的操作,而其中可能存在每個操作的時間復雜度都不一樣,有的很高,有的又幾乎可以忽略,所以采用這種攤還分析方法更加適合分析這些數據結構的操作性能。就以本文將要說的斐波那契堆來說,這種堆結構是由“堆排序”中所用到的最小堆組成,至於為什么叫這個名字,是由斐波那契堆上每個節點的度所決定的——其具有斐波那契數列的性質(具體可以看書本的推導)。另外,再來看一個書上提出的例子——棧操作:
1 Multi-Pop(S, k) 2 while not Stack-Empty(S) and k > 0 3 POP(S) 4 k = k - 1
其中,POP()操作的代價是1,假設棧的大小最大為n,一般的時間復雜度的分析方法是:Multi-Pop()操作在最壞情況下的代價為O(n),那么假設有n個這樣的操作,則最壞情況下時間復雜度就為O(n^2),雖然這種分析方法是正確的,但通過單獨分析每個操作的最壞情況代價得到的操作序列的最壞情況時間O(n^2)並不是一個確界。如果使用攤還分析方法(方法有三種,具體可以查看書本,此處不做詳述),則可以得到更好的上界。分析如下:從整體上來看,n個Multi-Pop()序列的操作,最多進行n次,因為POP()的次數最多與棧的大小相等,有多少個元素就POP多少次,即使有n次的操作也是如此。所以,n次序列的操作,代價至多為O(n),任意一個操作的平均時間就為O(n)/n=O(1),這個就是Multi-Pop()的攤還代價。
二、斐波那契堆
1、斐波那契堆由一組最小堆序有根樹組成,其中每棵樹必須滿足最小堆的性質;
2、每個最小堆用一個雙循環鏈表連接起來,稱為根鏈表;
3、斐波那契堆是一種合並堆,除了支持可合並堆的五種操作之外(Make-Heap, Insert, Minimum, Extract_min, Union),還支持Decrease_Key和Delete兩種操作;
4、斐波那契堆是為了改進普通二叉堆Union操作( O(n) )而提出的一種可合並堆,除了Union操作,普通二叉堆都能在最壞情況時間為O(lgn)下完成,但斐波那契堆所有操作均能在常數攤還時間下完成,除了Extract_min和Delete操作。
5、斐波那契堆在優化加速圖算法中有很大的用途。比如用於解決諸如最小生成樹、尋找單源最短路徑等問題的快速算法都要用到斐波那契堆。
6、斐波那契堆具有以下屬性:
1)根節點的孩子節點也組成一個雙循環鏈表,稱為孩子鏈表;
2)每個節點有指向父親節點,指向某一個孩子節點,指向左兄弟節點和有兄弟節點的指針;
3)H.min指向根鏈表中的最小節點;
4)H.n表示節點數目
5)每個節點x有兩個屬性: x.degree表示節點的度; x.mark則用來標識一個非根結點是否已經失去 了一個孩子(這樣的結點,不能在奪其子女了,可能要進行一些其它的特別操作),該標志主要用於刪除操作。下面看一個斐波那契堆的內存結構圖(引自:斐波那契堆之圖文解析)
1 template<typename Data, typename Key> 2 class FibHeapNode 3 { 4 public: 5 template<typename D, typename K> friend class FibHeap; 6 FibHeapNode() {} 7 FibHeapNode( Data d, Key k): 8 myKey( k ), 9 myData( d ), 10 degree( 0 ), 11 marked( false ), 12 child( NULL ) 13 { 14 prev = next = this; //doubly linked circular list 15 } 16 17 Key key() const 18 { 19 return myKey; 20 } 21 22 Data data() const 23 { 24 return myData; 25 } 26 28 private: 29 Key myKey; 30 Data myData; 31 32 unsigned int degree; //節點的孩子節點數 33 bool marked; //標識一個節點的孩子節點是否被刪除過,用於decreaseKey 操作 34 35 FibHeapNode<Data, Key> *prev; //雙循環鏈表的上一個節點 36 FibHeapNode<Data, Key> *next; //雙循環鏈表的下一個節點 37 38 FibHeapNode<Data, Key> *child; //孩子鏈表中的第 39 一個節點 40 FibHeapNode<Data, Key> *parent;//父節點 41 };
下面是斐波那契堆類的定義:
1 template<typename Data, typename Key> 2 class FibHeap 3 { 4 public: 5 FibHeap() {} 6 FibHeap(): rootWithMinKey( NULL ), nCount( 0 ), maxDegree( 0 ) { } 7 8 ~FibHeap(){ 9 10 } 11 12 13 private: 14 typedef FibHeapNode<Data, Key>* pNode; 15 16 pNode rootWithMinKey; 17 unsigned int nCount; 18 unsigned int maxDegree; 19 };
三、斐波那契堆動態集合的操作
其中最難的,也是最麻煩的可能就是Extrac_min()和Decrease_key()操作了,書上對於這兩個操作也是寫得很詳細,具體的我就不再贅述了,我相信認真照着書本上推導,一個下午絕對可以搞定,如果確實難以吃下,推薦一個博客:斐波那契堆(一)之 圖文解析 和 C語言的實現,其圖文並茂的方式可能比書上更淺顯易懂一點,感謝作者。下面附上自己借鑒這個作者所寫的程序:

#ifndef _FIBONACCI_HEAP_H_ #define _FIBONACCI_HEAP_H_ template<typename Data, typename Key> class FibHeapNode { public: template<typename D, typename K> friend class FibHeap; FibHeapNode() {} FibHeapNode( Data d, Key k): myKey( k ), myData( d ), degree( 0 ), marked( false ), child( NULL ) { prev = next = this; //doubly linked circular list } Key key() const { return myKey; } Data data() const { return myData; } public: bool isSingle() const { return ( this == this->next ); } //插入一個節點或節點鏈表 void Insert( FibHeapNode<Data, Key> *other ) { if ( !other ) return; //for example: given 1->2->3->1, insert a->b->c->a after node 3; //result: 1->2->3->a->b->c->1 this->next->prev = other->prev; other->prev->next = this->next; this->next = other; other->prev = this; } //刪除當前節點 void RemoveNode() { this->prev->next = this->next; this->next->prev = this->prev; this->next = this->prev = this; } //連接其他節點到當前節點 void addChild( FibHeapNode<Data, Key> *other ) { if ( !child ) child = other; else child->Insert( other ); //更新相應的信息 other->parent = this; other->marked = false; degree ++; } //刪除孩子節點 void removeChild( FibHeapNode<Data, Key> *other ) { if ( other->parent != this ) throw string ( "Trying to remove a child from a non-parent!"); //只有一個節點 if ( other->isSingle() ) { if ( child != other ) throw string ("Trying to remove a non-child"); child = NULL; } else { if ( child == other ) { child = other->next; } other->RemoveNode(); } //更新 other->parent = NULL; other->marked = false; degree --; } //<<操作符重載 friend ostream& operator<< ( ostream& out, const FibHeapNode& n) { return ( out << n.myData << ";" << n.myKey ); } //打印樹 void printTree( ostream& out ) const { out << myData << ":" << myKey << ":" << degree << ":" << marked; if ( child ) { out << "("; const FibHeapNode<Data, Key> *n = child; do { if ( n== this ) throw string ( "Illegal pointer - node is child of itself"); n->printTree( out ); out << " "; n = n->next; }while ( n != child ); out << ")"; } } void printAll( ostream& out) const { const FibHeapNode<Data, Key> *n = this; do { n->printTree( out ); out << " "; n = n->next; }while ( n != this ); out << endl; } private: Key myKey; Data myData; unsigned int degree; //節點的孩子節點數 bool marked; //標識一個節點的孩子節點是否被刪除過,用於decreaseKey 操作 FibHeapNode<Data, Key> *prev; //雙循環鏈表的上一個節點 FibHeapNode<Data, Key> *next; //雙循環鏈表的下一個節點 FibHeapNode<Data, Key> *child; //孩子鏈表中的第 一個節點 FibHeapNode<Data, Key> *parent;//父節點 }; template<typename Data, typename Key> class FibHeap { public: FibHeap() {} FibHeap(): rootWithMinKey( NULL ), nCount( 0 ), maxDegree( 0 ) { } ~FibHeap(){ } bool empty() const { return nCount == 0; } pNode minimum() const { if ( !rootWithMinKey ) throw string("no minimum element"); return rootWithMinKey; } void printRoots( ostream &out ) const { out << "maxDegree=" << maxDegree << " count=" << count << " roots="; if ( rootWithMinKey ) rootWithMinKey->printAll( out ); else out << endl; } void merge ( const FibHeap& other ) // Fibonacci-Heap-Union { rootWithMinKey->Insert( other.rootWithMinKey ); if ( !rootWithMinKey || ( other.rootWithMinKey && other.rootWithMinKey->key() < rootWithMinKey->key() ) ) this->rootWithMinKey = other.rootWithMinKey; nCount += other.nCount; } pNode insertHeap( Data d, Key k ) { nCount ++; return insertNode( new FibHeapNode<Data, Key>(d, k)); } void removeMinimum() // Fibonacci-Heap-Extract-Min, CONSOLIDATE { if ( !rootWithMinKey ) throw string( "trying to remove from an empty heap" ); /// Phase 1: Make all the removed root's children new roots: // Make all children of root new roots: if ( rootWithMinKey->child ) { pNode c = rootWithMinKey->child; do { c->parent = NULL; c = c->next; } while ( c != rootWithMinKey->child ); rootWithMinKey->child = NULL; // removed all children rootWithMinKey->Insert( c ); } /// Phase 2-a: handle the case where we delete the last myKey: if ( rootWithMinKey->next == rootWithMinKey ) { if ( count != 0 ) throw string ( "Internal error: should have 0 keys" ); rootWithMinKey = NULL; return; } /// Phase 2: merge roots with the same degree: vector<pNode> degreeRoots ( maxDegree + 1 ); // make room for a new degree fill ( degreeRoots.begin(), degreeRoots.end(), ( pNode )NULL ); maxDegree = 0; pNode currentPointer = rootWithMinKey->next; unsigned int currentDegree; do { currentDegree = currentPointer->degree; pNode current = currentPointer; currentPointer = currentPointer->next; while ( degreeRoots[currentDegree] ) // merge the two roots with the same degree: { pNode other = degreeRoots[currentDegree]; // another root with the same degree if ( current->key() > other->key() ) swap( other, current ); // now current->key() <= other->key() - make other a child of current: other->RemoveNode(); // remove from list of roots current->addChild( other ); degreeRoots[currentDegree] = NULL; currentDegree++; if ( currentDegree >= degreeRoots.size() ) degreeRoots.push_back( ( pNode )NULL ); } // keep the current root as the first of its degree in the degrees array: degreeRoots[currentDegree] = current; } while ( currentPointer != rootWithMinKey ); /// Phase 3: remove the current root, and calcualte the new rootWithMinKey: delete rootWithMinKey; rootWithMinKey = NULL; unsigned int newMaxDegree = 0; for ( unsigned int d = 0; d < degreeRoots.size(); ++d ) { if ( degreeRoots[d] ) { degreeRoots[d]->next = degreeRoots[d]->prev = degreeRoots[d]; insertNode( degreeRoots[d] ); if ( d > newMaxDegree ) newMaxDegree = d; } } maxDegree = newMaxDegree; } void decreaseKey( pNode node, Key newKey ) { if ( newKey >= node->myKey ) throw string( "Trying to decrease key to a greater key" ); // Update the key and possibly the min key: node->myKey = newKey; // Check if the new key violates the heap invariant: pNode parent = node->parent; if ( !parent ) // root node - just make sure the minimum is correct { if ( newKey < rootWithMinKey->key() ) rootWithMinKey = node; return; // heap invariant not violated - nothing more to do } else if ( parent->key() <= newKey ) { return; // heap invariant not violated - nothing more to do } for( ;; ) { parent->removeChild( node ); insertNode( node ); if ( !parent->parent ) // parent is a root - nothing more to do { break; } else if ( !parent->marked ) // parent is not a root and is not marked - just mark it { parent->marked = true; break; } else { node = parent; parent = parent->parent; continue; } } } void remove( pNode node, Key minusInfinity ) { if ( minusInfinity >= minimum()->key() ) throw string( "2nd argument to remove must be a key that is smaller than all other keys" ); decreaseKey( node, minusInfinity ); removeMinimum(); } protected: pNode insertNode ( pNode newNode ) { if ( !rootWithMinKey ) rootWithMinKey = newNode; else { rootWithMinKey->Insert( newNode ); if ( newNode->key() < rootWithMinKey->key() ) rootWithMinKey = newNode; } return newNode; } private: typedef FibHeapNode<Data, Key>* pNode; pNode rootWithMinKey; unsigned int nCount; unsigned int maxDegree; }; #endif//_FIBONACCI_HEAP_H_
我的公眾號 「Linux雲計算網絡」(id: cloud_dev),號內有 10T 書籍和視頻資源,后台回復 「1024」 即可領取,分享的內容包括但不限於 Linux、網絡、雲計算虛擬化、容器Docker、OpenStack、Kubernetes、工具、SDN、OVS、DPDK、Go、Python、C/C++編程技術等內容,歡迎大家關注。