赫夫曼樹
過去我們小學、中學一般考試都是用百分制來表示學科成績的。這帶來了一個弊端,就是很容易讓學生、家長,甚至老師自己都以分取人,讓分數代表了一切。有時想想也對,90分和95分也許就只是一道題目對錯的差距,但卻讓兩個孩子可能受到完全不同的待遇,這并不公平。于是在如今提倡素質教育的背景下,我們很多的學科,特別是小學的學科成績都改作了優秀、良好、中等、及格和不及格這樣模糊的詞語,不再通報具體的分數。
不過對于老師來講,他在對試卷評分的時候,顯然不能憑感覺給優良或及格不及格等成績,因此一般都還是按照百分制算出每個學生的成績后,再根據統一的標準換算得出五級分制的成績。比如下面的代碼就實現了這樣的轉換。
if (a < 60)
b = "不及格";
else if (a < 70)
b = "及格";
else if (a < 80)
b = "中等";
else if (a < 90)
b = "良好";
else
b = "優秀";
上面的代碼粗略看沒什么問題,可是通常都認為,一張好的考卷應該是讓學生成績大部分處于中等或良好的范圍,優秀和不及格都應該較少才對。而上面這樣的程序,就使得所有的成績都需要先判斷是否及格,再逐級而上得到結果。輸入量很大的時候,其實算法是有效率問題的。
如果在實際的學習生活中,學生的成績在5個等級上的分布規律如下圖所示
那么70分以上大約占總數80%的成績都需要經過3次以上的判斷才可以得到結果,這顯然不合理。
有沒有好一些的辦法,仔細觀察發現,中等成績(70~79分之間)比例最高,其次是良好成績,不及格的所占比例最少。我們把圖6-12-2這棵二叉樹重新進行分配。改成如下圖的做法試試看。
赫夫曼樹定義與原理
我們先把這兩棵二叉樹簡化成葉子結點帶權的二叉樹(注:樹結點間的邊相關的數叫做權Weight),如圖下所示。其中A表示不及格、B表示及格、C表示中等、D表示良好、E表示優秀。每個葉子的分支線上的數字就是剛才我們提到的五級分制的成績所占百分比。
赫夫曼大叔說,從樹中一個結點到另一個結點之間的分支構成兩個結點之間的路徑,路徑上的分支數目稱做路徑長度。上圖的二叉樹a中,根結點到結點D的路徑長度就為4,二叉樹b中根結點到結點D的路徑長度為2。樹的路徑長度就是從樹根到每一結點的路徑長度之和。二叉樹a的樹路徑長度就為1+1+2+2+3+3+4+4=20。二叉樹b的樹路徑長度就為1+2+3+3+2+1+2+2=16。
帶權結點路勁的計算
“如果考慮到帶權的結點,結點的帶權的路徑長度為從該結點到樹根之間的路徑長度與結點上權的乘積。樹的帶權路徑長度為樹中所有葉子結點的帶權路徑長度之和。假設有n個權值{w1,w2,...,wn},構造一棵有n個葉子結點的二叉樹,每個葉子結點帶權wk,每個葉子的路徑長度為lk,我們通常記作,則其中帶權路徑長度WPL最小的二叉樹稱做赫夫曼樹。也有不少書中也稱為最優二叉樹,我個人覺得為了紀念做出巨大貢獻的科學家,既然用他們的名字命名,就應該要堅持用他們的名字稱呼,哪怕“最優”更能體現這棵樹的品質也應該只作為別名。
有了赫夫曼對帶權路徑長度的定義,我們來計算一下上圖這兩棵樹的WPL值。
二叉樹a的WPL=5×1+15×2+40×3+30×4+10×4=315
注意:這里5是A結點的權,1是A結點的路徑長度,其他同理。
二叉樹b的WPL=5×3+15×3+40×2+30×2+10×2=220
這樣的結果意味著什么呢?如果我們現在有10000個學生的百分制成績需要計算五級分制成績,用二叉樹a的判斷方法,需要做31500次比較,而二叉樹b的判斷方法,只需要22000次比較,差不多少了三分之一量,在性能上提高不是一點點。
赫夫曼樹的構建
1.先把有權值的葉子結點按照從小到大的順序排列成一個有序序列,即:A5,E10,B15,D30,C40。
2.取頭兩個最小權值的結點作為一個新節點N1的兩個子結點,注意相對較小的是左孩子,這里就是A為N1的左孩子,E為N1的右孩子,如下圖所示。新結點的權值為兩個葉子權值的和5+10=15。
3.將N1替換A與E,插入有序序列中,保持從小到大排列。即:N115,B15,D30,C40。
4.重復步驟2。將N1與B作為一個新節點N2的兩個子結點。如下圖所示。N2的權值=15+15=30。
5.將N2替換N1與B,插入有序序列中,保持從小到大排列。即:N230,D30,C40。
6.重復步驟2。將N2與D作為一個新節點N3的兩個子結點。如下圖所示。N3的權值=30+30=60。
7.將N3替換N2與D,插入有序序列中,保持從小到大排列。即:C40,N360。
8.重復步驟2。將C與N3作為一個新節點T的兩個子結點,如圖6-12-8所示。由于T即是根結點,完成赫夫曼樹的構造。
此時的二叉樹的帶權路徑長度WPL=40×1+30×2+15×3+10×4+5×4=205。與上面的二叉樹b的WPL值220相比,還少了15。顯然此時構造出來的二叉樹才是最優的赫夫曼樹。
通過剛才的步驟,我們可以得出構造赫夫曼樹的赫夫曼算法描述。
- 根據給定的n個權值{w1,w2,...,wn}構成n棵二叉樹的集合F={T1,T2,...,Tn},其中每棵二叉樹Ti中只有一個帶權為wi根結點,其左右子樹均為空。
- 在F中選取兩棵根結點的權值最小的樹作為左右子樹構造一棵新的二叉樹,且置新的二叉樹的根結點的權值為其左右子樹上根結點的權值之和。
- 在F中刪除這兩棵樹,同時將新得到的二叉樹加入F中。
- 重復2和3步驟,直到F只含一棵樹為止。這棵樹便是赫夫曼樹。”
赫夫曼編碼
比如我們有一段文字內容為“BADCADFEED”要網絡傳輸給別人,顯然用二進制的數字(0和1)來表示是很自然的想法。我們現在這段文字只有六個字母ABCDEF,那么我們可以用相應的二進制數據表示,如下圖所示。
這樣真正傳輸的數據就是編碼后的“001000011010000011101100100011”,對方接收時可以按照3位一分來譯碼。如果一篇文章很長,這樣的二進制串也將非常的可怕。而且事實上,不管是英文、中文或是其他語言,字母或漢字的出現頻率是不相同的,比如英語中的幾個元音字母“ae i o u”,中文中的“的 了 有 在”等漢字都是頻率極高。
假設六個字母的頻率為A 27,B 8,C 15,D15,E 30,F 5,合起來正好是100%。那就意味著,我們完全可以重新按照赫夫曼樹來規劃它們。
下圖左圖為構造赫夫曼樹的過程的權值顯示。右圖為將權值左分支改為0,右分支改為1后的赫夫曼樹。
此時,我們對這六個字母用其從樹根到葉子所經過路徑的0或1來編碼,可以得到如下圖所示這樣的定義。
我們將文字內容為“BADCADFEED”再次編碼,對比可以看到結果串變小了。
- 原編碼二進制串:001000011010000011101100100011(共30個字符)
- 新編碼二進制串:1001010010101001000111100(共25個字符)
因此在解碼時,還是要用到赫夫曼樹,即發送方和接收方必須要約定好同樣的赫夫曼編碼規則。
當我們接收到1001010010101001000111100時,由約定好的赫夫曼樹可知,1001得到第一個字母是B,接下來01意味著第二個字符是A,
一般地,設需要編碼的字符集為{d1,d2,...,dn},各個字符在電文中出現的次數或頻率集合為{w1,w2,...,wn},以d1,d2,...,dn作為葉子結點,以w1,w2,...,wn作為相應葉子結點的權值來構造一棵赫夫曼樹。規定赫夫曼樹的左分支代表0,右分支代表1,則從根結點到葉子結點所經過的路徑分支組成的0和1的序列便為該結點對應字符的編碼,這就是赫夫曼編碼。