[知识点] 7.5 Huffman哈夫曼树

总目录 > 7 数据结构 > 7.5 Huffman 哈夫曼树

前言

这是一篇原网站没有涉及到的知识点,很久之前也学过了,趁着数据结构课要做实验就再复习下好啦。

子目录列表

1、最优二叉树

2、构造方法

3、哈夫曼编码

4、译码

5、代码

7.5 Huffman 哈夫曼树

1、最优二叉树

哈夫曼树(Huffman Tree),又名最优树,是一类带权路径长度最短的树,应用比较广泛。这里我们暂时只讨论最优二叉树

先给出几个概念:

路径:从树中一个结点到另一个结点之间的分支构成两个结点之间的路径;

路径长度:路径上的分支数目;

树的路径长度:从根结点到每一个结点的路径长度之和。

哈夫曼树的特点在于,有且仅有叶子结点带权,比如:

带权路径长度:根结点到该结点的路径长度与结点权值的乘积;

树的带权路径长度:树中所有叶子结点的带权路径长度之和,通常记作:

WPL = ∑ w[k] * l[k]

其中,k ∈ [1, n],n 表示结点个数,w[k] 表示结点 k 的权值,l[k] 表示结点 k 到根结点的路径长度。 

扯了这么多,回到最开始的定义:哈夫曼树是带权路径长度最短的树,即给出若干个结点构造一棵树,当且仅当其 WPL 是所有构造方式中最小的,就是哈夫曼树,例如上图的 4 个字符 {a, b, c, d},其三种构造方式:

图 a 对应:WPL = 7 * 2 + 5 * 2 + 2 * 2 + 4 * 2 = 36

图 b 对应:WPL = 7 * 3 + 5 * 3 + 2 * 1 + 4 * 2 = 46

图 c 对应:WPL = 7 * 1 + 5 * 2 + 2 * 3 + 4 * 3 = 35

我们发现,图 c 所表示的树正是哈夫曼树。

2、构造方法

给出 n 个带权结点,第 i 个结点权值为 w[i]。

① 将 n 个结点视作 n 棵二叉树,有且仅有一个根结点,根结点权值为 w[i],全部放入二叉树集合 T;

② 构造一棵新二叉树。从 T 中选择两棵根结点最小的树,分别作为这棵新二叉树的左右子树;

③ 将新二叉树加入集合 T,将被选择的两棵原二叉树移除;

④ 反复执行 ② 和 ③,直到 T 中只有一棵树。此时,这棵树即为哈夫曼树。

3、哈夫曼编码

说了这么多,还没有看出哈夫曼树的意义在哪?

数字国和字母国因为争论谁是世界霸主而掀起一场旷世大战。数字国在字母国安插了许多间谍,有一天一个间谍截获了一份字母国写满了字母的机密文件,想要通过用电文传回给数字国,但担心被字母国发现,于是决定将字母全部转化为 0 和 1 两个数字,现在他需要考虑一个最合适的转换方案,使其发送的电文尽可能短。

我们假设这份机密文件只有 "abaccda" 这 4 种字符,7 个字母。最朴素的编码方案为:

a - 00, b - 01, c - 10, d - 11,则电文为:00010010101100,共 14 位,对方接收后,直接将其两位两位断开,即可获得原文。

但是否存在更优解?我们这样编码:

a - 0, b - 1, c - 10, d - 11,则电文为:0101010110,共 10 位,确实短了,却出现了一个致命的问题:由于编码方案不等长,对方并不能直接等长隔断;同时,a 的编码是 c, d 的前缀,字母之间存在歧义,比如如何告知对方,10 到底表示的是 c 还是 ba?所以,我们需要保证任何一个字符的编码都不是另一个编码的前缀,这种编码称作前缀编码。再来一次:

a - 0, b - 10, c - 110, d - 111,则电文为:0100110110111,共 13 位,没有任何理解问题,但是,这仍然并非最优解。

我们发现,在短短 7 个字母中,a 出现了 3 次,c 出现了 2 次,而 b, d 只有 1 次;我们如果希望总长度尽可能短,那么就应该使出现频率较高的采用较短的编码,频率较低的采用较长的编码,比如:

a - 0, b - 110, c - 10, d - 111,则电文为:011001010111,共 12 位,为最优解,即哈夫曼编码。

那么如何得到这样的哈夫曼编码?

