赫夫曼编码是赫夫曼树在电讯通信中的经典应用之一。
广泛地用于数据文件压缩。其压缩率通常在20%~90%之间。
赫夫曼编码是可变长编码(VLC)的一种。
赫夫曼编码压缩数据,是无损压缩

原理剖析

定长编码

比如下面的字符串:

i like like like java do you like a java

一共有40个字符(包括标点符号和空格)

计算机中的信息最终都会转换成01二进制,上面的字符串最终转换成二进制如下

01101001001000000110110001101001011010110110010100100000011011000110100101101011011001010010000001101100011010010110101101100101001000000110101001100001011101100110000100100000011001000110111100100000011110010110111101110101001000000110110001101001011010110110010100100000011000010010000001101010011000010111011001100001

上面的二进制可以参考下图,不足8位的在前面补0,8位一字节,就是上面字符串的一个字符。
转换成二进制后,二进制的长度是320
image.png

变长编码

i like like like java do you like a java

  1. 统计每个字符对应的个数
    • d:1
    • y:1
    • u:1
    • j:2
    • v:2
    • o:2
    • l:4
    • e:4
    • i:5
    • a:5
    • :9
  2. 按各个字符出现的次数进行编码,原则是出现次数越多的,则编码越小,比如空格出现了9次,编码为0
    • 0=
    • 1=a
    • 10=i
    • 11=e
    • 100=k
    • 101=l
    • 110=o
    • 111=v
    • 1000=j
    • 1001=u
    • 1010=y
    • 1011=d
  3. 按照上面的各个字符编码规定,最终的编码是

    10010110100……

根据颜色可以一一对应上面的编码规则,但是这种编码存在二义性。
比如前面三个字符100,这里是分成10,0,代表i,也可以划分成1,0,0,代表a
解决这种二义性的办法,如下

前缀编码

字符的编码都不能是其他字符编码的前缀,符合此要求的编码叫做前缀编码,即不能匹配到重复的编码。
具体例子如下

赫夫曼编码

i like like like java do you like a java

  1. 统计每个字符对应的个数
    • d:1
    • y:1
    • u:1
    • j:2
    • v:2
    • o:2
    • l:4
    • e:4
    • i:5
    • a:5
    • :9
  2. 按照上面字符出现的次数构建一颗赫夫曼树(看赫夫曼树的笔记),次数作为权值,字符也要记录在节点里面

image.png

  1. 根据赫夫曼树给各个字符规定编码,向左的路径为0,向右的路径为1,编码如下:
    • o:1000
    • u:10010
    • d:100110
    • y:100111
    • i:101
    • a:110
    • k:1110
    • e:1111
    • j:0000
    • v:0001
    • l:001
    • :01

赫夫曼编码是前缀编码,没有哪个编码是另一个编码的前缀,这样就不会出现多义性

  1. 按照赫夫曼编码,上面的字符对应的编码就是

    101010011011110111101001101111011110100110111101111010000110000111001100111100011001111000100100100110111101111011100100001100001110

如上编码,匹配数据时,从左到右,1没有,10没有,101,发现能匹配到i,101后面的0没有,01发现能匹配空格……

赫夫曼编码长度变成了132,压缩了(320-132)/320=58.75%

注意:
赫夫曼编码根据排序方法的不同,也可能不太一样,这样对应的赫夫曼编码不完全一样,但是WPL是一样的,都是最小的。所以最终的结果都是一样的

