哈夫曼树又称最优二叉树,是一种带权路径长最短的树。树的路径长度是从树根到每一个叶子之间的路径长度之和。节点的带树路径长度为从该节点到树根之间的路径长度与该节点权(比如字符在某串中的使用频率)的乘积。

比如有一串字符串如:3334444555556666667777777,它是由3、4、5、6、7这五个数字组成的,现要使用一种编码方式,让它编码存储最短,如何做?如果五个数使用3位的定长的

二进制就可表示,如:(3:000) (4:001) (5:010) (6:100) (7:101),则编码后的存储空间需 3 * (3 + 4 + 5 + 6 + 7) = 75 比特位。能否有一种压缩的方法把存储空间缩小?这就是Huffman编码,它是一种不等长编码,这就要求一个字符编码的不是另一个字符编码的前缀,它是一种最优前缀编码。这需要一开始就需要统计出每个字符出现的频率,然后基于这些频率来设计出编码树,将可以节省大量的空间。利用字符出现的频率决定编码这一思想是Huffman编码的基础,Huffman编码是所有无前缀编码中最优的一种编码策略。Huffman编码是Unix中compress工具的基础,也是联合图的是专家组(JPEG)编码过程上的一部分。

人们在数据压缩领域使用了优先级队列。给定一段消息,可以对每个字符进行无前缀的编码,使其编码长度具有最少的比特位。使用Huffman树,可以得到这种最小编码。Huffman树是这样一棵完全的二叉树,它的每个叶节点都表示一个原消息中的不同字符,每个左分支都标为0,而每个右分支都标示为1。沿着根节点到叶节点字符的路径,将该路径中的分支标签依次组合起来,就可以得到该字符的Huffman编码。

下面给出二种编码的二叉树,但只有第二种是最优二叉树:

(:25)
    0/  \1
   (:18) 7
  0/  \1
(:7) (:11) 
0/ \1 0/ \1
3   4 5   6
权值 = (3 + 4 + 5 + 6) * 3 + 7 * 1 = 61(非最优二叉树)

(:25)
         0/   \1
        (:11) (:14)
       0/ \1  0/ \1
       5   6   7  (:7)
                  0/ \1
                  3   4
权值 = (3 + 4) * 3 + 7 * 2 + (5 + 6) * 2 = 57(最优二叉树)

因此,五个数的编码为 (3:000) (4:001) (7:01) (5:10) (6:11),从这些不等长编码来看,不存在一个字符的编码是另一个字符编码的前缀。一个保证无前缀比特编码的方法是创建一棵二叉树,它的左分支通常使用0来表示,而右分支用1来表示。如果每个已编码的字符都在树的叶子上,那么该字符的编码就不可能是其它字符编码的前缀,换句话说,到达每个字符路径正好是一个无前缀编码。

哈夫曼树的构造过程:从原始元素集合T中拿出两个频度最小的元素组成一个二叉树,二叉树的根为这两个节点频度的和,然后从集合T中删除这两个元素,把根元素加入到T集合中,如此反复直集合T为空。

那么我说究竟如果实现上面叙述的思想呢?
在统计完每个字符出现的频率之后,按照频率递增的顺序将每个字符—频率对插入到一个优先级队列中,即优先队列中具有最高优先级的字符—频率对中的字符具有最小的出现频率,这些字符将在离Huffman树根最远的叶子节点外结束,因此它们的编码具有最多的比特位。相反,出现频率最高的字符将具有最小的比特位编码。

首先将下列字符—频率对插入到优先队列中:
(3:3)(4:4)(5:5)(6:6)(7:7)
形成的初始堆如下:
      3
     / \
    4   5
   / \
6    7

基于字符—频率对组成的优先级队列所构造的二叉树称作Huffman树,我们将自底向上构建Huffman树。现假设所有字符元素都已按使用频率添加到了优先级队列中去了,即初始堆已构造好(如上述所示),下面开始构建Huffman树:

首先调用两次优先级队列的removeMin方法,得到两个频率最低的字符。“3”是第一个被删除的元素,即第一个出队的元素,它成为二叉树的左叶子节点,而“4”成为右叶节点,它们两者的频率之和(:7)成为树的根节点,并又将根(:7)添加到优先级队列中,现在得到如下的Huffman树:
      (:7)
     0/ \1
     3   4
