哈夫曼樹Huffman tree 又稱最優完全二叉樹,切入正題之前,先看幾個定義
1、路徑 Path
簡單點講,路徑就是從一個指定節點走到另一個指定節點所經過的分支,比如中的紅色分支(A->C->B與C->D->E->F)
圖1
2、路徑長度(Path Length)
即路徑中的分支個數,比如(a)中的路徑長度為2,(b)中的路徑長度為3
3、結點的權重(Weight of Node)
在一些特定應用中,有時候要刻意區分節點之間的重要程度(或優先程度),比如認為A節點比B節點要重要(更優先),可以給這些節點增加一個int型的屬性值weight,用該值來標明這種重要性,這就是結點的權重.
圖2
4、結點的帶權(重)路徑長度(Weight Path Length of Node):
從該節點到樹的根節點的路徑長度*該結點的權重,得到的結果就是這個東東
中
節點1的帶權路徑長度為 1 * 2 = 2;
節點2的帶權路徑長度為 2 * 2 = 4;
節點3的帶權路徑長度為 3 * 2 = 6;
節點4的帶權路徑長度為 4 * 2 = 8;
5、樹的帶權(重)路徑長度
樹中的每個節點均按4中的定義計算自身的帶權路徑長度,然後把得到的結果加在一起,就是整顆樹的帶權路徑長度。
(即圖2)中,樹的帶權路徑長度為 2 + 4 + 6 + 8 = 20
如果給定4個節點,其權重值分別是1,2,3,4,那麼構造一顆完全二叉樹的方法有很多種,如:
顯示,(c)樹的帶權路徑總長最小(為19),而其它樹的帶權路徑均為20,ok,它就是傳說中的哈夫曼樹,可通俗的理解為:
給定一組帶權重的分葉節點,用它們來構造完全二叉樹,最終整顆樹的帶權路徑(總)長度最小的即為啥夫曼樹。(當然,這是根據我的理解給出的民間山寨定義,官方定義大家自己去看“資料結構與演算法”這本書吧,上面有一堆數學符號,對於不喜歡數位同學們,估計看起來很暈)
啥夫曼樹的構造演算法:
1、在給定的帶權分葉節點中,找出權重最小的二個(通常為了方便,可以先將分葉節點按權重從小到大先排好,這樣只需要取前面二項即可),然後添加一個臨時節點作為這二個節點的父節點(其權重為這二個分葉節點的權重之合)
2、將剛才處理過的二個節葉點去掉,然後把新增加的臨時節點與剩下的分葉節點放在一起做同樣的處理,即:從新節點和分葉節點的集合中,繼續找到權重最小的二個,再繼續增加新節點,做第1中的處理
3、重複以上過程,直到每個分葉節點都處理完。
假如我們現在有權重為1,2,3,4的一組分葉節點,上述過程圖解為:
c#的演算法實現:
先回顧上一篇提到的二個重要知識點:
1、由二叉樹的數學特性4知:
對於一棵非空二叉樹,如果度為0的結點數目為x,度為2的結點數目為y,則有 x = y +1(即y = x-1)
也就是說全部節點總數為 x+y = x + (x-1) = 2*x-1
2、完全二叉樹,可以方便的使用順序儲存(即用線性結構的數組或List<T>來儲存)
Huffman樹的節點類Node.cs:
using System;using System.Collections.Generic;using System.Linq;using System.Text;namespace 哈夫曼樹{ public class Node { private int weight;//權重值 private int lChild;//左子節點的序號 private int rChild;//右子節點的序號 private int index;//本節點的序號 public int Weight { get { return weight; } set { weight = value; } } public int LChild { get { return this.lChild; } set { lChild = value; } } public int RChild { get { return this.rChild; } set { rChild = value; } } public int Index { get { return this.index; } set { index = value; } } public Node() { weight = 0; lChild = -1; rChild = -1; index = -1; } public Node(int w, int lc, int rc, int p) { weight = w; lChild = lc; rChild = rc; index = p; } }}
HuffmanTree.cs(註:下面這段代碼的Create演算法在運行效率上也許並非最高的,但很容易理解)
using System;using System.Collections.Generic;using System.Linq;using System.Text;namespace 哈夫曼樹{ public class HuffmanTree { private List<Node> _tmp; private List<Node> _nodes; public HuffmanTree(params int[] weights) { if (weights.Length < 2) { throw new Exception("分葉節點不能少於2個!"); } int n = weights.Length; Array.Sort(weights); //先產生葉子節點,並按weight從小到大排序 List<Node> lstLeafs = new List<Node>(n); for (int i = 0; i < n; i++) { var node = new Node(); node.Weight = weights[i]; node.Index = i; lstLeafs.Add(node); } //建立臨時節點容器 _tmp = new List<Node>(2 * n - 1); //真正存放所有節點的容器 _nodes = new List<Node>(_tmp.Capacity); _tmp.AddRange(lstLeafs); _nodes.AddRange(_tmp); } /// <summary> /// 構造Huffman樹 /// </summary> public void Create() { while (this._tmp.Count > 1) { var tmp = new Node(this._tmp[0].Weight + this._tmp[1].Weight, _tmp[0].Index, _tmp[1].Index, this._tmp.Max(c => c.Index) + 1); this._tmp.Add(tmp); this._nodes.Add(tmp); //刪除已經處理過的二個節點 this._tmp.RemoveAt(0); this._tmp.RemoveAt(0); //重新按權重值從小到大排序 this._tmp = this._tmp.OrderBy(c => c.Weight).ToList(); } } /// <summary> /// 測試輸出各節點的關索引值(調試用) /// </summary> /// <returns></returns> public override string ToString() { StringBuilder sb = new StringBuilder(); for (int i = 0; i < _nodes.Count; i++) { var n = _nodes[i]; sb.AppendLine("index:" + i + ",weight:" + n.Weight.ToString().PadLeft(2, ' ') + ",lChild_index:" + n.LChild.ToString().PadLeft(2, ' ') + ",rChild_index:" + n.RChild.ToString().PadLeft(2, ' ')); } return sb.ToString(); } }}
測試一下:
using System;namespace 哈夫曼樹{ class Program { static void Main(string[] args) { HuffmanTree tree = new HuffmanTree(2,1,4,3); tree.Create(); Console.WriteLine("最終樹的節點值如下:"); Console.WriteLine(tree.ToString()); Console.ReadLine(); } }}
輸出結果如下:
最終樹的節點值如下:
index:0,weight: 1,lChild_index:-1,rChild_index:-1
index:1,weight: 2,lChild_index:-1,rChild_index:-1
index:2,weight: 3,lChild_index:-1,rChild_index:-1
index:3,weight: 4,lChild_index:-1,rChild_index:-1
index:4,weight: 3,lChild_index: 0,rChild_index: 1
index:5,weight: 6,lChild_index: 2,rChild_index: 4
index:6,weight:10,lChild_index: 3,rChild_index: 5
輸出結果也許並不直觀,對照下面這張圖就明白了
哈夫曼編碼(Huffman Encoding)
先扯貌似不相干的話題,在電報傳輸中,通常要對傳輸的內容進行編碼(因為電報發送時只用0,1表示,所以需要將ABCDE這類字元最終變成0與1的組合,這就涉及到如何將字元集[A-Z]與[0,1]組合一一對應的問題)
假設現在有電文內容:AAAABBBCCD 需要編碼後轉送,現在要一套編碼方案
首先很容易想到下面的這種定長編碼方案,每個字元用2位元字表示,比如:
A->00
B->01
C->10
D->11
那麼AAAABBBCCD最終的編碼為00,00,00,00,01,01,01,10,10,11(註:這裡加逗號是為了看得更直觀,實際編碼中並不需要)
但電報磚家們,提出了另一種更短的不定長編碼方案:
A->0
B->10
C->111
D->110
按這種編碼方案,AAAABBBCCD最終的編碼為:0,0,0,0,10,10,10,111,111,110
把這二種方案的編碼列在一起對比一下:
00,00,00,00,01,01,01,10,10,11 (不算逗號共20位)
0,0,0,0,10,10,10,111,111,110 (不算逗號共19位)
磚家果然是磚家!
仔細分析一下,會發現這種“不定長”的編碼方案要想解碼成功,要有一個重要的前提:任何一個編碼,都不能是其它編碼的首碼!否則解碼時就會出現歧義。
比如:如果C編碼為10,D編碼為101,A編碼為1,B編碼為01
現在接收到了一個 10101,那麼到底是解碼為 CCA,還是DB呢?
現在揭曉哈夫曼編碼的秘密:
就剛才舉例的AAAABBBCCD而言,電文中僅包含A,B,C,D這個字元,如果把它們看作分葉節點,並且考慮到權重(D出現次數最小,權重認為最低;C出現次數比D高,因此權重高於D,其它類推),這樣我們就有了一組帶權重的分葉節點(A-權重4,B-權重3,C-權重2,D-權重1),用它們來構造一顆哈夫曼樹:
同時,我們把有分支做一個約定:向左的分支對應為數字0,向右的分支對應為數字1,這樣從根節點到每個葉子節點的路徑就能得到一串數字。
即: A->0,B->10,C->110,D->111 ,這就是一種編碼!
另外應該注意到,對於二叉樹,某一個確定的分葉節點只可能在一個唯一的分支上(即不可能一個分葉節點即在這個分支上,又在其它分支上),這就保證了每個分葉節點得到的編碼都不可能是其它編碼的首碼。
OK,尋找哈夫曼編碼的問題最終就轉化成了哈夫曼樹的構造問題,問題得到解決了。(學會了哈夫曼編碼,也許我們能跟某些冰雪聰明的MM們玩點另類告白的小遊戲,發一串數字過去,然後配一張圖,看她懂不懂你的心意,如果她能成功解出背後的含義是ILOVEYOU,然後回傳一串吉祥數字給你,那麼...恭喜你!)