赫夫曼编码与解码代码

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.IO;
  5. using System.Runtime.Serialization.Formatters.Binary;
  6. using System.Text;
  7. namespace ConsoleApp1
  8. {
  9. class Program
  10. {
  11. //生成赫夫曼树对应的赫夫曼编码,用字典存,key是原始数据,value是编码值
  12. static Dictionary<byte, string> huffmanCodes = new Dictionary<byte, string>();
  13. static void Main(string[] args)
  14. {
  15. string str = "i like like like java do you like a java";
  16. //得到字节数组
  17. byte[] bytes = Encoding.Default.GetBytes(str);
  18. Console.WriteLine("赫夫曼编码后的字节数组");
  19. byte[] hfcb = huffmanZip(bytes);
  20. foreach (var b in hfcb)
  21. {
  22. Console.Write(b + " ");
  23. }
  24. Console.WriteLine();
  25. Console.WriteLine("解码");
  26. byte[] dbytes = decode(hfcb);
  27. Console.WriteLine(Encoding.Default.GetString(dbytes));
  28. }
  29. #region 编码
  30. //把数据字节数组做成节点集合
  31. public static List<HFNode> getNodes(byte[] bytes)
  32. {
  33. List<HFNode> nodes = new List<HFNode>();
  34. //存储每个byte出现的次数,用字典,唯一键
  35. Dictionary<byte, int> weights = new Dictionary<byte, int>();
  36. foreach(var b in bytes)
  37. {
  38. bool bo =weights.ContainsKey(b);
  39. if (bo)
  40. weights[b] += 1;
  41. else
  42. weights.Add(b, 1);
  43. }
  44. //把每个键值对转换成nodes集合
  45. foreach(var w in weights)
  46. {
  47. nodes.Add(new HFNode(w.Value, w.Key));
  48. }
  49. return nodes;
  50. }
  51. //创建赫夫曼树
  52. public static HFNode CreateHuffmanTree(List<HFNode> nodes)
  53. {
  54. while (nodes.Count > 1)
  55. {
  56. //排序从小到大
  57. nodes.Sort();
  58. //取出前两棵最小的二叉树
  59. HFNode leftnode = nodes[0];
  60. HFNode rightnode = nodes[1];
  61. //合并成一颗新的二叉树
  62. HFNode parent = new HFNode(leftnode.weight + rightnode.weight, 0);
  63. parent.left = leftnode;
  64. parent.right = rightnode;
  65. //去掉已经使用的两个节点,加入新的节点
  66. nodes.Remove(leftnode);
  67. nodes.Remove(rightnode);
  68. nodes.Add(parent);
  69. }
  70. //nodes最后的一个节点就是赫夫曼树的根节点
  71. return nodes[0];
  72. }
  73. //将传入的hfnode节点的所有叶子节点的赫夫曼编码得到
  74. //node是传入节点
  75. //code是路径:左子节点是0,又子节点是1
  76. //path是用于拼接路径
  77. public static void getCodes(HFNode node,string code,StringBuilder path)
  78. {
  79. path.Append(code);
  80. if (node != null)
  81. {
  82. //判断当前节点是叶子节点还是非叶子节点
  83. if (node.left!=null || node.right!=null)//左右子节点只要有一个不为null,就是非叶子节点
  84. {
  85. //递归
  86. getCodes(node.left, "0", path);
  87. getCodes(node.right, "1", path);
  88. }
  89. else//叶子节点
  90. {
  91. //表示找到了某个叶子节点的最后
  92. huffmanCodes.Add(node.data, path.ToString());
  93. }
  94. }
  95. }
  96. //将字符串对应的byte[]数组,通过生成赫夫曼编码表,返回一个赫夫曼编码压缩后的byte[]
  97. //bytes:原始数组
  98. public static byte[] Zip(byte[] bytes)
  99. {
  100. //利用huffmanCodes将bytes转成赫夫曼编码对应的字符串
  101. string hfcstr = "";//赫夫曼编码对应的字符串
  102. StringBuilder strb = new StringBuilder();//用StringBuilder来拼接,速度快
  103. foreach( var b in bytes)
  104. {
  105. //在赫夫曼编码字典里面查找
  106. strb.Append(huffmanCodes[b]);
  107. }
  108. hfcstr = strb.ToString();
  109. //Console.WriteLine(hfcstr);
  110. //将hfcstr字符串,8位一字节的转换成一个byte[]数组
  111. //先计算byte[]的长度,如果hfcstr%8有余数,始终都要+1,不如直接+7后/8,保留整数部分
  112. int len = (hfcstr.Length + 7) / 8;
  113. //创建存储压缩后的byte数组
  114. byte[] hfcb = new byte[len];
  115. int index = 0;//数组的计数器,记录第几个byte
  116. for(int i = 0; i < hfcstr.Length; i += 8)//8位1字节所以步长+8
  117. {
  118. //从i开始截取8个字符
  119. string strbyte = "";
  120. if (i + 8 > hfcstr.Length)//最后一次,不够8位
  121. strbyte += hfcstr.Substring(i);
  122. else
  123. strbyte += hfcstr.Substring(i, 8);
  124. //将strbyte转换成byte放入到hfcb里面
  125. hfcb[index++] = Convert.ToByte(strbyte,2);
  126. }
  127. return hfcb;
  128. }
  129. /// <summary>
  130. /// 赫夫曼压缩总方法
  131. /// </summary>
  132. /// <param name="bytes">原始字节数组</param>
  133. /// <returns></returns>
  134. public static byte[] huffmanZip(byte[] bytes)
  135. {
  136. //1.根据原始字节数据,得到赫夫曼节点集合
  137. List<HFNode> hfl = getNodes(bytes);
  138. //2.根据赫夫曼节点集合创建赫夫曼树,得到根节点
  139. HFNode root = CreateHuffmanTree(hfl);
  140. //3.得到赫夫曼编码字典
  141. getCodes(root, "", new StringBuilder());
  142. //4.得到一个赫夫曼编码压缩后的字节数组
  143. byte[] hfcb = Zip(bytes);
  144. return hfcb;
  145. }
  146. #endregion
  147. #region 解码
  148. //将一个byte转换成一个二进制的字符串,flag:最后一个字节不用补高位,这儿flag就是判断是否补高位的
  149. public static string byteTobitstring(byte b,bool flag)
  150. {
  151. string bitstr = Convert.ToString(b,2);//转换成二进制字符串
  152. if (flag)//要补高位
  153. {
  154. int len = 8 - bitstr.Length;
  155. for (int t = 0; t < len; t++)
  156. bitstr = "0" + bitstr;
  157. }
  158. return bitstr;
  159. }
  160. /// <summary>
  161. /// 赫夫曼解码.
  162. /// </summary>
  163. /// <param name="huffmanBytes">传入一个赫夫曼编码后的一个字节数组</param>
  164. /// <returns></returns>
  165. public static byte[] decode(byte[] huffmanBytes)
  166. {
  167. //先得到huffmanBytes对应的二进制字符串
  168. string bitstr = "";
  169. StringBuilder sb = new StringBuilder();//用StringBuilder拼接速度快
  170. for(int i = 0; i < huffmanBytes.Length; i++)
  171. {
  172. //判断是否是最后一个字节,如果是最后一个字节,不用补高位,是多少位就是多少位数
  173. bool b = !(i == huffmanBytes.Length - 1);
  174. sb.Append(byteTobitstring(huffmanBytes[i], b));
  175. }
  176. bitstr = sb.ToString();
  177. Console.WriteLine("得到对应的二进制字符串完成");
  178. //Console.WriteLine(bitstr);
  179. //把字符串按照指定的赫夫曼编码进行解码
  180. //原来的字典是通过字符ASCII码查询赫夫曼编码,现在重新建一个字典,反过来通过赫夫曼编码查询字符ASCII码
  181. Dictionary<string, byte> hfcodes = new Dictionary<string, byte>();
  182. foreach(var a in huffmanCodes)
  183. hfcodes.Add(a.Value, a.Key);
  184. //创建一个集合存放byte
  185. List<byte> list = new List<byte>();
  186. //扫描bitstr,i就是每个字符的指针
  187. for(int i = 0; i < bitstr.Length; i++)
  188. {
  189. //长度,从i开始多少长度的字符串,是一个赫夫曼编码。这个长度区间在1——bitstr.Length - i之间,一定得是<=,能匹配到最后一个字符
  190. for (int t = 1; t <= bitstr.Length - i; t++)
  191. {
  192. //判断从i开始,取t个字符的字符串是不是已经存在的赫夫曼编码,
  193. bool b = hfcodes.ContainsKey(bitstr.Substring(i, t));
  194. if(b)
  195. {
  196. //如果是赫夫曼编码,就把数据字符的ASCII码加入到list中
  197. list.Add(hfcodes[bitstr.Substring(i, t)]);
  198. i += t-1;//从0开始到i+t之前的字符串已经扫描过了,现在从i+t开始,为什么要t-1,因为这里退出后,进入外层循环,i++,先-1,在++,就保持不变
  199. break;
  200. }
  201. }
  202. }
  203. //到这里list里面就已经存放了所有字符的ASCII码
  204. return list.ToArray();
  205. }
  206. #endregion
  207. /// <summary>
  208. /// 赫夫曼节点实现IComparable接口,作用是进行排序
  209. /// </summary>
  210. public class HFNode : IComparable<HFNode>
  211. {
  212. public int weight;//权值,表示字符出现的次数
  213. public byte data;//存放数据本身,比如'a'==97,' '==32,存放ASCII码
  214. public HFNode left;
  215. public HFNode right;
  216. public HFNode(int weight,byte data)
  217. {
  218. this.weight = weight;
  219. this.data = data;
  220. }
  221. //实现接口方法
  222. public int CompareTo(HFNode hfnode)
  223. {
  224. //从小到大排序
  225. return this.weight - hfnode.weight;
  226. }
  227. //前序遍历
  228. public void preOrder()
  229. {
  230. Console.WriteLine(this.weight);
  231. if (this.left != null)
  232. this.left.preOrder();
  233. if (this.right != null)
  234. this.right.preOrder();
  235. }
  236. }
  237. }
  238. }

