Huffman编码数据压缩算法实现(附MFC界面)
前言
最近老板接了一个国网电力压缩的项目交给我们来做,因为要写一个演示的界面,电脑上又没有装QT,想着自己以后可能也不会写界面这一类的用的不多也就没下,就直接用了Windows的MFC写了一个简单的小压缩程序,这是本人上研究生以来老板给的第一个横向项目,自己也是个小菜鸡到现在什么语言都用的不太熟悉,就这个机会将自己这次写的程序写成一篇文章记录下来,以备后续参考。应用程序中集成了两种无损压缩算法,Huffman编码和LZ77编码,还有两种压缩算法叠加的组合压缩。ps:本人不搞数据压缩,还花了点儿时间研究了下这两种算法,只懂基本的原理 = =!!
Huffman算法的实现
Huffman算法也是一个比较老的算法了,基本流程是先构建Huffman树,再进行Huffman编码,最后将编码结果用二进制的比特流写入二进制文件,不多废话,直接上代码:
这是huffman算法的头文件Huffman.h
#pragma once
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <map>
using namespace std;
//定义树节点的最大容量
const int MAX_N = 100;
//定义为无穷大
const int INF = 0x7fffffff;
//哈夫曼树节点定义
class HNode
{
public:
char data; //节点值
double weight; //权重
int parent; //双亲节点
int lchild; //左孩子节点
int rchild; //右孩子节点
};
//哈夫曼编码节点
class HuffCode
{
public:
CString code;//数据的编码
char huffinfo;//被编码数据
};
class HuffMan
{
public:
HuffMan() {}
~HuffMan() {}
/*********************************************************/
//函数名:CreateForest
//返回值类型:void
//参数:string filename
//功能:
// 1.读取文件中数据信息
// 2.构建单节点二叉树组成的森林
/*******************************************************/
bool CreateForest(CString filename, bool replace);
/*********************************************************/
//函数名:CreateHuffManTree
//返回值类型:void
//参数:NULL
//功能:利用森林构建哈夫曼树
/********************************************************/
void CreateHuffManTree();
/*********************************************************/
//函数名:CreateHuffCode
//返回值类型:bool -> 判断是否编码成功,成功返回true,否则false
//功能:生成哈夫曼编码
/********************************************************/
bool CreateHuffCode();
/*********************************************************/
//函数名:compress
//返回值类型:double
//参数:需要压缩文件的路径
//功能:压缩文件并输出压缩比例
/********************************************************/
double compress(CString filepath, CString outpath, CProgressCtrl &pro, CStatic &m_proview);
/*********************************************************/
//函数名:DeCompress
//返回值类型:void
//参数:需要解压缩文件的路径
//功能:解压缩文件并输出
/********************************************************/
void DeCompress(CString outpath, string codestream, CProgressCtrl &pro, CStatic &m_proview, bool replace);
/*********************************************************/
//函数名:GetInfo
//返回值类型:string
//参数:需要获取信息的文件的路径
//功能:输出目标文件的大小,单位为字节
//返回值:从编码的二进制文件中得到源文件的字符统计信息和二进制流,并将二进制流转换为字符串返回
/********************************************************/
string GetInfo(CString inpath);
/*********************************************************/
//函数名:AcquireLength
//返回值类型:long
//参数:需要获取长度的文件的路径
//功能:输出目标文件的大小,单位为字节
/********************************************************/
unsigned long long AcquireLength(CString filepath);
/*********************************************************/
//这个函数其实对于huffman编码来说没啥用,可以忽略,是我用来代替文本中重复字符串用的
//函数名:Replace
//返回值类型:string
//参数:需要替换的目标字符串,要替换为的字符串,
//功能:输出目标文件的大小,单位为字节
//返回值:返回替换后的字符串
/********************************************************/
string Replace(string str, string rep, string c);
public:
HNode ht[MAX_N];//哈夫曼树
HuffCode hcode[MAX_N];//编码
long snum;//总字符数
int count = 0;//需要编码的字符总数
map<char, double> statistic;//用于存储源文件字符统计信息的字典
};
上述程序中,compress和DeCompress函数中CProgressCtrl &pro, CStatic &m_proview,这两个参数是为了跟MFC进行信息交换的(本来想写个进度条的),事实上也没用到= =,不需要的可以省略。
这是Huffman算法的主程序文件
#include "stdafx.h" //没写界面这个可以不要
#include "ApplicationDlg.h" //没写界面这个可以不要
#include "Huffman.h"
#include <bitset>
/*********************************************************/
//函数名:AcquireLength
//返回值类型:long
//参数:需要获取长度的文件的路径
//功能:输出目标文件的大小,单位为字节
/********************************************************/
unsigned long long HuffMan::AcquireLength(CString filepath)
{
CStdioFile inputFile;
unsigned long long inSize;
if (inputFile.Open(filepath, CStdioFile::modeRead))
{
inSize = inputFile.GetLength();
}
inputFile.Close();
return inSize;
}
/*********************************************************/
//这个函数其实对于huffman编码来说没啥用,可以忽略,是我用来代替文本中重复字符串用的
//函数名:Replace
//返回值类型:string
//参数:需要替换的目标字符串,要替换为的字符串,
//功能:输出目标文件的大小,单位为字节
//返回值:返回替换后的字符串
/********************************************************/
string HuffMan::Replace(string str, string rep, string c)
{
int pos;
pos = str.find(rep);查找指定的串
while (pos != -1)
{
str.replace(pos, rep.length(), c);用新的串替换掉指定的串
pos = str.find(rep);//继续查找指定的串,直到所有的都找到为止
}
return str;
}
/*********************************************************/
//函数名:CreateForest
//返回值类型:void
//参数:string filename
//返回:创建成功返回True,失败返回False
//功能:
// 1.读取文件中数据信息
// 2.构建单节点二叉树组成的森林
/*******************************************************/
bool HuffMan::CreateForest(CString filename, bool replace = FALSE)
{
int i = 0;
fstream readfile;
ULONGLONG inSize = AcquireLength(filename);
string strALL; //存储从文件中读到的字符
char c, b;
long pcount = 0;
readfile.open(filename, ios::in);
if (!readfile.is_open()) {
cout << "Could not find the file\n";
cout << "Program terminating\n";
exit(EXIT_FAILURE);
}
//下面这个if语句是为了替换源文件中的一些字符串并将新内容从新输入到一个文件中, 文件名为replace.txt
if (replace)
{
while (!readfile.eof())
{
if (readfile.good())
{
c = readfile.get();
if (c == '6')
{
b = readfile.get();
if (b == '8')
{
strALL += '#';
}
else
{
readfile.seekg(-1, ios::cur);
strALL += c;
}
}
else
{
strALL += c;
}
}
}
CString root = filename.Left(filename.ReverseFind('\\') + 1);
CString file_replace = root + TEXT("replace.txt");
ofstream repfile;
repfile.open(file_replace, ios::out);
repfile << strALL;
repfile.close();
}
else
{
while (!readfile.eof())
{
if (readfile.good())
{
c = readfile.get(); //每个字符都读,包括回车键‘\n’
strALL += c;
}
}
}
readfile.close();
pcount = strALL.length();
snum = pcount - 1;
//读取文件中各个字符出现的频次,单个字符的ASCII码值不会超过127,所以使用一个大小为127的数组进行统计
double *numbers = new double[127]();
for (long i = 0; i < snum; i++)
{
numbers[strALL[i]]++;
}
//构造霍夫曼树的节点,并将字符出现的频次信息写入一个字典中待后续使用
for (int j = 0; j < 127; j++)
{
if (numbers[j] != 0)
{
ht[count].data = (char) j;
ht[count].weight = numbers[j];
statistic[ht[count].data] = ht[count].weight;
count++;
}
}
delete[] numbers;
return true;
}
/*********************************************************/
//函数名:CreateHuffManTree
//返回值类型:void
//参数:NULL
//功能:利用森林构建哈夫曼树
/********************************************************/
void HuffMan::CreateHuffManTree()
{
//左右孩子
int lnode, rnode;
//最小的两个频率值
double min1, min2;
//先将各个待编码字符的左右孩子节点的值初始化为-1,作为后续判断叶子节点的标志
for (int i = 0; i < count; i++)
{
ht[i].parent = ht[i].lchild = ht[i].rchild = -1;
}//end for -> init result
//根据单独的节点构造出一颗完整的霍夫曼树,整棵树的节点数量为2 * count - 1
for (int j = count; j < 2 * count - 1; j++)
{
//每次重新搜寻最小权值的时候都将最小值和次小值初始化
min1 = min2 = INF;
lnode = rnode = -1;
//遍历寻找最小的两个频率值min1和min2
for (int k = 0; k < j; k++)
{
//逐一排查不存在双亲的节点
if (ht[k].parent == -1)
{
//每次寻找最小值节点时先除去含有双亲的节点
if (ht[k].weight < min1)//小于最小
{
min2 = min1;//最小赋值给次小
rnode = lnode;//同理下标
min1 = ht[k].weight;//新的最小频率值
lnode = k;//其下标
}//end if -> less
//不小于最小,小于次小
else if (ht[k].weight < min2)
{
min2 = ht[k].weight;
rnode = k;
}//end else if -> only bigger than min1
//no else
}//end if -> search
}
ht[j].weight = ht[lnode].weight + ht[rnode].weight;//双亲结点权重
ht[j].lchild = lnode;//双亲节点左孩子
ht[j].rchild = rnode;
ht[lnode].parent = j;//原最小频率值所在节点的双亲节点赋值为当前节点j
ht[rnode].parent = j;
ht[j].parent = -1;//双亲节点参与比较,赋值为-1
}
}
/*********************************************************/
//函数名:CreateHuffCode
//返回值类型:bool -> 判断是否编码成功,成功返回true,否则false
//功能:根据构造好的霍夫曼树生成哈夫曼编码
/********************************************************/
bool HuffMan::CreateHuffCode()
{
int f, c;
HuffCode hc;
for (int i = 0; i < count; i++)//作为hcode下标
{
//自下往上搜寻,直至找到parent为-1的节点即是根节点
hc.huffinfo = ht[i].data;
c = i;//下标->左节点
f = ht[i].parent;
while (f != -1)//未到根节点
{
if (ht[f].lchild == c)//找到左节点
{
hc.code = _T("0") + hc.code;//左边较小赋值为0
}//end if -> left node
else
{
hc.code = _T("1") + hc.code;//右边赋值为1
}//end else -> right node
c = f;//替换为上一层节点
f = ht[f].parent;//上一层的双亲节点
}//end while
hcode[i] = hc;//赋值当前编码
hc.code = "";//清空code内容,进行下一次访问
}//end for
return true;
}
/*********************************************************/
//函数名:compress
//返回值类型:double
//参数:需要压缩文件的路径,压缩文件的输出路径
//返回值:压缩比例
//功能:对源文件进行压缩并输出
/********************************************************/
double HuffMan::compress(CString filepath, CString outpath, CProgressCtrl &pro, CStatic &m_proview)
{
ifstream inFile;
inFile.open(filepath);
if (!inFile.is_open()) {
cout << "Could not find the file\n";
cout << "Program terminating\n";
exit(EXIT_FAILURE);
}
ofstream outFile;
outFile.open(outpath, ios::out | ios::binary);
if (!outFile.is_open()) {
cout << "Could not find the file\n";
cout << "Program terminating\n";
exit(EXIT_FAILURE);
}
//将字符的统计信息写入压缩文件,字符总数+各个字符出现的频次,以供后续解压使用
outFile << snum;
outFile << ":";
for (auto iter : statistic)
{
outFile << iter.first << "~" << iter.second << " ";
}
//统计信息结束标志
outFile << "*@#";
//开始写入压缩内容,每8位一个字节写入文件,末尾不够8位的话补0
char c;
int pos = 0;
long number = 0;
char value = 0;
double progress = 0;
CString code, str;
double step = 100 / snum;
inFile >> noskipws; //读字符的时候不跳过换行符
inFile >> c;
number++;
while (!inFile.eof())
{
if (inFile.good())
{
for (int j = 0; j < count; j++)
{
if (c == hcode[j].huffinfo)
{
code = hcode[j].code; //找到字符对应的编码
break;
}
}
for (int i = 0; i < code.GetLength(); i++)
{
value = value << 1;
if (code[i] == '1')
{
value |= 1; //这个应该好理解, 00000000 | 00000001 = 00000001,相当于最低位赋值1
}
if (++pos == 8)
{
outFile << value; // 满8位就将其写入文件
value = 0;
pos = 0;
}
}
inFile >> c;
}
}
//末尾不够8位补0
if (pos)
{
value = value << (8 - pos);
outFile << value;
}
inFile.close();
outFile.close();
ULONGLONG originLen = AcquireLength(filepath);
ULONGLONG compressLen = AcquireLength(outpath);
double ratio = double(compressLen) * 100 / double(originLen);
return ratio;
}
/*********************************************************/
//函数名:GetInfo
//返回值类型:string
//参数:需要获取信息的文件的路径
//功能:输出目标文件的大小,单位为字节
//返回值:从编码的二进制文件中得到源文件的字符统计信息和二进制流,并将二进制流转换为字符串返回
/********************************************************/
string HuffMan::GetInfo(CString inpath)
{
//先将待解压文件中的字符统计信息和编码内容读取出来,分别用strline和codestream来表示
char c, o, p, q;
bitset<8> a;
string strline, codestream; //strline存储二进制文件中的字符及对应权值,codestream存储01串
fstream infile_b(inpath, ios::in | ios::binary);
while (true) //从二进制文件中获取字符及对应权值
{
if (infile_b.peek() == EOF)
break;
infile_b.read(&o, 1);
if (o == '*')
{
infile_b.read(&p, 1);
infile_b.read(&q, 1);
if (p == '@'&&q == '#')
break;
else infile_b.seekg(-2, ios::cur);
}
strline += o;
}
while (true) //读取字符转化为01串
{
if (infile_b.peek() == EOF)
break;
infile_b.read(&c, 1);
a = c; //字符赋值给bitset<8>可直接转化为二进制
codestream += a.to_string();
}
//根据字符统计信息构造霍夫曼树,然后逐字符读取编码内容根据霍夫曼树进行解码
char key;
double b;
long t;
double snumcopy;
string tmp, num;
map<char, double> data_b;
while (!strline.empty()) //读取其中存储的字符以及出现的频次,由此生成哈弗曼树
{
if (snum == 0)
{
//提取文件中字符的总数
t = strline.find(':');
num = strline.substr(0, t);
strline.erase(0, t + 1);
snum = long(stoi(num, nullptr, 10));
snumcopy = double(snum);
}
//提取各个字符的数量并将其存入字典
key = strline.at(0);
strline.erase(0, 2);
t = strline.find(' ');
tmp = strline.substr(0, t);
strline.erase(0, t + 1);
b = double(stoi(tmp, nullptr, 10));
pair<char, double> item(key, b);
data_b.insert(item);
}
map<char, double>::iterator it;
count = 0;
for (it = data_b.begin(); it != data_b.end(); it++)
{
ht[count].data = (*it).first;
ht[count].weight = (*it).second;
count++;
}
CreateHuffManTree();//此处生成霍夫曼树
infile_b.close();
return codestream;
}
/*********************************************************/
//函数名:DeCompress
//返回值类型:void
//参数:需要解压缩文件的路径
//功能:解压缩文件并输出,输出文件为.txt文件
/********************************************************/
void HuffMan::DeCompress(CString outpath, string codestream, CProgressCtrl &pro, CStatic &m_proview, bool replace = FALSE)
{
fstream outfile_t(outpath, ios::out);
CString str;
long number = 0;
double snumcopy;
double progress = 0;
snumcopy = double(snum);
double step = 100 / snumcopy;
//从根节点开始,根据压缩内容,遇到0向左,遇到1向右,直至读取到叶子节点,将叶子节点代表的字符写入txt文件
HNode root = ht[2 * count - 2];
for (auto i : codestream) //用01串遍历哈弗曼树
{
if (i == '0') //遇0,走左
root = ht[root.lchild];
else //遇1,走右
root = ht[root.rchild];
if (root.lchild == -1 && root.rchild == -1) //遇叶子节点,提取字符并存储
{
if (replace)
{
number++;
if (root.data == '#')
{
outfile_t << "68";
}
else
{
outfile_t << root.data;
}
root = ht[2 * count - 2];
snum -= 1;
}
else
{
number++;
outfile_t << root.data;
/*pro.SetPos(number);
progress += step;
str.Format(L"%.2f", progress);
str = str + TEXT("%");
m_proview.SetWindowTextW(str);*/
root = ht[2 * count - 2];
snum -= 1;
}
}
if (snum == 0)
{
break;
}
}
outfile_t.close();
}
关于使用C++遍历某个文件夹下所有文件的方法
这里仅记录关键代码,下述代码可以封装进函数里,前提是文件夹中只含有文件而不含有子文件夹,包含子文件夹遍历的代码多加一层判断写个递归就好了
bool cmp(std::string const &arg_a, std::string const &arg_b)
{
return arg_a.size() < arg_b.size() || (arg_a.size() == arg_b.size() && arg_a < arg_b);
}
long hFile = 0;
string path = "E:\\胡浩星\\工作项目\\电力数据压缩\\霍夫曼编码实验\\[450, 500)";
vector<string> files;
vector<string>::iterator it;
struct _finddata_t fileinfo;
string p;
if ((hFile = _findfirst(p.assign(path).append("\\*").c_str(), &fileinfo)) != -1)
{
do {
files.push_back(p.assign(path).append("\\").append(fileinfo.name));
} while (_findnext(hFile, &fileinfo) == 0); //寻找下一个,成功返回0,否则-1
_findclose(hFile);
}
files.erase(files.begin(), files.begin() + 2); //这里一般来讲返回的vector中前两个元素是文件夹当前目录,与文件夹所在目录,程序中用不到就剔除了
sort(files.begin(), files.end(), cmp); //很多需要根据文件名的数字顺序进行排序,所以需要这一句,比如图片如果是按照1, 2, 3, 4, 5.....这样命名的话就需要这步操作
最终的效果图:
第一次写博客,如果中间有什么东西写错了误导了大家请见谅,欢迎大家批评指正。