1. 基本知识
哈夫曼树 (Huffman Tree),又称最优二叉树,是 带权路径长度最短 的树
路径和路径长度:
- 在一颗树中,从一个节点往下可以达到的子孙节点之间的通路,称为路径
- 通路中分支的数目称为路径长度
- 若规定根节点的深度为 1,则从根节点到第 L 层节点的路径长度为 L-1
图例:
如图中二叉树,根节点 与 红色节点的路径长度为 3
节点的带权路径长度和树的带权路径长度:
- 节点的带权路径长度,为从根结点到该结点之间的路径长度与该结点的权值的乘积
- 树的带权路径长度 (WPL),为所有叶子节点的带权路径长度之和
图例:
如图中二叉树,红色节点的带权路径长度为 12,树的带权路径长度 (WPL) 为4*3+2*2+3*1=19
哈夫曼编码是一种压缩编码的编码算法,是基于哈夫曼树的一种编码方式
那么哈夫曼编码跟 ASCII 编码有什么区别?ASCII 编码是对照 ASCII 表进行的编码,每一个字符符号都有对应的编码,其编码长度是固定的。而哈夫曼编码对于不同出现频率的字符,其使用的编码是不一样的,会对频率较高的字符使用较短的编码,频率低的字符使用较长的编码,这样保证总体使用的编码长度会更少,从而实现到了数据压缩的目的
那么如何将哈夫曼树与哈夫曼编码对应起来?
从根节点开始,向左走记为加上 '0' (可以更改为别的字符),向右走记为加上 '1' (可以更改为别的字符),走到叶子节点处记录下当前编码即为该叶子节点的哈夫曼编码
2. 初始化哈夫曼树
一般哈夫曼树的一个节点有如下属性:
- 当前节点的下标
- 左孩子的下标
- 右孩子的下标
- 当前节点的权值
- 当前节点代表的字符
于是可以得到节点的代码:
struct Node {
int self; // 当前节点的下标
int lhs; // 左孩子的下标
int rhs; // 右孩子的下标
int weig; // 当前节点的权值
char c; // 当前节点代表的字符
} HT[60];
3. 构建哈夫曼树
对于给定的字符串种类,及每种字符串的出现次数,大致建树思路如下:
- 将每个字符串的出现次数看做许多一一对应的根节点,即一个森林
- 每次从森林中选出 两个根节点权值最小 的树,作为一个新建节点的左右子树,且这个新建节点的权值等于其左、右子树根节点之和
- 从森林中删除选取的两棵树,再将新树加入森林
- 重复执行 1.2.3. 操作,直到森林中只省下一棵树,该树即为所求的哈夫曼树
图例:
对于以下给出的字符串种类,及每种字符串的出现次数,构建一棵哈夫曼树
a | b | c | d |
5 | 3 | 4 | 1 |
1. 将其看作有四棵树的森林(每棵树中都只有根节点)
2. 选出两棵根节点权值最小的树,作为一个新建节点的左右子树,并删除这两棵树,加入新树
3. 重复 1.2. 操作,直到森林中只剩下一棵树
4. 此时,这棵树即为所求的哈夫曼树,例如记向左走为加上 '0',向右走为加上 '1',此时各叶子节点的编码为
a(5) | b(3) | c(4) | d(1) |
0 | 110 | 10 | 111 |
于是可以得到构建哈夫曼树的代码:
// 对于给定字符串 (只包括小写字母),构建哈夫曼树
#include <bits/stdc++.h>
using namespace std;
int n, m; // 字符串的长度,共有多少个小写字母
int frequent[30] = {0}; // frequent[i] 指第 i 个小写字母的出现次数
string s, kkk = "abcdefghijklmnopqrstuvwxyz";
struct Node { // 初始化哈夫曼树
int self;
int lhs;
int rhs;
int weig;
char c;
} HT[60];
bool operator > (Node node1, Node node2) { // 重载运算符,定义优先队列中结构体的比较逻辑
return node1.weig > node2.weig;
}
priority_queue<Node, vector<Node>, greater<Node> > q; // 使用优先队列模拟,因为其可以自动排序
int main() {
scanf("%d%d", &n, &m);
cin >> s;
for (int i = 0; i < s.size(); i++) {
frequent[s[i] - 'a']++; // 统计单个小写字母的出现频率
}
for (int i = 0; i < m; i++) {
HT[i] = Node{i, -1, -1, frequent[i], kkk[i]}; // 初始化,构建只有根节点的树
q.push(HT[i]); // 入队
}
int pos = m + 1;
while (q.size() > 1) { // 注意不是 !q.empty(),因为队列中只有一棵树时就应停止循环
Node tmp1 = q.top(); // 找到两棵根节点权值最小的树
q.pop(); // 出队
Node tmp2 = q.top();
q.pop(); // 出队
HT[pos] = Node{pos, tmp1.self, tmp2.self, tmp1.weig + tmp2.weig, '*'}; // 构建新节点
q.push(HT[pos]); // 入队
pos++;
}
return 0;
}
4. 获取哈夫曼编码
具体获取方法其实已经在 基本知识 中介绍过了,这里直接放代码,具体可以看注释
map <char, string> s_code; // 定义一个 map 来存小写字母对应的编码
void dfs(int pos, string code) { // 当前节点的位置,当前的编码
if (HT[pos].lhs == -1 && HT[pos].rhs == -1) { // 如果当前节点是叶子节点
s_code[HT[pos].c] = code; // 直接记录编码
return;
}
dfs(HT[pos].lhs, code + "0"); // 向左子树搜索
dfs(HT[pos].rhs, code + "1"); // 向右子树搜索
}
创作不易,各位看官若觉得有用就点个赞吧~