此时优先级队列中包含:
(5:5)(6:6)(7:7)(:7)
堆结构如下:
      5
     / \
    6   7
   /
(:7)

然后,删除5、6,但它们不能直接连先前哈夫曼树中,因为它们元素都不在哈夫曼树中。因为它们成为另一棵树的左子叶节点和右子叶节点,且该树的根是它们的频率之后(:11),根将被插入到优先级队列中,现在有两棵Huffman树:
      (:11)    和        (:7)
     0/ \1             0/  \1
     5   6              3    4
此时,优先级队列中包含的元素如下:
(7:7) (:7) (:11)
堆结构如下:
      7
     /  \
(:7) (:11)

再然后,当(:7)被删除时,它成为二叉树的左分支,而另一个被删除的7元素则是树的右分支,两者频率之和成为二叉树的根(:14),被插入优先级队列中。由于(:7)在树中,所以这一次在原来已有的某树上进行扩充,这样就得到下面Huffman树:
  (:11)     和     (:14)
0/ \1            0/ \1
5   6            7  (:7)
                      0/ \1
                      3   4
此时优先队列中包含:
(:11) (:14)
堆结构如下:
(:11)
   /
(:14)

最后,删除(:11)与(:14)两个节点,由于这两个节点都存在于已创建好的Huffman中,所以这次实质上这次是合并这两个Huffman树,最后形成最终的Huffman树:
          (:25)
         0/   \1
        (:11) (:14)
       0/ \1  0/ \1
       5   6   7  (:7)
                  0/ \1
                  3   4

  1. package huffman;
  2.  
  3. import java.util.HashMap;
  4. import java.util.Iterator;
  5. import java.util.Map;
  6.  
  7. import priorityqueue.heap.Heap;
  8.  
  9. /**
  10. * 哈夫曼树与哈夫曼编解码
  11. *
  12. * @author jzj
  13. * @data 2010-1-8
  14. */
  15. public class Huffman {
  16.  
  17. //哈夫曼树节点
  18. private static class Entry implements Comparable<Entry> {
  19.  
  20. int freq;//节点使用频率,优先级就是根据此决定
  21. String code;//节点huffman编码
  22. char c;//节点所对应的字符
  23. Entry left, right, parent;//哈夫树遍历相关字段
  24.  
  25. //节点的优先级比较
  26. public int compareTo(Entry entry) {
  27. return freq - entry.freq;
  28. }
  29.  
  30. public String toString() {
  31. return "(" + c + ":" + code + ")";
  32. }
  33. }
  34.  
  35. //这里我们仅只对Unicodeue前256个字符编码,所以只能输入ISO8859-1字符串
  36. protected final int SIZE = 256;
  37.  
  38. //哈夫编码表,用于快速查询某字符的哈夫编码
  39. protected Entry[] leafEntries;
  40.  
  41. //堆,用来动态进行优先级排序
  42. protected Heap<Entry> pq;
  43.  
  44. //要编码的输入串
  45. protected String input;
  46.  
  47. public Huffman(String input) {
  48. this.input = input;
  49. createPQ();
  50. createHuffmanTree();
  51. calculateHuffmanCodes();
  52. }
  53.  
  54. //创建初始堆
  55. public void createPQ() {
  56.  
  57. //初始化哈夫编码表
  58. Entry entry;
  59. leafEntries = new Entry[SIZE];
  60. for (int i = 0; i < SIZE; i++) {
  61. leafEntries[i] = new Entry();
  62. leafEntries[i].freq = 0;//使用频率
  63. /*
  64. * leafEntries哈夫编码表中的索引与字符的编码对应,这样在读取时
  65. * 很方便
  66. */
  67.  
  68. leafEntries[i].c = (char) i;//节点点是对应的字符
  69.  
  70. }
  71.  
  72. //填充哈夫编码表
  73. fillLeafEntries();
  74.  
  75. //开始创建初始堆
  76. pq = new Heap<Entry>();
  77. for (int i = 0; i < SIZE; i++) {
  78. entry = leafEntries[i];
  79. if (entry.freq > 0) {//如果被使用过,则放入堆中
  80. pq.add(entry);
  81. }
  82. }
  83. }
  84.  
  85. //根据输入的字符串填充leafEntries哈夫编码表
  86. public void fillLeafEntries() {
  87.  
  88. Entry entry;
  89.  
  90. for (int i = 0; i < input.length(); i++) {
  91.  
  92. entry = leafEntries[(int) (input.charAt(i))];
  93. entry.freq++;
  94. entry.left = null;
  95. entry.right = null;
  96. entry.parent = null;
  97. }
  98. }
  99.  
  100. // 创建哈夫曼树
  101. public void createHuffmanTree() {
  102.  
  103. Entry left, right, parent;
  104.  
  105. //每次需从堆中取两个,所以需大于1,如果小于等于1时表示哈夫曼树已创建完毕
  106. while (pq.size() > 1) {
  107.  
  108. // 使用贪婪法,每次从优先级队列中读取最小的两个元素
  109. left = (Entry) pq.removeMin();
  110. left.code = "0";//如果做为左子节点,则为路径编码为0
  111.  
  112. right = (Entry) pq.removeMin();
  113. right.code = "1";//如果做为右子节点,则为路径编码为1
  114.  
  115. parent = new Entry();
  116. parent.parent = null;
  117.  
  118. //父节点的使用频度为两者之和
  119. parent.freq = left.freq + right.freq;
  120. parent.left = left;
  121. parent.right = right;
  122. left.parent = parent;
  123. right.parent = parent;
  124.  
  125. //再把父节点放入堆中,将会进行重组堆结构
  126. pq.add(parent);
  127. }
  128. }
  129.  
  130. // 计算输入串的每个字符的哈夫编码
  131. public void calculateHuffmanCodes() {
  132.  
  133. String code;
  134. Entry entry;
  135.  
  136. for (int i = 0; i < SIZE; i++) {
  137.  
  138. code = "";
  139. entry = leafEntries[i];
  140. if (entry.freq > 0) {//如果使用过该字符时就需要求哈夫编码
  141.  
  142. do {
  143. /*
  144. * 拼接从叶节点到根节点路径上各元素的路径编码,最后得到哈夫编码,
  145. * 注,这里倒着来的,所以不能有这样:code = code + entry.code;
  146. */
  147. code = entry.code + code;
  148. entry = entry.parent; // 要一直循环到根
  149. } while (entry.parent != null);
  150.  
  151. leafEntries[i].code = code;//设置最后真真的哈夫编码
  152.  
  153. }
  154. }
  155. }
  156.  
  157. //得到哈夫曼编码表
  158. public Map<String, String> getHuffmancodeTable() {
  159.  
  160. Map<String, String> map = new HashMap<String, String>();
  161.  
  162. for (int i = 0; i < SIZE; i++) {
  163. Entry entry = leafEntries[i];
  164. if (entry.freq > 0) {//如果使用过该字符时就需求哈夫编码
  165. map.put(String.valueOf(entry.c), entry.code);
  166. }
  167. }
  168.  
  169. return map;
  170. }
  171.  
  172. //得到字符串所对应的哈夫曼编码
  173. public String getHuffmancodes() {
  174. StringBuffer sb = new StringBuffer();
  175. for (int i = 0; i < input.length(); i++) {
  176. Entry entry = leafEntries[input.charAt(i)];
  177. sb.append(entry.code);
  178. }
  179. return sb.toString();
  180. }
  181.  
  182. //将huffman消息串还原成字符串
  183. public static String huffmancodesToString(Map<String, String> map, String huffmanCodes) {
  184. Entry root = createTreeFromCode(map);
  185. return encoding(root, huffmanCodes);
  186. }
  187.  
  188. //根据指定的哈夫曼编码创建哈夫曼树
  189. private static Entry createTreeFromCode(Map<String, String> map) {
  190. Iterator<Map.Entry<String, String>> itr = map.entrySet().iterator();
  191. Map.Entry<String, String> mapEntry;
  192. Entry root = new Entry(), parent = root, tmp;
  193.  
  194. while (itr.hasNext()) {
  195. mapEntry = itr.next();
  196.  
  197. //从根开始创建树
  198. for (int i = 0; i < mapEntry.getValue().length(); i++) {
  199.  
  200. if (mapEntry.getValue().charAt(i) == '0') {
  201. tmp = parent.left;
  202. if (tmp == null) {
  203. tmp = new Entry();
  204. parent.left = tmp;
  205. tmp.parent = parent;
  206. tmp.code = "0";
  207. }
  208. } else {
  209. tmp = parent.right;
  210. if (tmp == null) {
  211. tmp = new Entry();
  212. parent.right = tmp;
  213. tmp.parent = parent;
  214. tmp.code = "1";
  215. }
  216. }
  217.  
  218. if (i == mapEntry.getValue().length() - 1) {
  219. tmp.c = mapEntry.getKey().charAt(0);
  220. tmp.code = mapEntry.getValue();
  221. parent = root;
  222. } else {
  223. parent = tmp;
  224. }
  225. }
  226.  
  227. }
  228. return root;
  229. }
  230.  
  231. //根据给定的哈夫曼编码解码成字符
  232. private static String encoding(Entry root, String huffmanCodes) {
  233. Entry tmp = root;
  234. StringBuffer sb = new StringBuffer();
  235.  
  236. for (int i = 0; i < huffmanCodes.length(); i++) {
  237. if (huffmanCodes.charAt(i) == '0') {
  238. tmp = tmp.left;//找到与当前编码对应的节点
  239. //如果哈夫曼树左子树为空,则右子树也肯定为空,也就是说,分支节点一定是用两个节点的节点
  240. if (tmp.left == null) {//如果为叶子节点,则找到完整编码
  241. sb.append(tmp.c);
  242. tmp = root;//准备下解码下一个字符
  243. }
  244. } else {
  245. tmp = tmp.right;
  246. if (tmp.right == null) {
  247. sb.append(tmp.c);
  248. tmp = root;
  249. }
  250. }
  251. }
  252. return sb.toString();
  253. }
  254.  
  255. public static void main(String[] args) {
  256. String inputStr = "3334444555556666667777777";
  257. Huffman hfm = new Huffman(inputStr);
  258.  
  259. Map<String, String> map = hfm.getHuffmancodeTable();
  260. String huffmancodes = hfm.getHuffmancodes();
  261. System.out.println("输入字符串 - " + inputStr);
  262. System.out.println("哈夫曼编码对照表 - " + map);
  263. System.out.println("哈夫曼编码 - " + huffmancodes);
  264. String encodeStr = Huffman.huffmancodesToString(map, huffmancodes);
  265. System.out.println("哈夫曼解码 - " + encodeStr);
  266. /*
  267. * output:
  268. * 输入字符串 - 3334444555556666667777777
  269. * 哈夫曼编码对照表 - {3=110, 5=00, 7=10, 4=111, 6=01}
  270. * 哈夫曼编码 - 110110110111111111111000000000001010101010110101010101010
  271. * 哈夫曼解码 - 3334444555556666667777777
  272. */
  273. }
  274. }

