哈夫曼(Huffman)树与哈夫曼编码

声明:原创作品,转载时请注明文章来自SAP师太技术博客:www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将追究法律责任!原文链接:http://www.cnblogs.com/jiangzhengjun/p/4289610.html

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

比如有一串字符串如: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 }
时间: 2024-12-19 15:38:36

哈夫曼(Huffman)树与哈夫曼编码的相关文章

哈夫曼 (Huffman) 树的动画演示

 哈夫曼 (Huffman) 树的动画演示: http://people.cs.pitt.edu/~kirk/cs1501/animations/Huffman.html 此网站中亦有诸多其它算法的动画演示,可供学习算法或是数据结构相关内容时参考.

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

一.哈夫曼(Huffman)树和哈夫曼编码 1.哈夫曼树(Huffman)又称最优二叉树,是一类带权路径长度最短的树, 常用于信息检测. 定义: 结点间的路径长度:树中一个结点到另一个结点之间分支数目称为这对结点之间的路径长度. 树的路径长度:树的根结点到树中每一结点的路径长度之和. 带权路径长度:从根结点到某结点的路径长度与该结点上权的乘积. 树的带权路径长度:树中所有叶子结点的带权路径长度之和记为WPL. 例如: 对图(a): WPL =9×2+5×2+2×2+3×2=38 对图(b): W

哈夫曼树和哈夫曼编码

在一般的数据结构的书中,树的那章后面,著者一般都会介绍一下哈夫曼(HUFFMAN)树和哈夫曼编码.哈夫曼编码是哈夫曼树的一个应用.哈夫曼编码应用广泛,如JPEG中就应用了哈夫曼编码. 首先介绍什么是哈夫曼树. 哈夫曼树又称最优二叉树,是一种带权路径长度最短的二叉树.所谓树的带权路径长度,就是树中所有的叶结点的权值乘上其到根结点的 路径长度(若根结点为0层,叶结点到根结点的路径长度为叶结点的层数).树的带权路径长度记为WPL= (W1*L1+W2*L2+W3*L3+...+Wn*Ln),N个权值W

HUFFMAN 树

在一般的数据结构的书中,树的那章后面,著者一般都会介绍一下哈夫曼(HUFFMAN) 树和哈夫曼编码.哈夫曼编码是哈夫曼树的一个应用.哈夫曼编码应用广泛,如 JPEG中就应用了哈夫曼编码. 首先介绍什么是哈夫曼树.哈夫曼树又称最优二叉树, 是一种带权路径长度最短的二叉树.所谓树的带权路径长度,就是树中所有的叶结点 的权值乘上其到根结点的 路径长度(若根结点为0层,叶结点到根结点的路径长度 为叶结点的层数).树的带权路径长度记为WPL= (W1*L1+W2*L2+W3*L3+...+Wn*Ln) ,

Huffman树及其应用

哈夫曼树又称为最优二叉树,哈夫曼树的一个最主要的应用就是哈夫曼编码,本文通过简单的问题举例阐释哈夫曼编码的由来,并用哈夫曼树的方法构造哈夫曼编码,最终解决问题来更好的认识哈夫曼树的应用--哈夫曼编码. 一.引子 在学习中我们经常遇到将各科成绩改为优秀.良好.中等.及格和不及格.那么根据分级原理,代码表示为: if(a<60) b = "不及格"; else if(a<70) b = "及格"; else if(a<80) b = "中等&

Huffman树

结点定义: 1 /* 2 * Huffman树结点定义 3 */ 4 struct Node 5 { 6 ElementType weight; // 结点的权值 7 struct Node *leftChild; // 结点的左指针 8 struct Node *rightChild; // 结点的右指针 9 }; 根据给定权值数组,构建一个Huffman树: 1 /* 2 * 输出内存申请失败的消息 3 */ 4 void showFailureMessage() 5 { 6 printf(

Huffman tree(赫夫曼树、霍夫曼树、哈夫曼树、最优二叉树)

flyfish 2015-8-1 Huffman tree因为翻译不同所以有其他的名字 赫夫曼树.霍夫曼树.哈夫曼树 定义引用自严蔚敏<数据结构> 路径 从树中一个结点到另一个结点之间的分支构成两个结点之间的路径. 路径长度 路径上的分支数目称作路径长度. 树的路径长度 树的路径长度就是从根节点到每一结点的路径长度之和. 结点的带权路径长度 结点的带权路径长度就是从该结点到根节点之间的路径长度与结点上权的乘积. 树的带权路径长度 树的带权路径长度就是树中所有叶子结点的带权路径长度之和,通常记做

哈夫曼(huffman)树和哈夫曼编码

哈夫曼树 哈夫曼树也叫最优二叉树(哈夫曼树) 问题:什么是哈夫曼树? 例:将学生的百分制成绩转换为五分制成绩:≥90 分: A,80-89分: B,70-79分: C,60-69分: D,<60分: E. if (a < 60){ b = 'E'; } else if (a < 70) { b = ‘D’; } else if (a<80) { b = ‘C’; } else if (a<90){ b = ‘B’; } else { b = ‘A’; } 判别树:用于描述分类

(转)哈夫曼(huffman)树和哈夫曼编码

原文地址 哈夫曼树也叫最优二叉树(哈夫曼树) 问题:什么是哈夫曼树? 例:将学生的百分制成绩转换为五分制成绩:≥90 分: A,80-89分: B,70-79分: C,60-69分: D,<60分: E. if (a < 60){ b = 'E'; } else if (a < 70) { b = ‘D’; } else if (a<80) { b = ‘C’; } else if (a<90){ b = ‘B’; } else { b = ‘A’; } 判别树:用于描述分类