赫夫曼编码压缩文件代码

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.IO;
  5. using System.Runtime.Serialization.Formatters.Binary;
  6. using System.Text;
  7. namespace ConsoleApp1
  8. {
  9. class Program
  10. {
  11. //生成赫夫曼树对应的赫夫曼编码,用字典存,key是原始数据,value是编码值
  12. static Dictionary<byte, string> huffmanCodes = new Dictionary<byte, string>();
  13. static void Main(string[] args)
  14. {
  15. //zipFile(@"D:\B\a.pdf", @"D:\B\a.dat");
  16. unzipFile(@"D:\B\a.dat", @"D:\B\Aa.pdf");
  17. }
  18. //省略部分和上面一样
  19. #region 编码
  20. public static List<HFNode> getNodes(byte[] bytes){}
  21. public static HFNode CreateHuffmanTree(List<HFNode> nodes){}
  22. public static void getCodes(HFNode node,string code,StringBuilder path){}
  23. public static byte[] Zip(byte[] bytes){}
  24. public static byte[] huffmanZip(byte[] bytes){}
  25. #endregion
  26. #region 解码
  27. public static string byteTobitstring(byte b,bool flag){}
  28. public static byte[] decode(byte[] huffmanBytes){}
  29. #endregion
  30. #region 文件压缩
  31. public static void zipFile(string srcFileName,string dstFileName)
  32. {
  33. FileStream srcstream = new FileStream(srcFileName, FileMode.Open, FileAccess.Read);
  34. byte[] b = new byte[srcstream.Length];
  35. srcstream.Read(b, 0, b.Length);
  36. byte[] hfcb = huffmanZip(b);
  37. //序列化两个东西到文件里面,赫夫曼字节数组,和赫夫曼编码字典
  38. FileStream dststream = new FileStream(dstFileName, FileMode.OpenOrCreate, FileAccess.Write);
  39. BinaryFormatter formatter = new BinaryFormatter();
  40. formatter.Serialize(dststream, hfcb);
  41. formatter.Serialize(dststream, huffmanCodes);
  42. srcstream.Dispose();
  43. dststream.Dispose();
  44. Console.WriteLine("压缩成功");
  45. }
  46. public static void unzipFile(string zipFileName,string dstFileName)
  47. {
  48. FileStream zipstream = new FileStream(zipFileName, FileMode.Open, FileAccess.Read);
  49. BinaryFormatter formatter = new BinaryFormatter();
  50. byte[] hfbs = (byte[])formatter.Deserialize(zipstream);
  51. huffmanCodes = (Dictionary<byte, string>)formatter.Deserialize(zipstream);
  52. Console.WriteLine("反序列化完成");
  53. //解压赫夫曼编码
  54. byte[] bytes = decode(hfbs);
  55. Console.WriteLine("解压赫夫曼编码完成");
  56. //输出文件
  57. FileStream dststream = new FileStream(dstFileName, FileMode.OpenOrCreate, FileAccess.Write);
  58. dststream.Write(bytes, 0, bytes.Length);
  59. zipstream.Dispose();
  60. dststream.Dispose();
  61. Console.WriteLine("解压成功");
  62. }
  63. #endregion
  64. public class HFNode : IComparable<HFNode>{}
  65. }
  66. }