换一种思路的霍夫曼编码(基于MFC)
前言:
[首先]因为对网上搜索的代码我其实看得不是很有耐心(本人问题),所以用了自己的思路来写了霍夫曼编码,主要关键词: stl, 入栈出栈。
[目的]对简单的字符数据进行霍夫曼编码,从文本框或本地文件地址中读入文本,输出每个字符的霍夫曼编码。
搭建简易的MFC对话框
在vs studio中创建一个默认的MFC应用(记得选择对话框应用),通过视图打开工具箱,然后简单的拖拽对齐打造一个简单的界面。
可视化操作的逻辑设计
1.双击如图1所示中的‘运行’按钮(具体就是你自己设计的按钮),跳转到了cpp源文件的OnClickButton1()函数中。
2.接下来就是要读入文本框中的内容进行编码,先来说读入:
int state1 = 0,state2=0;
//定义两个状态,区分文本框文本读入和地址读入
void CHuffmanDlg::OnBnClickedButton1()
{
CString content; //MFC应用中的所有可视化的输出和输入都是以CString存储的
//这里定义一个content用来接收文本框读入的内容
GetDlgItem(IDC_EDIT2)->GetWindowText(content);
//调用一个接收函数,参数IDC_EDIT2是你需要获取文本的文本框的ID,content是用来接收的变量
std::string temp = CT2A(content.GetBuffer());
//这里定义一个string类型,然后将CString类型转换为String,方便计算和操作
if (state2 == 1) {//如果是地址读入
std::ifstream infile;
infile.open(temp,std::ios::in);
if (!infile.is_open())
{
std::cout << "读取文件失败" << std::endl;
return;
}//通过ifstream中的open()函数打开本地文件
char buf[1024];
temp.clear();
//创建一个字符数组用来存储输入流中读取到的字符
//记得用完之后将读入的地址temp变量清空,下面要用来存储读入到的字符,省下一个变量空间
while (infile.getline(buf, sizeof(buf)))
{
std::cout << buf << std::endl;
temp = buf;
}
//将buf读到的字符存储到temp中
}
}
还记得第一行定义的state1和state2吗,就是用来记录用户点击的是字符串还是本地文件地址(图一所示),接下来双击图一中的转态按钮创建两个函数:
void CHuffmanDlg::OnBnClickedRadio1()
{
// TODO: 在此添加控件通知处理程序代码
state1 = 1;
state2 = 0;
}
void CHuffmanDlg::OnBnClickedRadio2()
{
// TODO: 在此添加控件通知处理程序代码
state2 = 1;
state1 = 0;
}
对字符进行统计和排序
std::map<char, float> dic;//创建字典<出现的字符,字符出现的次数>
std::map<char, float> ::iterator it;//创建字典迭代器
int flg = 0;//标志重复项
//对得到的字符串temp遍历
//对字典遍历
//如果字符在字典中第一次出现,标志为0且在字典中插入新的数据,反之标志为1,并且对应该字符的次数+1
for (int i = 0; i <= temp.size(); i++) {
for (it = dic.begin(); it != dic.end(); it++){
if (temp[i] != (it->first)) {
flg = 0;
}
else{
flg = 1;
it->second += 1;
break;
}
}
if (flg == 0) {
dic.insert(std::pair<char, float>(temp[i], 1)); //插入新的种类
}
}
char name = { };
dic.erase(name);
//这一步是为了删除创建字典时产生的第一个空的数据,不利于我们后面对字典的遍历操作
//将字符出现的次数转换为概率
int sum = 0, count = 0;
for (it = dic.begin(); it != dic.end(); it++) {
count += 1;
sum = it->second + sum;
}
for (it = dic.begin(); it != dic.end(); it++) {
it->second = (it->second) / sum;
}
//计算数据的冗余量
std::vector<std::pair<char, float>> vec;//创建容器方便对数据进行计算
float R = 0, H0 = log(count) / log(2), H = 0;//计算冗余量
for (it = dic.begin(); it != dic.end(); it++) {
H = H + (it->second) * (log(it->second) / log(2));
//vec中的first与second分别对应字典dic中的char类型和float类型中的数据
vec.push_back(std::pair<char, float>(it->first, it->second));
}
R = H0 + H;
//R就是我们要计算的冗余量
//最后我们要将数据由小到大进行排序,方便我们后面对数据创建霍夫曼树
sort(vec.begin(), vec.end(),cmp);
//容器的好处就在这里体现出来了
这里有一个小小的细节需要注意,sort()函数默认调用的cmp函数是由大到小排序的,所以我们要在源文件中找到cmp()函数进行修改:
bool cmp(std::pair<char, float> a, std::pair<char, float> b) {
return a.second < b.second;//这里的<或者>就用来控制降序还是升序排序的
}
霍夫曼树
现在我们在跳转到xxx.h的头文件中(xxx为你的工程名字),定义出我们的霍夫曼树和对应的函数:
struct HuffmanNode {
char chara; //字符
float weight; //概率,也就是权值
HuffmanNode* lchild, * rchild; //对应的左子树和右子树
};
关于霍夫曼树的基本原理这里就不赘述了,很多文章都有介绍,这里我只做结构和操作的解释:
接下来我们写一下有关霍夫曼树的一系列操作的函数:
1.创建节点
static HuffmanNode* creatnode(char fchara, float fweight) {
HuffmanNode* node = (HuffmanNode*)malloc(sizeof(HuffmanNode));
//分配一个节点空间
node->chara = fchara;
node->weight = fweight;
node->lchild = NULL;
node->rchild = NULL;
return node;
}
2.将两个节点连接成一个新的节点(由两个子节点合成一个父节点)
static HuffmanNode* linknode(HuffmanNode* x1, HuffmanNode* x2) {
HuffmanNode* node = (HuffmanNode*)malloc(sizeof(HuffmanNode));
node->chara = 'l'; //将合成之后的节点字符名命名为l方便后面的操作
node->weight = x1->weight + x2->weight;
node->lchild = x1;
node->rchild = x2;
return node;
}
这里用一个图解释一下:
3.比较三个节点两两组合的权值总和大小
static bool cmpnode(HuffmanNode* x1, HuffmanNode* x2, HuffmanNode* x3) {
float sum1 = x1->weight + x2->weight;
float sum2 = x2->weight + x3->weight;
if (sum1 <= sum2) {
return 1;
}
else return 0;
}
这个函数我们稍后用到的时候再详细讲一下,先把它写到我们的头文件中
4.输出霍夫曼树的所有结点(不包括合成节点)
static void show(HuffmanNode* root) {
if (root) {
if (root->chara == 'l') {
show(root->lchild);
show(root->rchild);
}
else {
std::cout << root->chara<<std::endl;
}
}
}
5.查找某个字符的霍夫曼编码(最关键的函数)
static int flag = 0;//设置一个状态用来判断是否查找到我们需要编码的字符
static std::vector<char> search(HuffmanNode* root,char target,std::vector<char> code) {
if (root) {
if (root->chara == target) {
flag = 1;
return code;
}
else {
if(flag==0)code.push_back('0');
if (flag == 0)code=search(root->lchild, target, code);
if (flag == 0)code.push_back('1');
if (flag == 0)code=search(root->rchild, target, code);
if (flag == 0)code.pop_back();
}
}
else {
code.pop_back();
return code;
}
return code;
基本思路:在查找的过程中,进入左子树我们在code容器中压入0,进入右子树我们压入1,若没有找到且返回上层函数则将我们进入左右子树入栈的0和1都pop掉。这里我们后面举个例子讲解会比较清晰。
6.重置状态
static void change_flag() {
flag = 0;
}
举个例子
这里我引用了一组数据:
BABACACADDAABBCBBAEBEDDABBBEEE
对应我们产生的字典:
B:10/30
A:8/30
C:3/30
D:4/30
E:5/30
转换为容器进行排序:
B:10/30
A:8/30
E:5/30
D:4/30
C:3/30
对应排序后的容器我们创建出一个霍夫曼树:
我们用flg来记录树是否满足有两个节点的情况。
第一轮循环结束。
到这里循环结束,我们的霍夫曼树也创建好了,所有的节点都保存在root节点中。
源码如下:
int flag=0;
HuffmanNode*root=(HuffmanNode*)malloc(sizeof(HuffmanNode));
HuffmanNode*root2 = (HuffmanNode*)malloc(sizeof(HuffmanNode));
if (vec.size() > 2) {
for (std::vector<std::pair<char, float>>::iterator ii = vec.begin(); ii != vec.end();) {
//如果是第一次创建
if (flag == 0) {
HuffmanNode* n1 = creatnode(ii->first, ii->second);
ii++;
if (ii != vec.end()) {
HuffmanNode* n2 = creatnode(ii->first, ii->second);
HuffmanNode* link = linknode(n1, n2);
root = link;
ii++;
flag = 1;
}
}
//满足树中有两个节点后
if (flag == 1) {
HuffmanNode* n1 = creatnode(ii->first, ii->second);
ii++;
if (ii != vec.end()) {
HuffmanNode* n2 = creatnode(ii->first, ii->second);
if (cmpnode(root, n1, n2)) {
if (root->weight > n1->weight) {
root = linknode(n1, root);
}
else { root = linknode(root, n1); }
}
else {
HuffmanNode* link = linknode(n1, n2);
if (root->weight > link->weight) {
root = linknode(link, root);
}
else { root = linknode(root, link); }
ii++;
}
}
else {
root = linknode(root, n1);
}
}
}
}
如果需要创建的霍夫曼树节点数小于等于2,我们就简单地分配编码就好了:
if (vec.size() <= 2)
{
CString result;
std::string coding,s,code;
std::vector<std::pair<char, float>>::iterator ic=vec.begin();
s = "的霍夫曼编码为:";
s = ic->first + s;
code = '0';
coding = s + code;
result = coding.c_str();
AfxMessageBox(result);
if (vec.size() == 2) {
coding.clear();
ic++;
s = "的霍夫曼编码为:";
s = ic->first + s;
code = '1';
coding = s + code;
result = coding.c_str();
AfxMessageBox(result);
}
}
最后,我们只要对创建好的霍夫曼树进行编码就可以了:
这里的第三步第四步和第二步是一样的所以函数图也是一样的。
接下来就是最重要的两个环节了:
函数图中右下角的罗马数字代表第几次的函数,有利于大家理解递归函数的过程。
后面的过程都是一样的就不赘述了,最后我们就得到了某一个字符的霍夫曼编码:
对霍夫曼树中的所有字符编码 只需要循环遍历调用我们前面写好的search函数就可以了:
std::string coding;
for (std::vector<std::pair<char, float>>::iterator ii = vec.begin(); ii != vec.end();ii++)
{
pointer = root;
code = search(pointer, ii->first, code);//对每一个字符的编码函数
std::string::iterator is = coding.begin();
std::string s = "的霍夫曼编码为:";
std::cout << ii->first << "的霍夫曼编码为:";
coding.insert(is,ii->first);
coding=coding+s;
is = coding.end();
for (std::vector<char>::iterator it = code.begin(); it != code.end(); it++) {
is=coding.insert(is, *it);
std::cout << *it;
}
CString result;
result = coding.c_str();
AfxMessageBox(result);
coding.clear();
std::cout << std::endl;
code.clear();
change_flag();
}
小结
内容其实有点多了,本文章只用于学习交流,写的不好的地方欢迎指正批评!完整程序会在后面更新放出。