[数据结构与算法]哈夫曼(Huffman)树与哈夫曼编码的更多相关文章

  1. 哈夫曼(Huffman)树和哈夫曼编码

    一.哈夫曼(Huffman)树和哈夫曼编码 1.哈夫曼树(Huffman)又称最优二叉树,是一类带权路径长度最短的树, 常用于信息检测. 定义: 结点间的路径长度:树中一个结点到另一个结点之间分支数目 ...

  2. Java数据结构和算法(七)B+ 树

    Java数据结构和算法(七)B+ 树 数据结构与算法目录(https://www.cnblogs.com/binarylei/p/10115867.html) 我们都知道二叉查找树的查找的时间复杂度是 ...

  3. 哈夫曼(Huffman)树+哈夫曼编码

    前天acm实验课,老师教了几种排序,抓的一套题上有一个哈夫曼树的题,正好之前离散数学也讲过哈夫曼树,这里我就结合课本,整理一篇关于哈夫曼树的博客. 主要摘自https://www.cnblogs.co ...

  4. Huffman树、霍夫曼编码

    Huffman树指的是带权路径长度WPL最小的二叉树 WPL=路径*权值 Huffman常用于压缩编码,正常传输ABCDEF这些字母需要3位二进制树来描述,但由于一篇文章中ABCDEF这些字母出现的概 ...

  5. 数据结构与算法(周测3-Huffman树)

    判断题 1.Given a Huffman tree for N (≥2) characters, all with different weights. The weight of any non- ...

  6. 为什么我要放弃javaScript数据结构与算法(第八章)—— 树

    之前介绍了一些顺序数据结构,介绍的第一个非顺序数据结构是散列表.本章才会学习另一种非顺序数据结构--树,它对于存储需要快速寻找的数据非常有用. 本章内容 树的相关术语 创建树数据结构 树的遍历 添加和 ...

  7. 看图轻松理解数据结构与算法系列(NoSQL存储-LSM树) - 全文

    <看图轻松理解数据结构和算法>,主要使用图片来描述常见的数据结构和算法,轻松阅读并理解掌握.本系列包括各种堆.各种队列.各种列表.各种树.各种图.各种排序等等几十篇的样子. 关于LSM树 ...

  8. 17-看图理解数据结构与算法系列(NoSQL存储-LSM树)

    关于LSM树 LSM树,即日志结构合并树(Log-Structured Merge-Tree).其实它并不属于一个具体的数据结构,它更多是一种数据结构的设计思想.大多NoSQL数据库核心思想都是基于L ...

  9. 【数据结构与算法】Trie(前缀树)模板和例题

    Trie 树的模板 Trie 树的简介 Trie树,又称字典树,单词查找树或者前缀树,是一种用于快速检索的多叉树结构,如英文字母的字典树是一个26叉树,数字的字典树是一个10叉树.他的核心思想是空间换 ...

随机推荐

  1. sql server 2008 安装过程与创建建sql server登录用户

    1.sql server 下载安装包路径:http://pan.baidu.com/s/1qWuzddq 2.安装过程图解教程 ,参照网址:http://jingyan.baidu.com/album ...

  2. 【python cookbook】【字符串与文本】1.针对任意多的分隔符拆分字符串

    问题:将分隔符(以及分隔符之间的空格)不一致的字符串拆分为不同的字段: 解决方案:使用更为灵活的re.split()方法,该方法可以为分隔符指定多个模式. 说明:字符串对象的split()只能处理简单 ...

  3. 160921、React入门教程第一课--从零开始构建项目

    工欲善其事必先利其器,现在的node环境下,有太多好用的工具能够帮助我们更好的开发和维护管理项目. 我本人不建议什么功能都自己写,我比较喜欢代码复用.只要能找到npm包来实现的功能,坚决不自己敲代码. ...

  4. FTPS加密上传

    公司要求ftp接口不能以明文方式传输,所以adc系统将增加ftps方式 但是在网找了很多方式都无法实现用了方法一 FtpWebRequest request = (FtpWebRequest)WebR ...

  5. js 字符串比较

    <script type="text/javascript"> function test(){ //1)纯数字之间比较 //alert(1<3);//true ...

  6. mysqldump备份过程中都干了些什么

    mysqldump备份方便,易读,功能丰富,相信大家都有 使用过这个命令进行备份,但是这个命令在备份的过程中都做了写什么呢,下面打开general_log进行查看: 1.登录mysql命令行客户端: ...

  7. [Unity3D]MonoBehaviour函数介绍

    原文地址: http://www.cocos2dev.com/?p=486 Unity中的脚本都是继承自MonoBehaviour. 一.基础函数: 创建脚本就默认的update.start方法:(这 ...

  8. ACM题目————图的广度优先搜索

    题目描述 图的广度优先搜索类似于树的按层次遍历,即从某个结点开始,先访问该结点,然后访问该结点的所有邻接点,再依次访问各邻接 点的邻接点.如此进行下去,直到所有的结点都访问为止.在该题中,假定所有的结 ...

  9. 【转】MYSQL入门学习之八:数据库及表的基本操作

    转载地址:http://www.2cto.com/database/201212/175867.html 一.操作数据库  www.2cto.com    1.查看数据库          show ...

  10. PHP和ajax详解

    优点:减轻服务器的负担,按需取数据,最大程度的减少冗余请求局部刷新页面,减少用户心理和实际的等待时间,带来更好的用户体验基于xml标准化,并被广泛支持,不需安装插件等进一步促进页面和数据的分离缺点:A ...