目录
[1. 信息论基础](#1. 信息论基础)
[2. 核心概念](#2. 核心概念)
1.算法核心思想
哈夫曼编码是一种变长编码算法,由David A. Huffman于1952年提出。其核心思想是:
为出现频率高的字符分配较短的编码,为出现频率低的字符分配较长的编码,从而减少整体编码长度
2.算法原理
1. 信息论基础
熵编码:根据信息出现的概率进行编码
最优前缀码:没有任何一个编码是另一个编码的前缀,确保解码唯一性
2. 核心概念
字符频率:每个字符在文本中出现的次数
权重:节点的重要性,通常使用字符频率
路径长度:从根节点到叶节点的边数
3.算法步骤
步骤1:统计字符频率
输入:字符串 "aaaaaaabbbbbccdddd" 统计结果: a: 7次 b: 5次 c: 2次 d: 4次步骤2:构建哈夫曼树
构建过程:
将每个字符看作一个独立的树,权重为其频率
选择权重最小的两棵树合并,新节点权重为两者之和
重复直到只剩一棵树
示例构建:
text
初始:[(a,7), (b,5), (c,2), (d,4)] 第1步:合并c(2)和d(4) → 新节点(6) 第2步:合并b(5)和新节点(6) → 新节点(11) 第3步:合并a(7)和新节点(11) → 根节点(18)步骤3:生成哈夫曼编码
编码规则:
左分支标记为
0右分支标记为
1从根到叶子的路径即为该字符的编码
示例编码:
text
a: 0 (最短,频率最高) b: 10 (次短) d: 111 (较长) c: 110 (最长,频率最低)步骤4:数据压缩与解压
压缩 :将原文字符替换为对应的哈夫曼编码
解压:根据哈夫曼树从根开始,按位遍历直到叶子节点
4.实现

4.1头文件包含与函数声明
#include<iostream>
#include<string>
#include<vector>
#include<unordered_map>
#include<queue>
#include<climits>
#include<algorithm>
using namespace std;
//结点使用三叉链,便于后续找双亲和孩子
struct treeNode
{
int _weight;//权重
int _lchild, _rchild, _parent;//左右孩子双亲结点
//结点初始化
treeNode()
{
_weight = _lchild = _rchild = _parent = 0;
}
};
//字符对应信息
struct chInfo
{
char _ch;//字符
int _freq;//频率
chInfo()
{
_ch = '\0';
_freq = 0;
}
chInfo(char ch, int freq)
{
_ch = ch;
_freq = freq;
}
};
void select(vector<treeNode>& HuffmanTree, int n, int& node1, int& node2);
//构建哈夫曼树 节点数组 叶子结点个数
void createHuffmanTree(vector<treeNode>& HuffmanTree, int n);
//构建哈夫曼码 每个字符对应一个编码 有效字符个数
void createHuffmanCode(vector<treeNode>& HuffmanTree, vector<string>& HuffmanCode, int n);
//译码
string deHuffmanCode(string& str, vector<treeNode>& HuffmanTree, vector<chInfo>& chs, int n);
(1)结点使用三叉链,存放父亲、左孩子、右孩子的下标以及自身的权重
后续结点下标从1开始,令各变量初始值为0
(2)构建一个类,记录字符及字符出现的频率(权重)
题目以小写字符为主,令各变量初始值为0
(3)声明相关函数
4.2整体框架
int main()
{
string str;
cin >> str;
if (str.size() == 0 || str.size() == 1) return 0;//空串、单字符不参与后续
//哈希表统计字符频率
int hash[26] = { 0 };
for (auto ch : str)
hash[ch - 'a']++;
//字符类对象数组
vector<chInfo> chs(1);//有效字符的下标从1开始,与后续对应
unordered_map<char, int> chMap;
int num = 1;
for (int i = 0; i < 26; ++i)//下标从小到大,对应ASCII顺序
{
if (hash[i] > 0)
{
chs.push_back(chInfo('a' + i, hash[i]));
chMap['a' + i] = num++;
}
}
//第一行输出频率
int chs_size = chs.size()-1;
for (int i = 1; i < chs_size; ++i)
{
printf("%c:%d ", chs[i]._ch, chs[i]._freq);
}
printf("%c:%d\n", chs[chs_size]._ch, chs[chs_size]._freq);
//存储结点的数组 叶子节点由chs_size个,总结点由2*chs_size-1个,下标从1开始计数
//自动调用结点的构造函数,初始时,三叉链、权重均为0
vector<treeNode> HuffmanTree(2 * chs_size);
//初始化叶子结点权重,叶子节点的下标为1~chs_size
for (int i = 1; i <= chs_size; ++i)
{
HuffmanTree[i]._weight = chs[i]._freq;
}
//构建哈夫曼树
createHuffmanTree(HuffmanTree, chs_size);
//打印结构 2n至2n+2行
for (int i = 1; i <= 2 * chs_size - 1; ++i)
{
cout << i << ' ' << HuffmanTree[i]._weight << ' '
<< HuffmanTree[i]._parent << ' '
<< HuffmanTree[i]._lchild << ' ' << HuffmanTree[i]._rchild << endl;
}
//构建哈夫曼码,共有chs_size个字符
vector<string> HuffmanCode(chs_size + 1);
createHuffmanCode(HuffmanTree, HuffmanCode, chs_size);
//打印各字符对应编码
for (int i = 1; i < chs_size; ++i)
{
printf("%c:%s ", chs[i]._ch, HuffmanCode[i].c_str());
}
printf("%c:%s\n", chs[chs_size]._ch, HuffmanCode[chs_size].c_str());
//打印字符串编码
string tmpstr;
for (auto ch : str)
{
tmpstr += HuffmanCode[chMap[ch]];
}
cout << tmpstr << endl;
//译码并打印
cout << deHuffmanCode(tmpstr, HuffmanTree, chs, chs_size) << endl;
return 0;
}
(1)输入小写字符串,空串、单字符不参与后续
(2)哈希表统计字符频率
字符、下标、频率三个变量后续常用,下标可用数组处理,将字符、频率封装为一个"字符类",那么数组中存储字符类对象,通过下标就可以找到对应字符类对象及其频率,再创建一个哈希表,建立字符与下标间的映射关系,那么通过字符就可以找到下标,进而找到频率
(3)字符类对象数组、后续的节点数组,哈夫曼码数组的对应下标一致
(4)chs_size是叶子结点个数,后续经常用到
(1)叶子结点有chs_size个,则哈夫曼树总结点数为2*chs_size-1个(2)节点数确定,使用vectoc存储每个结点,注意:结点下标从1开始
(3)叶子节点的下标为1~n,构建出来的节点下标为n+1~2*n-1
(1)叶子结点有chs_size个,哈夫曼码就有chs_size个,注意:结点下标从1开始
4.3构建哈夫曼树
void createHuffmanTree(vector<treeNode>& HuffmanTree, int n)
{
for (int i = n + 1; i <= 2 * n - 1; ++i)
{
//数组中选取两个未被使用的结点
int node1 = 0, node2 = 0;
select(HuffmanTree, n, node1, node2);
//新节点只不修改父亲
HuffmanTree[i]._weight = HuffmanTree[node1]._weight + HuffmanTree[node2]._weight;
HuffmanTree[i]._lchild = node1;
HuffmanTree[i]._rchild = node2;
//叶子节点修改父亲
HuffmanTree[node1]._parent = i;
HuffmanTree[node2]._parent = i;
}
}
(1)哈夫曼树的构建过程就是每次从父亲节点为0的结点中,挑选出权重最小的两个结点组成一个新的节点(父亲节点为0)
(2)新节点的权重等于挑选出来的两节点权重之和,新节点的左右孩子为挑选出来的两个结点(顺序可交换),新节点的父亲不变,仍为0,挑选出来的两个结点的父亲节点就是他们组成的新节点
void select(vector<treeNode>& HuffmanTree, int n, int& node1, int& node2)
{
//直接查找
int min1 = INT_MAX, min2 = INT_MAX;
for (int i = 1; i <= 2 * n - 1; ++i)
{
if (HuffmanTree[i]._weight == 0)
break;
if (HuffmanTree[i]._parent == 0 && HuffmanTree[i]._weight < min1)
{
min2 = min1;
node2 = node1;
min1 = HuffmanTree[i]._weight;
node1 = i;
}
else if (HuffmanTree[i]._parent == 0 && HuffmanTree[i]._weight < min2)
{
min2 = HuffmanTree[i]._weight;
node2 = i;
}
}
}
在当前数组中挑选出父亲结点为0,且权重最小的两个结点:
node1是父亲结点为0,权重最小的结点,node1是父亲结点为0,权重次小的结点
(1)令min1 为当前查找范围的最小权重 ,min2 为当前查找范围的次小权重
遍历查找过程中:
(2)当前结点的权重为0,意味着遍历到了当前数组的最后一个结点的下一个结点
(3)如果当前结点的父亲结点为0且权重小于min1,更新min1,node1
如果当前结点的父亲结点为0且权重大于min1,小于min2,更新min2,node2
4.4生成哈夫曼码
void createHuffmanCode(vector<treeNode>& HuffmanTree, vector<string>& HuffmanCode, int n)
{
//自底向上遍历每个字符的祖先,所以string是相反的
for (int i = 1; i <= n; ++i)
{
int cur = i, parent = HuffmanTree[i]._parent;
while (parent != 0)
{
if (HuffmanTree[parent]._lchild == cur) HuffmanCode[i] += '0';
else HuffmanCode[i] += '1';
cur = parent;
parent = HuffmanTree[parent]._parent;
}
reverse(HuffmanCode[i].begin(), HuffmanCode[i].end());
}
}
(1)因为左分支标记为
0,右分支标记为1,从根到叶子的路径即为该字符的编码且已知叶子节点的下标为1~n,那么我们可以选择从叶子结点向上遍历到根节点,记录遍历的路径,最后再翻转字符串
4.5数据压缩与解压
string tmpstr;
for (auto ch : str)
{
tmpstr += HuffmanCode[chMap[ch] + 1];
}
压缩:遍历所输入的字符串,先根据字符找到下标,再根据下标找到对应字符的哈夫曼码,然后把哈夫曼码添加到输出字符串中
string deHuffmanCode(string& str, vector<treeNode>& HuffmanTree, vector<chInfo>& chs, int n)
{
int len = str.size();
string tmp;
int cur = 2 * n - 1;
for (int i = 0; i < len;)//遍历字符串
{
//不是叶子节点,字符串未越界
while (HuffmanTree[cur]._lchild != 0 && i < len)
{
if (str[i] == '0') cur = HuffmanTree[cur]._lchild;
else cur = HuffmanTree[cur]._rchild;
++i;
}
//字符串正确的话一定不会越界
tmp += chs[cur]._ch;
cur = 2 * n - 1;
}
return tmp;
}
遍历所压缩的字符串并从哈夫曼树的根节点开始移动,字符零到左子树,字符1到右子树,直到叶子节点,解压出一个字符后,再重复上述步骤