哈夫曼编码/译码器、树形图打印
// 参考文章:
- https://github.com/luochana/HuffmanTree
- https://blog.csdn.net/Juheng_luo/article/details/119746998
以上是主要参考文章,感谢大佬的馈赠
//很多总结和解释是凑字数的
一、课题内容和要求
设计一个利用哈夫曼算法的编码和译码系统进行信息通讯
基本要求:
- 初始化:建立哈夫曼树
- 编码:利用建好的哈夫曼树生成哈夫曼编码;输出编码
- 译码:将哈夫曼编码进行译码并显示
- 设字符集及频度如下表:
字符 空格 A B C D E F G H I J K L M
频度 44 64 13 42 32 103 21 15 37 57 1 5 52 20
字符 N O P Q R S T U V W X Y Z
频度 57 77 15 1 84 51 80 23 8 18 10 16 1
首先,构造哈夫曼结点作为数据存储结构,如图1哈夫曼结点构造图:
图1 哈夫曼结点构造图
主要功能和函数,如图2所示
图2 主要功能以及函数
本设计实现了哈夫曼树的树形图动态生成,在本地的map<>中修改值,可以自适应地生成树形图。同时实现了哈夫曼树的明文加密和密文解码。
二、数据结构说明
哈夫曼结点类:
class HuffNode
{
public:
char value; //字符值
int freq; //单词出现频率
string huffcode; //编码串
HuffNode* left; //左结点指针
HuffNode* right; //右结点指针
int xPos; //在树形图中的输出坐标
};
排序类(哈夫曼树生成中的比较排序):
//排序函数
class cmpByFreq {
public:
cmpByFreq() {}
bool operator()(const HuffNode *node1, const HuffNode *node2)const {
return node1->freq > node2->freq;//根据字符频率最小值优先
}
};
三、算法设计
1、建立哈夫曼树函数:HuffNode* huffmanTree()
采用两个存储结构;
一为map<char, int>目的是将频度表直观地导入,这样做方便后续对本程序地改进,修改为文件输入输出等改动较小;
二为最小优先权队列priority_queue<HuffNode*, vector<HuffNode*>, cmpByFreq> min_queue,运用我们自己定义地排序方式对Map重新读入,这样做是不会对原来地map进行修改的,保证数据地完整和安全。
构建树的过程较为简单,从最小优先权队列中读入前两个结点,分别出队组成小树,再将小树的根节点压入队列中,依次循环,最终只剩两个结点时,直接将两个节点插在根节点上,即可组成一棵树。
[注1:]本函数中调用了编码函数,因此在树生成过程中,编码串的0和1时随着树的长高动态生成的
[注2:]存在一个全局map <char, HuffNode> huffmanWordMap,目的是在树生成过程中标记有初始值的结点,这样在明文加密过程中直接在这张表里对key进行搜索即可直接翻译(译码中没有用到)**
2、排序函数
这个函数是包含在排序类里面的,排序类只有一个空参数的构造函数。
排序函数使用了仿函数(functor),重载了运算符“()”(仿函数的性质要求),使得两个结点比较返回频度较低的那个。
设计目的是为了实现一个不需要额外占用内存,又能实现比较并能返回bool值的函数,同时因为不需要写原值,不会因为多个调用污染原内存。
[注3:] return node1->freq > node2->freq;这个语句看着是返回字符频度高的,但是根据官方写的使用手册,这里返回值代表第一个参数优先级是否小于第二个参数,也就是说,若返回是,说明第一个参数优先级低,返回否,说明第一个参数优先级高,所以这里还是频度低的优先。
3、生成哈夫曼编码函数
在生成树中被调用。原理很简单:
若拼接在左子树则字符串+’0’,反之则+’1’,最终生成树时,结点的编码已经生成完毕
4、中序遍历函数
此部分主要是为了生成各个结点的打印坐标以及判断树是否为空。
选择中序遍历的原因是:动态打印树形图时,因为结点的宽度,以及每个子树的深度都是不一样的,因此设计根节点的横坐标需要考虑到他的左子树,设计右子树的坐标需要考虑根节点和左子树的坐标,否则会使图形挤在一起,因此使用中序遍历的顺序最为符合上述的遍历流程。
5、打印函数
此部分代码较为繁琐,主要思路是:在左右子树到根节点的上方生成横线,同时在每个结点的上方生成竖线,指向目标结点(根节点除外)。
其中,横线生成时需要考虑到本身打印的字符串的长度,否则树会歪掉,于是用到p += printf(“(‘%c’)%d”,T->value,T->freq)语句,打印的同时将printf的打印长度返回给横线打印的字符串中,可以兼顾二者。
6、输出编码函数
打印所有有值的结点的哈夫曼编码,比较简单,直接读取,然后%4d这样规范格式打印即可。
7、加密函数
主要是测试译码函数时敲的太麻烦,于是加了一个,也很简单,直接在输入流的长度范围内循环搜索key读cout<<huffmanWordMap.find(str[i])->second->huffcode.c_str()即可。
8、译码函数
从根节点开始,读入待译码字符串,为0则向左树读,为1则向右树读,读到叶节点,则输出这个结点的值(这是哈夫曼树的特性),然后循环再从根节点继续读到字符串结尾。
这样设计的原因是只需要一个结点的指针,一个循环就可以读完,代码比较简洁,不需要另外占用存储空间。
四、详细设计
源代码:
#include<iostream>
#include<string>
#include<map>
#include<queue>
#include<string.h>
using namespace std;
class HuffNode
{
public:
char value; //字符值
int freq; //单词出现频率
string huffcode; //编码串
HuffNode* left;
HuffNode* right;
int xPos;
};
class cmpByFreq {
public:
cmpByFreq() {}
bool operator()(const HuffNode *node1, const HuffNode *node2)const {
return node1->freq > node2->freq; //根据字符频率最小值优先
}
};
//运算符重载函数定义到最小优先权队列的排序规则
//此处返回值意思是(c++手册中):返回值确定第一个参数优先级是否小于第二个参数,
//若返回是,说明第一个参数优先级低
//返回否,说明第一个参数优先级高
//生成Huffman 01编码串的函数
void makeHuffmanCode(HuffNode *node, string codestr)
{
node->huffcode += codestr;
if (node->left != NULL)
makeHuffmanCode(node->left, node->huffcode + "0");
if (node->right != NULL)
makeHuffmanCode(node->right, node->huffcode + "1");
}
map<char, HuffNode*> huffmanWordMap;
//生成树的函数
HuffNode* huffmanTree()
{
map<char, int> wordMap =
{ {' ',44},{'A',64},{'B',13},{'C',42},{'D',32},{'E',103},{'F',21},
{'G',15},{'H',37},{'I',57},{'J',1},{'K',5},{'L',52},{'M',20},
{'N',57},{'O',77},{'P',15},{'Q',1},{'R',84},{'S',51},{'T',80},
{'U',23},{'V',8},{'W',18},{'X',10},{'Y',16},{'Z',1}
};
//获取最小优先队列
map<char, int>::iterator iter;
priority_queue<HuffNode*, vector<HuffNode*>, cmpByFreq> min_queue; //频率低的优先
for (iter = wordMap.begin(); iter != wordMap.end(); iter++)
{
HuffNode *huffNode = new HuffNode();
huffNode->left = NULL; huffNode->right = NULL;
huffNode->value = iter->first; // iter遍历
huffNode->freq = iter->second;
min_queue.push(huffNode); //新结点入栈
huffmanWordMap[huffNode->value] = huffNode;
}
//构造哈夫曼树
HuffNode *node1, *node2, *root = NULL;
for (int i = 0; i < 26; i++) { //n-1次
HuffNode *newNode = new HuffNode();
if (min_queue.size() == 2) //为了避免每次循环都要判断,可以先循环处理n-2次,最后一次再单独处理
root = newNode;
node1 = min_queue.top(); min_queue.pop();
node2 = min_queue.top(); min_queue.pop();
newNode->left = node1;
newNode->right = node2;
newNode->freq = node1->freq + node2->freq;//权重之和
//cout << node1->freq << " " << node2->freq <<" "<<newNode->freq << endl;
min_queue.push(newNode);
}
makeHuffmanCode(root, ""); //生成各个字符的Huffman编码串
//printLeaf(root);
return root;
}
// 获取树的深度
/*
int huffDepth(HuffNode *T) {
if (T == NULL) return 0;
int depthLeft, depthRight;
depthLeft = huffDepth(T->left);
depthRight = huffDepth(T->right);
return 1 + (depthLeft > depthRight ? depthLeft : depthRight);
}
*/
int midWatch(HuffNode* T)
{
static int num = 0;
if (!T) return 0;
midWatch(T->left); //中序遍历循环到最左边
T->xPos = num;
num += to_string(T->freq).size()+1;
midWatch(T->right);
return num;
}
void Huffprint(HuffNode* T)
{
int sum = midWatch(T); //中序遍历
printf("\n\nTree:\n\n");
if (!sum) {printf("NULL");return;}
queue<HuffNode*> q;
if (T) q.push(T);
while (!q.empty()) {//层序遍历
int k = q.size(), p = 0;
queue<int> q1;
while (k--) { //一次打印一层的节点,k表示一层的节点
T = q.front(); //返回第一个
q.pop();
string str = "";
if (T->left) {
q.push(T->left);
while (p < T->left->xPos) { //在数值左边是空格
str += ' ';
++p;
}
q1.push(p);
while (p < T->xPos) { //在每一个数值上方生成横线延伸到父节点
str += '_';
++p;
}
}
while (p < T->xPos) { //如果没有左子树,直接全为空格
str += ' ';
++p;
}
printf("%s", str.c_str());
if(T->value) //输出格式
{
p += printf("('%c')%d",T->value,T->freq); //printf函数返回值是打印字符数
}
else
p += printf("%d", T->freq);
str.resize(0);
if (T->right) {
q.push(T->right);
while (p < T->right->xPos) {
str += '_';
++p;
}
printf("%s_", str.c_str());
q1.push(p++);
}
}
printf("\n");
//打印竖线
p = 0;
string str = "";
while (!q1.empty()) {
int tmp = q1.front();
q1.pop();
while (p < tmp) {
str += ' ';
++p;
}
str += '|';
++p;
}
printf("%s\n", str.c_str());
}
}
void Huffcode(HuffNode*T)
{
if (T) {
Huffcode(T->right); //访问右子树
Huffcode(T->left); //访问左子树
if(T->value)
{
printf("value= '%c' , freq= %4d , huffcode = %s \n",T->value,T->freq,T->huffcode.c_str());
}
}
}
void code(HuffNode *T,string str)
{
int i = 0; //字符串长度
while(i < str.size())
{
cout<<huffmanWordMap.find(str[i])->second->huffcode.c_str();
i++;
}
cout<<endl;
}
void uncode(HuffNode *T,string str)
{
int i = 0; //字符串长度
while(i < str.size())
{
HuffNode *p = T; //查找字符串
while(p->left&&p->right)
{
if(str[i] == '0')
{
p = p -> left;
}
else
{
p = p -> right;
}
i++;
}
//cout<<p->value<<endl;
printf("%c",p->value); //到叶节点则为结束
}
}
int main()
{
HuffNode *p = huffmanTree();
Huffprint(p);
printf("\n");
Huffcode(p);
cout<<"\n请输入加密代码\n"<<endl;
string zipStr;
getline(cin,zipStr,'\n');
cout<<"\n加密结果为:\n"<<endl;
code(p,zipStr);
printf("\n加密结束:\n");
cout<<"\n请输入翻译代码\n"<<endl;
string curStr;
cin.sync();
cin >> curStr;
cout<<"\n翻译结果为:"<<endl;
uncode(p,curStr);
cout<<"\n翻译结束:\n"<<endl;
cin>>skipws; //恢复默认状态。
return 0;
}
五、测试数据及其结果分析
第一组:根据给定的字符频度表生成的树形图和哈夫曼编码
图3 树形图
编码:
value= ‘R’ , freq= 84 , huffcode = 1111
value= ‘T’ , freq= 80 , huffcode = 1110
value= ‘O’ , freq= 77 , huffcode = 1101
value= ‘M’ , freq= 20 , huffcode = 110011
value= ‘W’ , freq= 18 , huffcode = 110010
value= ‘H’ , freq= 37 , huffcode = 11000
value= ‘A’ , freq= 64 , huffcode = 1011
value= ‘D’ , freq= 32 , huffcode = 10101
value= ‘K’ , freq= 5 , huffcode = 10100111
value= ‘Z’ , freq= 1 , huffcode = 1010011011
value= ‘J’ , freq= 1 , huffcode = 1010011010
value= ‘Q’ , freq= 1 , huffcode = 101001100
value= ‘V’ , freq= 8 , huffcode = 1010010
value= ‘Y’ , freq= 16 , huffcode = 101000
value= ‘I’ , freq= 57 , huffcode = 1001
value= ‘N’ , freq= 57 , huffcode = 1000
value= ‘P’ , freq= 15 , huffcode = 011111
value= ‘G’ , freq= 15 , huffcode = 011110
value= ‘B’ , freq= 13 , huffcode = 011101
value= ‘X’ , freq= 10 , huffcode = 011100
value= ‘L’ , freq= 52 , huffcode = 0110
value= ‘E’ , freq= 103 , huffcode = 010
value= ‘S’ , freq= 51 , huffcode = 0011
value= ‘U’ , freq= 23 , huffcode = 00101
value= ‘F’ , freq= 21 , huffcode = 00100
value= ’ ’ , freq= 44 , huffcode = 0001
value= ‘C’ , freq= 42 , huffcode = 0000
第二组:进行加密
输入:I LOVE CHINA AND I LOVE THE WORLD
输出:如图4所示:
图4 加密明文
加密结果为:
100100010110110110100100100001000011000100110001011000110111000101010001100100010110110110100100100001111011000010000111001011011111011010101
第三组:译码,如图5所示
输入密码即为上面的加密结果:
图5 第三组测试截图
翻译结果为:
I LOVE CHINA AND I LOVE THE WORLD
翻译结束:
六、算法设计和程序调试过程中的问题
- 问题1: 定义排序函数时一开始采用的常规的数组排序,发现不仅需要生成一个数组,需要写很麻烦的比较函数,而且定义一个整型的数组和哈夫曼结点类很不契合,需要读出,然后还要重新写入顺序。
解决方法:发现使用最小优先权队列自定义一个比较函数就可以很好的解决这个问题,重载一个适用于自定义类的符号,简洁易懂且不会因为重复调用污染内存。 - 问题2:写重载运算符时一直搞反返回的优先级,但是语句逻辑上都是对的
解决方法:检查发现,返回值 return node1->freq > node2->freq有问题,查询了c++手册,官方说明是采用负排序比较,也就是说返回1,说明第一个优先级低,返回0,则第一个优先级低。只能说这个定义和正常的逻辑不太一样。 - 问题3:如何把字符串的生成写的更加简洁,例如哈夫曼编码的动态生成过程
解决方法:只需要用加减符号在空字符串中修改即可 - 问题4:译码的过程开始写的很繁琐,直接查询表让人感受不到思考
解决方法: 从根节点开始,读入待译码字符串,为0则向左树读,为1则向右树读,读到叶节点,则输出这个结点的值(这是哈夫曼树的特性),然后循环再从根节点继续读到字符串结尾。这样设计的原因是只需要一个结点的指针,一个循环就可以读完,代码比较简洁,不需要另外占用存储空间。 - 问题5:如何建立一个直观的容器读入字符频度表,而对结点的排序又要方便且不能改变原来的表
解决方法: 用map直接写入,但处理使用最小优先权队列来调整,这样输入和输出是分开的,而且调整顺序比只用一个容器要方便 - 问题6:怎么让打印的树形图不歪
(1)不在同一个根节点的结点也有可能会互相挤开
(2)字符串长度是变化的,因为我让输出有初始值的结点时,把原字符也打印出来,这样容易把值打印到其他树上去
解决方法: (1)用中序遍历,因为打印是从左到右,对应树也是从左子树到父节点到右子树,这样打印坐标就能左右兼顾,并且往根节点返回时,上下的宽度也能兼顾到。
(2)使用p += printf(“(‘%c’)%d”,T->value,T->freq)语句,打印的同时将printf的打印长度返回给横线打印的字符串中,不需要重新写一个字符串,再统计长度,简洁实用 - 问题7:cout输出流自动换行,cin输入流输入一次就用不了
解决方法:cout<<endl中endl就是换行,只要把输出插在循环里不加endl,在循环结束再加上endl就可以让输出过程中不换行。
Cin还是缓冲区的问题,在问题1里面也遇到了,但是当时是用的cin.clear(),这里间歇性有用,为了搞定抽风问题又查了一下,发现cin.clear()是清除缓冲区错误数据,于是改成了cin.sync()重置了输入流的缓冲区,从此抽风不再。
七、课程设计总结
本次实验的第二题哈夫曼编码/译码就算完成了,这道题整体的思路不算难,但是实操过程中还是暴露了我代码量小的问题,有些细节确实是体现在实践中的。
程序的升级可以有几个角度,一方面哈夫曼的编码/译码可以改进为真正的编码译码器,虽然这个程序里的写法比较简单,也可以实现文件的编码压缩,用文件流进行编码,同时也不使用现有的字符频度表,而是根据文件编码获得,这些的实现势必需要更大的代码量,也对文件流的理解有更高的要求。
另一方面,我本来是想实现用GUI来展现哈夫曼树的动态创建的,于是实验周现场开始学Qt。思路也很简单,创建一个哈夫曼结点,就创建一个新标签插入到窗口中上一个节点的相对位置,这样不仅不用考虑打印树形图的坐标问题,而且可以像一般的演示图一样实现,有初始值的结点用圆圈,没有初始值的结点用方框。最后因为Qt信号与槽有点绕加上补课,没来得及实现,实验结束我还会再试一试。以前学过用java自带的包写了一个连接mysql的gui程序,然而那个实在太低级且麻烦了,这次学Qt让我感受到这个Qt creator的神奇之处,居然能直接鼠标拖窗口和标签,让本渣大开眼界!
总结一下,这个程序中学到的东西:
我对map容器和iter遍历器有了更深一步的理解,包括first和second这些的指向;最小优先权队列priority_queue我还是第一次用,这个是参考了网上大佬的代码,也是第一次实战用仿函数、运算符重载,实在是让我认识到代码的艺术性以及我和大佬水平的差距,那个负排序定义的重载返回值实在是让我纠结了很久,看到那个英文解释最后才看懂了;译码的函数本来写了一个直接暴力搜索表的,但是答辩的时候老师问我思路的时候我说直接搜索,老师说就这样嘛,于是我重新想了一个从树根节点开始译码的函数,还好不是很长,一会就改完了;打印也是参考了一些资料,中序遍历看着很简单但是确实很巧妙一开始没想到,但是打印的时候我想输出特殊字符,就是字符映射表里面的粗线,这样打印出来会很好看,然而我的vscode搞了半天也输出不了,不管是编码gb2312\utf-8\iso8899都没啥用,修改缓冲区标志也没成功,遂作罢。
总的来说,实验周的代码量不大,思路也不难,但是属实给我打回原形了,纯纯的新手水平,借此机会我重新温习了一遍c++的封装继承多态特性,后续我也会继续学习Qt框架的,未来更要加强代码量的训练!