赫夫曼樹又稱最優二叉樹,也就是帶權路徑最短的樹,對于赫夫曼樹,我想大家對它是非常的熟悉,也知道它的應用場景,但是有沒有自己親手寫過,這個我就不清楚了,不管以前寫沒寫,這一篇我們來玩一把。
一、概念
赫夫曼樹里面有幾個概念,也是非常簡單的,先來看下面的圖:
- 節點的權: 節點中紅色部分就是權,在實際應用中,我們用“字符”出現的次數作為權。
- 路徑長度:可以理解成該節點到根節點的層數,比如:“A”到根節點的路徑長度為 3。
- 樹的路徑長度:各個葉子節點到根節點的路徑長度總和,用 WPL 標記。
最后我們要討論的的赫夫曼樹也就是帶權路徑長度最小的一棵樹。
二、構建
由于要使 WPL 最短,赫夫曼樹的構建采用自低向上的方式,這里我們采用小根堆來存放當前需要構建的各個節點,我們的方式是每次從小根堆中取出最小的兩個節點,合并后放入堆中,然后繼續取兩個最小的節點,一直到小根堆為空,最后我們采用自底向上構建的赫夫曼樹也就完畢了。
好了,赫夫曼樹的典型應用就是在數據壓縮方面,下面我們就要在赫夫曼樹上面放入赫夫曼編碼了,我們知道普通的 ASCII 碼是采用等長編碼的,即每個字符都采用 2 個字節,而赫夫曼編碼的思想就是采用不等長的思路,權重高的字符靠近根節點,權重低的字符遠離根節點,標記方式為左孩子“0”,右孩子“1”,如下圖。
從圖中我們可以看到各個字符的赫夫曼編碼了,獲取字符的編碼采用從根往下的方式收集路徑上的‘0,1’,如:
A:110。B:111。C:0。D:10。
最后我們來比較他們的 WPL 的長度: ASCII 碼=102+202+402+802=300
赫夫曼碼=103+203+402+801=250
可以看到,赫夫曼碼壓縮了 50 個 0,1 字符。
三、代碼
1、樹節點
我們采用 7 元節點,其中 parent 方便我們在 DFS 的時候找到從葉子節點到根節點的路徑上的赫夫曼編碼。
#region 赫夫曼節點/// <summary>/// 赫夫曼節點/// </summary>public class Node{/// <summary>/// 左孩子/// </summary>public Node left;/// <summary>/// 右孩子/// </summary>public Node right;/// <summary>/// 父節點/// </summary>public Node parent;/// <summary>/// 節點字符/// </summary>public char c;/// <summary>/// 節點權重/// </summary>public int weight;//赫夫曼“0"or“1"public char huffmancode;/// <summary>/// 標記是否為葉子節點/// </summary>public bool isLeaf;}#endregion
2、構建赫夫曼樹(Build)
上面也說了,構建赫夫曼編碼樹我們采用小根堆的形式構建,構建完后,我們采用 DFS 的方式統計各個字符的編碼,復雜度為 N*logN。
#region 構建赫夫曼樹/// <summary>/// 構建赫夫曼樹/// </summary>public void Build(){//構建while (queue.Count() > 0){//如果只有一個節點,則說明已經到根節點了if (queue.Count() == 1){root = queue.Dequeue().t;break;}//節點1var node1 = queue.Dequeue();//節點2var node2 = queue.Dequeue();//標記左孩子node1.t.huffmancode = '0';//標記為右孩子node2.t.huffmancode = '1';//判斷當前節點是否為葉子節點,hufuman無度為1點節點(方便計算huffman編碼)if (node1.t.left == null)node1.t.isLeaf = true;if (node2.t.left == null)node2.t.isLeaf = true;//父節點root = new Node();root.left = node1.t;root.right = node2.t;root.weight = node1.t.weight + node2.t.weight;//當前節點為根節點node1.t.parent = node2.t.parent = root;//將當前節點的父節點入隊列queue.Eequeue(root, root.weight);}//深度優先統計各個字符的編碼DFS(root);}#endregion
3、編碼(Encode,Decode)
樹構建起來后,我會用字典來保存字符和”赫夫曼編碼“的對應表,然后拿著明文或者密文對著編碼表翻譯就行了, 復雜度 O(N)。
#region 赫夫曼編碼/// <summary>/// 赫夫曼編碼/// </summary>/// <returns></returns>public string Encode(){StringBuilder sb = new StringBuilder();foreach (var item in word){sb.Append(huffmanEncode[item]);}return sb.ToString();}#endregion#region 赫夫曼解碼/// <summary>/// 赫夫曼解碼/// </summary>/// <returns></returns>public string Decode(string str){StringBuilder decode = new StringBuilder();string temp = string.Empty;for (int i = 0; i < str.Length; i++){temp += str[i].ToString();//如果包含 O(N)時間if (huffmanDecode.ContainsKey(temp)){decode.Append(huffmanDecode[temp]);temp = string.Empty;}}return decode.ToString();}#endregion
最后我們做個例子,壓縮 9M 的文件,看看到底能壓縮多少?
public static void Main(){StringBuilder sb = new StringBuilder();for (int i = 0; i < 1 * 10000; i++){sb.Append("人民網北京12月8日電 (記者 宋心蕊) 北京時間8日晚的央視《新聞聯播》節目出現了直播失誤。上一條新聞尚未播放完畢時,播就將畫面切換回了演播間,主播李梓萌開始播報下一條新聞,導致兩條新聞出現了“混音”播出。央視新聞官方微博賬號在21點09分發布了一條致歉微博:【致歉】今晚《新聞聯播》因導播員口令失誤,導致畫面切換錯誤,特此向觀眾朋友表示歉意。央視特約評論員楊禹在個人微博中寫道:今晚《新聞聯播》出了個切換錯誤,@央視新聞 及時做了誠懇道歉。聯播一直奉行“金標準”,壓力源自全社會的高要求。其實報紙亦都有“勘誤”一欄,坦誠糾錯與道歉。《新聞聯播》是中國影響力最大的電視新聞節目。它有不可替代的符號感,它有失誤,更有悄然的進步。新的改進正在或即將發生,不妨期待");}File.WriteAllText(Environment.CurrentDirectory + "//1.txt", sb.ToString());Huffman huffman = new Huffman(sb.ToString());Stopwatch watch = Stopwatch.StartNew();huffman.Build();watch.Stop();Console.WriteLine("構建赫夫曼樹耗費:{0}", watch.ElapsedMilliseconds);//將8位二進制轉化為ascII碼var s = huffman.Encode();var remain = s.Length % 8;List<char> list = new List<char>();var start = 0;for (int i = 8; i < s.Length; i = i + 8){list.Add((char)Convert.ToInt32(s.Substring(i - 8, 8), 2));start = i;}var result = new String(list.ToArray());//當字符編碼不足8位時, 用‘艸'來標記,然后拿出’擦‘以后的所有0,1即可result += "艸" + s.Substring(start);File.WriteAllText(Environment.CurrentDirectory + "//2.txt", result);Console.WriteLine("壓縮完畢!");Console.Read();//解碼var str = File.ReadAllText(Environment.CurrentDirectory + "//2.txt");sb.Clear();for (int i = 0; i < str.Length; i++){int ua = (int)str[i];//說明已經取完畢了 用'艸'來做標記if (ua == 33401)sb.Append(str.Substring(i));elsesb.Append(Convert.ToString(ua, 2).PadLeft(8, '0'));}var sss = huffman.Decode(sb.ToString());Console.Read();}
看看,將 9M 的文件壓縮到了 4M。
主程序:
using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Diagnostics;using System.Threading;using System.IO;namespace ConsoleApplication2{public class Program{public static void Main(){StringBuilder sb = new StringBuilder();for (int i = 0; i < 1 * 10000; i++){sb.Append("人民網北京12月8日電 (記者 宋心蕊) 北京時間8日晚的央視《新聞聯播》節目出現了直播失誤。上一條新聞尚未播放完畢時,播就將畫面切換回了演播間,主播李梓萌開始播報下一條新聞,導致兩條新聞出現了“混音”播出。央視新聞官方微博賬號在21點09分發布了一條致歉微博:【致歉】今晚《新聞聯播》因導播員口令失誤,導致畫面切換錯誤,特此向觀眾朋友表示歉意。央視特約評論員楊禹在個人微博中寫道:今晚《新聞聯播》出了個切換錯誤,@央視新聞 及時做了誠懇道歉。聯播一直奉行“金標準”,壓力源自全社會的高要求。其實報紙亦都有“勘誤”一欄,坦誠糾錯與道歉。《新聞聯播》是中國影響力最大的電視新聞節目。它有不可替代的符號感,它有失誤,更有悄然的進步。新的改進正在或即將發生,不妨期待");}File.WriteAllText(Environment.CurrentDirectory + "//1.txt", sb.ToString());Huffman huffman = new Huffman(sb.ToString());Stopwatch watch = Stopwatch.StartNew();huffman.Build();watch.Stop();Console.WriteLine("構建赫夫曼樹耗費:{0}", watch.ElapsedMilliseconds);//將8位二進制轉化為ascII碼var s = huffman.Encode();var remain = s.Length % 8;List<char> list = new List<char>();var start = 0;for (int i = 8; i < s.Length; i = i + 8){list.Add((char)Convert.ToInt32(s.Substring(i - 8, 8), 2));start = i;}var result = new String(list.ToArray());//當字符編碼不足8位時, 用‘艸'來標記,然后拿出’擦‘以后的所有0,1即可result += "艸" + s.Substring(start);File.WriteAllText(Environment.CurrentDirectory + "//2.txt", result);Console.WriteLine("壓縮完畢!");Console.Read();//解碼var str = File.ReadAllText(Environment.CurrentDirectory + "//2.txt");sb.Clear();for (int i = 0; i < str.Length; i++){int ua = (int)str[i];//說明已經取完畢了 用'艸'來做標記if (ua == 33401)sb.Append(str.Substring(i));elsesb.Append(Convert.ToString(ua, 2).PadLeft(8, '0'));}var sss = huffman.Decode(sb.ToString());Console.Read();}}public class Huffman{#region 赫夫曼節點/// <summary>/// 赫夫曼節點/// </summary>public class Node{/// <summary>/// 左孩子/// </summary>public Node left;/// <summary>/// 右孩子/// </summary>public Node right;/// <summary>/// 父節點/// </summary>public Node parent;/// <summary>/// 節點字符/// </summary>public char c;/// <summary>/// 節點權重/// </summary>public int weight;//赫夫曼“0"or“1"public char huffmancode;/// <summary>/// 標記是否為葉子節點/// </summary>public bool isLeaf;}#endregionPriorityQueue<Node> queue = new PriorityQueue<Node>();/// <summary>/// 編碼對應表(加速用)/// </summary>Dictionary<char, string> huffmanEncode = new Dictionary<char, string>();/// <summary>/// 解碼對應表(加速用)/// </summary>Dictionary<string, char> huffmanDecode = new Dictionary<string, char>();/// <summary>/// 明文/// </summary>string word = string.Empty;public Node root = new Node();public Huffman(string str){this.word = str;Dictionary<char, int> dic = new Dictionary<char, int>();foreach (var s in str){if (dic.ContainsKey(s))dic[s] += 1;elsedic[s] = 1;}foreach (var item in dic.Keys){var node = new Node(){c = item,weight = dic[item]};//入隊queue.Eequeue(node, dic[item]);}}#region 構建赫夫曼樹/// <summary>/// 構建赫夫曼樹/// </summary>public void Build(){//構建while (queue.Count() > 0){//如果只有一個節點,則說明已經到根節點了if (queue.Count() == 1){root = queue.Dequeue().t;break;}//節點1var node1 = queue.Dequeue();//節點2var node2 = queue.Dequeue();//標記左孩子node1.t.huffmancode = '0';//標記為右孩子node2.t.huffmancode = '1';//判斷當前節點是否為葉子節點,hufuman無度為1點節點(方便計算huffman編碼)if (node1.t.left == null)node1.t.isLeaf = true;if (node2.t.left == null)node2.t.isLeaf = true;//父節點root = new Node();root.left = node1.t;root.right = node2.t;root.weight = node1.t.weight + node2.t.weight;//當前節點為根節點node1.t.parent = node2.t.parent = root;//將當前節點的父節點入隊列queue.Eequeue(root, root.weight);}//深度優先統計各個字符的編碼DFS(root);}#endregion#region 赫夫曼編碼/// <summary>/// 赫夫曼編碼/// </summary>/// <returns></returns>public string Encode(){StringBuilder sb = new StringBuilder();foreach (var item in word){sb.Append(huffmanEncode[item]);}return sb.ToString();}#endregion#region 赫夫曼解碼/// <summary>/// 赫夫曼解碼/// </summary>/// <returns></returns>public string Decode(string str){StringBuilder decode = new StringBuilder();string temp = string.Empty;for (int i = 0; i < str.Length; i++){temp += str[i].ToString();//如果包含 O(N)時間if (huffmanDecode.ContainsKey(temp)){decode.Append(huffmanDecode[temp]);temp = string.Empty;}}return decode.ToString();}#endregion#region 深度優先遍歷子節點,統計各個節點的赫夫曼編碼/// <summary>/// 深度優先遍歷子節點,統計各個節點的赫夫曼編碼/// </summary>/// <returns></returns>public void DFS(Node node){if (node == null)return;//遍歷左子樹DFS(node.left);//遍歷右子樹DFS(node.right);//如果當前葉節點if (node.isLeaf){string code = string.Empty;var temp = node;//回溯的找父親節點的huffmancode LgN 的時間while (temp.parent != null){//注意,這里最后形成的 “反過來的編碼”code += temp.huffmancode;temp = temp.parent;}var codetemp = new String(code.Reverse().ToArray());huffmanEncode.Add(node.c, codetemp);huffmanDecode.Add(codetemp, node.c);}}#endregion}}
小根堆:
using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Diagnostics;using System.Threading;using System.IO;namespace ConsoleApplication2{public class PriorityQueue<T> where T : class{/// <summary>/// 定義一個數組來存放節點/// </summary>private List<HeapNode> nodeList = new List<HeapNode>();#region 堆節點定義/// <summary>/// 堆節點定義/// </summary>public class HeapNode{/// <summary>/// 實體數據/// </summary>public T t { get; set; }/// <summary>/// 優先級別 1-10個級別 (優先級別遞增)/// </summary>public int level { get; set; }public HeapNode(T t, int level){this.t = t;this.level = level;}public HeapNode() { }}#endregion#region 添加操作/// <summary>/// 添加操作/// </summary>public void Eequeue(T t, int level = 1){//將當前節點追加到堆尾nodeList.Add(new HeapNode(t, level));//如果只有一個節點,則不需要進行篩操作if (nodeList.Count == 1)return;//獲取最后一個非葉子節點int parent = nodeList.Count / 2 - 1;//堆調整UpHeapAdjust(nodeList, parent);}#endregion#region 對堆進行上濾操作,使得滿足堆性質/// <summary>/// 對堆進行上濾操作,使得滿足堆性質/// </summary>/// <param name="nodeList"></param>/// <param name="index">非葉子節點的之后指針(這里要注意:我們/// 的篩操作時針對非葉節點的)/// </param>public void UpHeapAdjust(List<HeapNode> nodeList, int parent){while (parent >= 0){//當前index節點的左孩子var left = 2 * parent + 1;//當前index節點的右孩子var right = left + 1;//parent子節點中最大的孩子節點,方便于parent進行比較//默認為left節點var min = left;//判斷當前節點是否有右孩子if (right < nodeList.Count){//判斷parent要比較的最大子節點min = nodeList[left].level < nodeList[right].level ? left : right;}//如果parent節點大于它的某個子節點的話,此時篩操作if (nodeList[parent].level > nodeList[min].level){//子節點和父節點進行交換操作var temp = nodeList[parent];nodeList[parent] = nodeList[min];nodeList[min] = temp;//繼續進行更上一層的過濾parent = (int)Math.Ceiling(parent / 2d) - 1;}else{break;}}}#endregion#region 優先隊列的出隊操作/// <summary>/// 優先隊列的出隊操作/// </summary>/// <returns></returns>public HeapNode Dequeue(){if (nodeList.Count == 0)return null;//出隊列操作,彈出數據頭元素var pop = nodeList[0];//用尾元素填充頭元素nodeList[0] = nodeList[nodeList.Count - 1];//刪除尾節點nodeList.RemoveAt(nodeList.Count - 1);//然后從根節點下濾堆DownHeapAdjust(nodeList, 0);return pop;}#endregion#region 對堆進行下濾操作,使得滿足堆性質/// <summary>/// 對堆進行下濾操作,使得滿足堆性質/// </summary>/// <param name="nodeList"></param>/// <param name="index">非葉子節點的之后指針(這里要注意:我們/// 的篩操作時針對非葉節點的)/// </param>public void DownHeapAdjust(List<HeapNode> nodeList, int parent){while (2 * parent + 1 < nodeList.Count){//當前index節點的左孩子var left = 2 * parent + 1;//當前index節點的右孩子var right = left + 1;//parent子節點中最大的孩子節點,方便于parent進行比較//默認為left節點var min = left;//判斷當前節點是否有右孩子if (right < nodeList.Count){//判斷parent要比較的最大子節點min = nodeList[left].level < nodeList[right].level ? left : right;}//如果parent節點小于它的某個子節點的話,此時篩操作if (nodeList[parent].level > nodeList[min].level){//子節點和父節點進行交換操作var temp = nodeList[parent];nodeList[parent] = nodeList[min];nodeList[min] = temp;//繼續進行更下一層的過濾parent = min;}else{break;}}}#endregion#region 獲取元素并下降到指定的level級別/// <summary>/// 獲取元素并下降到指定的level級別/// </summary>/// <returns></returns>public HeapNode GetAndDownPriority(int level){if (nodeList.Count == 0)return null;//獲取頭元素var pop = nodeList[0];//設置指定優先級(如果為 MinValue 則為 -- 操作)nodeList[0].level = level == int.MinValue ? --nodeList[0].level : level;//下濾堆DownHeapAdjust(nodeList, 0);return nodeList[0];}#endregion#region 獲取元素并下降優先級/// <summary>/// 獲取元素并下降優先級/// </summary>/// <returns></returns>public HeapNode GetAndDownPriority(){//下降一個優先級return GetAndDownPriority(int.MinValue);}#endregion#region 返回當前優先隊列中的元素個數/// <summary>/// 返回當前優先隊列中的元素個數/// </summary>/// <returns></returns>public int Count(){return nodeList.Count;}#endregion}}