假设现在有 n 个字符,第 i 个字符出现的频数为 w[i],将其作为字符的权值,再直接套用上面的哈夫曼树构建方式就行,因为哈夫曼树的构建过程,本质就是使权值较大的结点极可能离根结点近,构建完后只需要对每一个结点连接左右子树的两条边分别标上 0, 1,就能很轻松地得到每一个字符的哈夫曼编码了,比如:

和上述推断出的哈夫曼编码一致。

因为所有字符之间不存在直接的祖先关系,所以不存在一个字符的编码是另一个字符编码的前缀的情况。

4、译码

数字国高层收到了间谍发来的情报,是一串数字加一棵树。可惜数字国高层都是丈育,他们不知道该怎么处理这个数字电文。

还是上面的例子,电文为:011001010111。从第1位开始,根据编码是0还是1在树上进行搜索,如果是0则向左子树搜索,反之向右子树搜索,直到找到了叶子结点,即译得了一个字符,并退出本次查找;以此反复,直到遍历完成,即完成对该哈夫曼编码的译码过程。

5、代码

直接把实验报告里这个带注释的复制过来辣。

注意,这个代码仅适用于小写字母的编码,要更广的适用性还需进行修改。

 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 
 4 #define MAXN 55
 5 
 6 class Tree {
 7 public:
 8     int root, ls, rs, w; // 分别表示子树根结点序号,左儿子结点,右儿子结点,根结点权值
 9     Tree(int _root, int _ls, int _rs, int _w):
10         root(_root), ls(_ls), rs(_rs), w(_w) {}
11     Tree(): root(0), ls(0), rs(0), w(0) {}
12     void init(int);
13     friend bool operator < (Tree a, Tree b) {
14         return a.w > b.w; 
15     }
16 } t[MAXN];
17 
18 int n, tmp[MAXN];
19 int* code[MAXN];
20 char ch[10005];
21 priority_queue <Tree> Q; // 优先队列,用于每次找出根结点权值最小的两棵子树
22 
23 void Tree :: init(int o) { // 初始化结点,将结点放入优先队列
24     cin >> w, root = o;
25     Q.push(t[o]);
26 }
27 
28 void coding() { // 哈夫曼编码函数
29     for (int i = 1; i < n; i++) {
30         Tree t1 = Q.top(); Q.pop(); 
31         Tree t2 = Q.top(); Q.pop(); // t1, t2分别是根结点权值最小的两棵子树,从优先队列中弹出
32         t[i + n] = (Tree) {n + i, t1.root, t2.root, t1.w + t2.w}; 
33         Q.push(t[i + n]); // 将合并的新二叉树加入优先队列
34     }
35 }
36 
37 void dfs(int o, int d) { // 树的遍历函数
38     if (o <= n) { // 如果是叶子结点
39         code[o] = (int*) malloc((d + 1) * sizeof(int)); // 动态分配数组内存
40         for (int i = 1; i < d; i++)
41             code[o][i] = tmp[i]; // 将存储在tmp中的01序列复制到字符o的哈夫曼编码数组
42         code[o][d] = 2; // 结束标志,用于输出编码
43         return;
44     } 
45     tmp[d] = 0, dfs(t[o].ls, d + 1);
46     tmp[d] = 1, dfs(t[o].rs, d + 1);
47 }
48 
49 void getCode() { // 输出编码函数
50     dfs(2 * n - 1, 1); // 从根结点开始遍历
51     for (int i = 1; i <= n; i++) {
52         int o = 1;
53         cout << (char)(i + 'a' - 1) << ' ';
54         while (code[i][o] != 2) cout << code[i][o], o++; // 输出编码,直到出现结束标志
55         cout << endl;
56     }
57 }
58 
59 void translate() { // 译码函数
60     while (1) { 
61         cin >> ch;
62         int l = strlen(ch), o = 2 * n - 1;
63         for (int i = 0; i < l; i++) {
64             if (ch[i] == '0') o = t[o].ls; // 如果是0,向左儿子搜索
65             else o = t[o].rs; // 反之向右儿子搜索
66             if (o <= n) { // 如果是叶子结点
67                 cout << (char)(o + 'a' - 1);
68                 o = 2 * n - 1; // 重新从根结点开始搜索
69             }
70         }
71     }
72 }
73  
74 int main() {
75     cin >> n;
76     for (int i = 1; i <= n; i++) t[i].init(i);
77     coding();
78     getCode();
79     translate();
80     return 0;
81 } 
原文地址:https://www.cnblogs.com/jinkun113/p/12783075.html