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 } 
  • 相关阅读:
    The Quad
    将OrCAD Capture CIS的设计文件(.dsn)导入到PADS Logic VX.2.3
    OrCAD Capture CIS 16.6 将版本16.6的设计文件另存为版本16.2的设计文件
    Eclipse IDE 添加jar包到Java工程中
    PADS Logic VX.2.3 修改软件界面语言
    切换Allegro PCB Editor
    Allegro PCB Design GXL (legacy) 将brd文件另存为低版本文件
    Allegro PCB Design GXL (legacy) 设置自动保存brd文件
    Could not create an acl object: Role '16'
    windows 下apache开启FastCGI
  • 原文地址:https://www.cnblogs.com/jinkun113/p/12783075.html
Copyright © 2011-2022 走看看