哈夫曼编码,又称霍夫曼编码,是一种编码方式,为可变字长编码(VLC)。该方法完全依据字符出现概率来构造异字头的平均长度最短的码字,有时称之为最佳编码。
该编码方式根据不同字符出现的概率来进行构建最佳二叉树,所有的字符都位于叶子节点,规定从根节点开始,往左走为0,往右走为1,通过这种方式,可以对所有的字符进行重新的编码,从而实现码字的平均长度最短。
具体编码方式如下:
1. 从列表中选出两个具有出现次数最少的符号,以这两个符号作为孩子节点,创建一棵哈夫曼子树,并为这两个及诶单创建一个父亲节点。
2. 以孩子结点的出现次数的总和作为父亲节点出现的次数,将父亲节点插入字符的列表中。
3. 从列表中将孩子结点删除(利用是否有父亲节点区分是否需要考虑)
4. 根据从根到叶子节点的路径为每一叶子节点赋予一个码字。
利用一个例子进行讲述,我们压缩以下一段字符串:
定义:头文件是指压缩文件中开始部分,包含原文件信息及压缩文件的字符编码信息编码。头文件不同,所得的文件解压方式不同,大小也会有所不同,甚至会变大。
意义:由于不同的文件压缩所用编码不同,必须将对应的编码写入到压缩文件中,才能在解压时对原文件的还原。如果没有压缩时所使用的编码信息,那么压缩出的文件就是没有用的。
解压大体过程:
1. 利用头文件中的字符频数构建原来额字母表。
2. 利用字母表构建哈夫曼树。
3. 以字符的方式读取原文件压缩内容为01串。
4. 利用哈夫曼树进行文件解压。
字母压缩 | 单词压缩 | Zip压缩 |
---|---|---|
时间效率 | 4.693 s | 1.855 s |
压缩效率 | 0.65336 | 0.303638 |
由于我的笔记本没有安装WinRar,所以无法调用rar指令进行rar压缩,这里使用Zip压缩进行对比。使用的是360压缩。
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
// 256 个字符,最多有 2 * 256 - 1 = 511个节点,这里用 512 + 5
#define MAXLEN 512+5
#define ASCLLNUM 256
int test = false;
//哈夫曼树节点
typedef struct huffNode
{
int parent,lchild,rchild; //二叉树关系
unsigned long count; //符号个数
unsigned char alpha; //符号
char code[MAXLEN]; //编码
}HuffNode;
//存放文件中各个字符,及其出现次数
typedef struct ascll{
unsigned char alpha; //符号
unsigned long count; //符号个数
}Ascll;
//这个存放字符及字符对应的编码
/*
typedef struct huffTable{
unsigned char alpha; //符号
char code[MAXLEN]; //编码
}HuffTable;
*/
//展示交互界面
void showGUI()
{
cout<<" 压缩、解压缩工具 \n\n";
cout<<"功能:"<cout<<" 1.压缩"<cout<<" 2.解压缩"<cout<<" 3.输出编码"<cout<<" 4.测试ZIP"<cout<<" 5.退出"<cout<cout<<"注意:使用本程序压缩后压缩文件拓展名为.gr。"<cout<<" 压缩和解压时请输入完整的文件路径。"<cout<cout<<"请选择操作:";
}
void select(HuffNode* HT, int i, int* s1, int* s2)
{
unsigned int j, s;
s = 0; //记录当前找到的最小权值的结点的下标
for(j=1;j<=i;j++)
{
if(HT[j].parent == 0) //找最小
{
if(s==0) //第一个找到的点
s=j;
if(HT[j].count < HT[s].count)
s=j;
}
}
*s1 = s;
s = 0;
for(j=1;j<=i;j++) //找次小
{
if((HT[j].parent == 0)&&(j!=*s1)) //仅比上面一个多了J!=*s1,应为不能是最小
{
if(s==0)
s=j;
if(HT[j].count < HT[s].count)
s=j;
}
}
*s2 = s;
}
//创建的哈夫曼树是以一维数组建立,同时起始地址是1。
int creatHuffmanTree(HuffNode* HT, Ascll* ascll)
{
int i,s1,s2,leafNum=0,j=0;
//初始化叶节点,256个ascll字符
for(i = 0; i < 256; i ++)
{
//只使用出现的过的字符 ascll[i].count > 0
if(ascll[i].count > 0)
{
HT[++j].count = ascll[i].count;
HT[j].alpha = ascll[i].alpha;
HT[j].parent=HT[j].lchild=HT[j].rchild=0;
}
}
// [叶子] [叶子] [叶子] [叶子] ···[内部] [根]
leafNum = j;
int nodeNum = 2*leafNum -1; //节点个数
//初始化内部节点
for(i = leafNum + 1; i <= nodeNum; i++)
{
HT[i].count = 0;
HT[i].code[0] = 0;
HT[i].parent = HT[i].lchild = HT[i].rchild = 0;
}
//给内部节点找孩子
for(i = leafNum + 1; i <= nodeNum; i++)
{
select(HT, i - 1, &s1, &s2); //找到当前最小和次小的树根
HT[s1].parent=i;
HT[s2].parent=i;
HT[i].lchild=s2;
HT[i].rchild=s1;
HT[i].count=HT[s1].count+HT[s2].count;
}
return leafNum;
}
//哈弗曼编码
void HuffmanCoding(char* hTable[ASCLLNUM], HuffNode* HT, int leafNum)
{
int i,j,m,c,f,start;
char cd[MAXLEN];
m = MAXLEN;
cd[m-1] = 0;
for(i=1;i <= leafNum;i++)
{
start = m-1;
//先是从后往前编码,从子叶开始编码
for(c=i,f=HT[c].parent; f!=0; c=f,f=HT[f].parent) //找爸爸
{ //判断自己c是爸爸的哪个孩子
if(HT[f].lchild==c)
{
// 左 0
cd[start--]='0';
}
else
{
// 右 1
cd[start--]='1';
}
}
// [0 0 0 0 0 start 0 1 0 1 1], start 表示偏移,m-start 表示压入的01的长度,start到达根
start++;
//int end = m-1;
for(j=0;j// 获取字符编码
HT[i].code[j]=cd[start+j];
// 编码 [叶子]---[根]
//HT[i].code[j]=cd[end--];
}
// 添加结尾
HT[i].code[j]='\0';
//写入字符-频数表
hTable[ HT[i].alpha ] = HT[i].code;
}
}
void compress(bool compress)
{
FILE *infile = NULL,
*outfile = NULL;
char infileName[MAXLEN],outfileName[MAXLEN];
cout<<"\n请输入你想要压缩的文件路径:";
cin>>infileName;
// 打开文件
infile = fopen(infileName,"rb");
while(infile == NULL)
{
cout<<"文件:"<"不存在..."<cout<<"重新输入需压缩文件路径(1)或返回主菜单(2)?"<char option;
cin>>option;
while(option != '1' && option != '2')
{
cout<cout<<"无效输入!"<cout<<"重新输入文件名(1)或返回主菜单(2)?"<cin>>option;
}
if(option == '2'){
return;
}
cout<<"\n请输入需压缩文件路径:";
cin>>infileName;
// 读取文件
infile = fopen(infileName,"rb");
}
// 创建文件名
strcpy(outfileName,infileName);
strcat(outfileName,".gr");
// 判断文件是否存在
// 对文件进行操作,判断文件是否存在
while( (_access(outfileName, 0 )) != -1 )
{
cout<<"文件:"<"已存在..."<cout<<"是否替换原文件?(Y/N):";
char option;
cin>>option;
while(option != 'Y' && option != 'N' && option != 'y' && option != 'n')
{
cout<<"\n无效输入!"<cout<<"请输入Y或者N:";
cin>>option;
}
if(option == 'Y' || option == 'y'){
break;
}
cout<<"请手动输入压缩文件文件路径(含拓展名):";
cin>>outfileName;
cout<//DEB
}
//判断是否可以创建该文件,如果不行,表示无法再文件系统中创建该文件。输入内容有误
outfile = fopen(outfileName,"wb");
if(outfile == NULL)
{
cout<<"\n无法创建该压缩文件..."<cout<<"请输入任意键返回主菜单...";
_getch();
return;
}
cout<<"文件压缩中..."<//[TIME-START]
const double begin=(double)clock()/CLK_TCK;
//统计字符种类数和频数
unsigned char c;
int i,k;
unsigned long total=0; //文件长度
// 利用hash表存放字母表及字母出现频数
Ascll ascll[ASCLLNUM];
for(i = 0; i < ASCLLNUM; i++)
{
ascll[i].count = 0;
}
while(!feof(infile))
{
c=fgetc(infile);
ascll[c].alpha = c;
ascll[c].count++;
total++; //读取到的字符个数
}
total--;
// ascll[c].count--; //TODO
// 创建 哈弗曼树节点数组
HuffNode HT[MAXLEN];
int leafNum = creatHuffmanTree(HT,ascll);
char *hTable[MAXLEN];
for(i = 0; i < ASCLLNUM; i ++)
{
hTable[i] = new char[MAXLEN];
}
// 哈夫曼编码
HuffmanCoding(hTable, HT, leafNum);
if(!compress){
cout<<"字母\t字频数表\t编码\t"<for(i = 0; i < 256; i ++){
if(ascll[i].count > 0){
cout<"\t"<"\t"<// 关闭打开的文件
fclose(infile);
fclose(outfile);
//[TIME-END]
const double end=(double)clock()/CLK_TCK;
cout<<"编码耗时:"<<(end-begin)<<" s"<return;
}
//写头文件 -- 将压缩的哈夫曼树编码信息写入哈夫曼树
fseek(infile,0,0);
fwrite(&total,sizeof(unsigned long),1,outfile); //原文件总长度
for(i=0; i<=255; i++)
{
// 将哈夫曼树按照 unsigned long 的方式压入文件中,下标表示字母,数值表示字母的频数
fwrite(&ascll[i].count,sizeof(unsigned long),1,outfile);
}
//开始压缩主文件
//char buf[MAXLEN];
unsigned long j=0; //最大为total
// buf[0]=0;
string buf = "\0";
int charNum=2;
while(!feof(infile))
{
c=fgetc(infile);
// cout<
// cout<
string tempCode = hTable[c];
j++;
// strcat(buf,tempCode);
buf += tempCode;
//k=strlen(buf);
k = buf.length();
c=0;
// 将所得的 0 1 每8个就可以构建一个字母的方式压入文件用
while(k>=8)
{
for(i=0; i<8; i++)
{
// 利用左移以为在右边空出一个空位
// 利用与 1 取或的方式压入 bit 1
if(buf[i]=='1')
c=(c<<1)|1;
else
c=c<<1;
}
// cout<
fwrite(&c,sizeof(unsigned char),1,outfile);
charNum ++;
// strcpy(buf,buf+8);
buf.erase(0,8);
//k=strlen(buf);
// 确定剩下的bit 的长度,如果大于8表示还可以压成一个字节
k = buf.length();
}
if(j==total){
break;
}
}
// 当 k < 8 时,表示还剩下不足 8 位的bit,需要拓展0位压缩
if(k>0) //可能还有剩余字符
{
// strcat(buf,"00000000");
buf += "00000000";
for(i=0; i<8; i++)
{
if(buf[i]=='1')
c=(c<<1)|1;
else
c=c<<1;
}
fwrite(&c,sizeof(unsigned char),1,outfile);
charNum ++;
}
// 关闭打开的文件
fclose(infile);
fclose(outfile);
//[TIME-END]
const double end=(double)clock()/CLK_TCK;
cout<<"压缩成功!"<float s;
s=(float)charNum / (float)total;
cout<<"压缩率为:"<cout <<"耗时为:"<<(end-begin)<<" s"<return ;
}
void decompress() {
FILE *infile,*outfile;
char infilename[255],outfilename[255];
cout<<"请输入要解压的文件的文件路径(不含.gr):";
cin>>outfilename;
// 构建解压文件名
strcpy(infilename,outfilename);
strcat(infilename,".gr");
infile = fopen(infilename,"rb");
//循环判断文件是否存在
while(infile==NULL){
char option;
cout<<"文件"<"不存在...\n";
cout<<"重新输入文件名(1)或返回主菜单(2)?";
cin>>option;
while(option!='1' && option!='2')
{
cout<<"\n无效的输入!\n";
cout<<"重新输入文件名(1)或返回主菜单(2)?";
cin>>option;
}
if(option == '2'){
return;
}
else {
cout<<"\n请输入要解压的文件的文件路径(不含.gr):";
cin>>outfilename;
// 构建解压文件名
strcpy(infilename,outfilename);
strcat(infilename,".gr");
infile = fopen(infilename,"rb");
}
}
// 输入解压后的文件名
// cout<<"请输入解压后的文件的文件路径:";
// cin>>outfilename;
outfile = fopen(outfilename,"wb");
if(outfile==NULL) {
cout<<"\n解压文件失败!无法创建解压后的文件...";
cout<<"\n按任意键回到主菜单...";
_getch();
return;
}
cout<<"解压文件中..."<//[TIME-BEGIN]
const double begin=(double)clock()/CLK_TCK;
unsigned long total = 0;
// 将第一个 long 长度数据读入 tatol 中,为文件的总大小
fread(&total,sizeof(unsigned long),1,infile);
//cout<<"原来大小: "<
Ascll ascll[ASCLLNUM];
int i;
for(i = 0; i < ASCLLNUM; i++) {
// 之后的每个long长度都是一个字符的频数
fread(&ascll[i].count,sizeof(unsigned long),1,infile);
ascll[i].alpha = i;
}
HuffNode HT[MAXLEN];
// 创建哈夫曼树
int leafNum = creatHuffmanTree(HT,ascll);
//cout<<"leafNum = "<
if(test){
for(i = 0; i < 256;i ++){
if(ascll[i].count > 0){
cout<"\t" </*
char *hTable[MAXLEN];
for(i = 0; i < MAXLEN; i ++) {
hTable[i] = new char[MAXLEN];
}*/
fseek(infile,sizeof(unsigned long)*257,0);
unsigned char c = 0;
int index = 2*leafNum - 1;
int charNum = 0;
while(!feof(infile))
{
// 按照字母读取
c=fgetc(infile);
if(test){
cout<<"第一个字符:"<// 从根节点往叶子走,读取到的是一个 字母(8位), 所以使用循环8次
for(i=0; i<8; i++)
{
unsigned int cod = (c & 128);
c = c << 1;
if(cod == 0 ){
// 左 0 右 1
index = HT[index].lchild;
}
else{
index = HT[index].rchild;
}
if(HT[index].rchild == 0 && HT[index].lchild == 0){
charNum ++;
// 到达叶子
char trueChar = HT[index].alpha;
fwrite(&trueChar,sizeof(unsigned char),1,outfile);
// index 重新指向根节点
index = 2*leafNum - 1;
if(charNum >= total){
break;
}
}
}
if(charNum >= total){
break;
}
}
// 关闭打开的文件
fclose(infile);
fclose(outfile);
//[TIME-END]
const double end=(double)clock()/CLK_TCK;
cout<<"解压成功"<float s;
s=(float)charNum / (float)total;
cout<<"完整度为:"<cout<<"耗时为:"<<(end-begin)<<" s"<return;
}
void shouEncode()
{
compress(false);
cout<<"已显示完成"<void testZip(){
//[TIME-START]
const double begin=(double)clock()/CLK_TCK;
system("D:\\Install\\360Zip\\360Zip.exe D:\\Practice\\C++\\shanchu\\cacm.all.rar D:\\Practice\\C++\\shanchu\\cacm.all");
//[TIME-END]
const double end=(double)clock()/CLK_TCK;
cout<<"耗时:"<<(end - begin)<int main(){
while(1){
showGUI();
char option;
cin>>option;
while(option!='1' && option!='2' && option!='3' && option!='4' && option!='5')
{
cout<<"无效的输入!\n";
cout<<"请选择操作:";
cin>>option;
}
switch(option){
case '1':{
compress(true);
break;
}
case '2':{
decompress();
break;
}
case '3':{
shouEncode();
break;
}
case '4':{
testZip();
break;
}
default:{
cout<<"谢谢您的使用!"<return 0;
}
}
system("cls");
}
}