我們希望建立這樣一株二叉樹,其葉結點為一組給定的帶權結點,稱這個樹的權重為每個葉子結點到根結點的距離與其權值的乘積的累和,即
$$ T.w=\sum_{x\ is\ leaf\ of\ T}^{}{x.w\cdot x.d} $$
其中x.w表示葉結點的權重,而x.d為葉結點的深度。哈夫曼樹是所有滿足上面條件的二叉樹中權重最小的。
下面說明一些關於哈夫曼樹的命題:
命題1:哈夫曼樹是滿二叉樹。
證明:這是顯然的,因為若哈夫曼樹不是滿二叉樹,即存在頂點p,其只有一個子結點。那么我們可以利用p的唯一子結點替換p,這樣就可以保證其下的所有葉結點的深度減少1,從而減少樹的權值。這與哈夫曼樹的定義相悖,故命題成立。
命題2:哈夫曼樹中權重較小的葉結點的深度不可能小於權重較大的葉結點的深度。
證明:如果情況發生,交換兩個葉結點的位置,即可降低樹的權重。
命題3:存在這樣一株哈夫曼樹,其中權重最小的兩個葉結點的父結點相同。
證明:設權重最小的葉結點為x,而第二小的為y。由命題1知,x的兄弟結點是存在的。而由命題2知,葉結點的深度是按權重值非嚴格遞減的。因此我們能保證y的深度與x一致,對於任意哈夫曼樹,我們只需要令y和x的兄弟的位置交換即可得到目標哈夫曼樹。
下面我們給出構建哈夫曼樹的過程。我們將所有葉結點加入一個最小堆中,只要其中至少有兩個結點,我們就彈出其中權重最小的兩個頂點, 並創建一個新結點,其兩個子結點為方才彈出的兩個結點。之后我們將新建的結點加入堆中。不斷循環上面過程,直到最終只余下一個頂點,此時余下的頂點即為哈夫曼樹的根結點。
buildHfmTree(nodeList) heap = an-empty-min-heap for node in nodeList heap.add(node) while(heap.size() >= 2) father = a-new-node father.left = heap.pop() father.right = heap.pop() father.w = father.left.w + father.right.w heap.add(father) return heap.pop()
簡直難以想象,這么簡短的代碼就可以實現哈夫曼樹。
算法的時間復雜度為O(nlog2(n)),其中n為葉結點數。
下面說明算法的正確性:通過歸納法說明,當只有一個頂點時,算法顯然正確,此時唯一的葉結點就是根結點,我們將其返回,而此時哈夫曼樹的權值達到了最小,為0。當頂點數少於k時,若上述算法都可以構建一株合理的哈夫曼樹。那么當我們持有k個結點組成的結點集合V時,由於命題3我們已知權重最小的兩個結點x,y可以有相同的父親f,我們利用上述算法,使用k-1個結點組成的結點集合V'(V中移除了x與y后加入f得到)建立對應的一株哈夫曼樹F。假設T為V的哈夫曼樹。我們可以在T的基礎上建立一株新樹T',其中T'與T的區別在於我們為f賦予權值x.w+y.w,同時從T中刪除x與y,顯然T'.w = T.w - x.w - y.w。而T'也是滿足以V'為葉結點的一株二叉樹,故知F.w<=T'.w=T.w-x.w-y.w。同樣我們可以在F的基礎上建立另外一株二叉樹F',其中F'與F的區別在於我們移除f結點的權值,並為其添加兩個子結點x與y,此時顯然有F'.w=F.w+x.w+y.w,而由於F'.w是V的一株二叉樹,因此T.w<=F'.w=F.w+x.w+y.w。結合兩條不等式可以得出F.w+x.w+y.w=T.w,即在F的基礎上做改變得到的樹F'是V的哈夫曼樹。因此我們可以通過遞歸的思路建立哈夫曼樹,而上述的代碼正是遞歸展開成為循環后的樣子。