資料結構C#版筆記–啥夫曼樹(Huffman Tree)與啥夫曼編碼(Huffman Encoding)

來源:互聯網
上載者:User

哈夫曼樹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,然後回傳一串吉祥數字給你,那麼...恭喜你!)

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.