zoukankan      html  css  js  c++  java
  • [知识点] 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 } 
  • 相关阅读:
    【Qt】Qt之自定义界面(QMessageBox)【转】
    【WinForm】线程中向listview添加数据
    【Qt】Qt之自定义界面(窗体缩放-跨平台终极版)【转】
    【Qt】Qt之自定义界面(窗体缩放)【转】
    【Qt】Qt之自定义界面(添加自定义标题栏)【转】
    【Qt】Qt之自定义界面(实现无边框、可移动)【转】
    【Qt】QWidget、QDialog、QMainWindow的异同点【转】
    【Qt】QSettings读写注册表、配置文件【转】
    【Qt】QSettings介绍【转】
    【Qt】Qt之启动外部程序【转】
  • 原文地址:https://www.cnblogs.com/jinkun113/p/12783075.html
Copyright © 2011-2022 走看看