哈夫曼编码--压缩与解压

来源:互联网 发布:神仙劫进阶数据 编辑:程序博客网 时间:2024/05/30 04:36

  • 算法描述
    • 哈夫曼编码算法的定义
    • 哈夫曼编码编码方式
  • 压缩
    • 压缩基本方法
    • 关于头文件
  • 解压缩
  • 程序执行基本界面

算法描述

哈夫曼编码算法的定义

  哈夫曼编码,又称霍夫曼编码,是一种编码方式,为可变字长编码(VLC)。该方法完全依据字符出现概率来构造异字头的平均长度最短的码字,有时称之为最佳编码。

哈夫曼编码编码方式

  该编码方式根据不同字符出现的概率来进行构建最佳二叉树,所有的字符都位于叶子节点,规定从根节点开始,往左走为0,往右走为1,通过这种方式,可以对所有的字符进行重新的编码,从而实现码字的平均长度最短。

具体编码方式如下:
1. 从列表中选出两个具有出现次数最少的符号,以这两个符号作为孩子节点,创建一棵哈夫曼子树,并为这两个及诶单创建一个父亲节点。
2. 以孩子结点的出现次数的总和作为父亲节点出现的次数,将父亲节点插入字符的列表中。
3. 从列表中将孩子结点删除(利用是否有父亲节点区分是否需要考虑)
4. 根据从根到叶子节点的路径为每一叶子节点赋予一个码字。

哈夫曼树创建图示
字母频数表及所创建的哈夫曼树

字母频数表及所创建的哈夫曼树

利用所创建的哈夫曼树对字母进行编码
利用所创建的哈夫曼树对字母进行编码

压缩

压缩基本方法

  利用一个例子进行讲述,我们压缩以下一段字符串:

abadeedcadf (共11个)

  按照图4中得到的编码可以得到以下的一段01字符串:
101111110111100011010010111101010 (共33个)

  由于每个ASCII字符都是大小8位的字符,所以按照每8个01字符生成一个ASCII字符,不足8个的在其后用0补充完整,可以得到:
10111111 01111000 11010010 11110101 00000000 (共4段)

  也就是说之前的11个字符就可以用4个字符来代替。从而实现有效的压缩。由于最后进行了0拓展,所以在解压的时候也会将这些编码进行解压,如果不凑巧的话,可能会多处一两个甚至更多的字符。因此我们需要压入记录原文件的大小,进行对比判断是否压缩完成。

关于头文件

定义:头文件是指压缩文件中开始部分,包含原文件信息及压缩文件的字符编码信息编码。头文件不同,所得的文件解压方式不同,大小也会有所不同,甚至会变大。

意义:由于不同的文件压缩所用编码不同,必须将对应的编码写入到压缩文件中,才能在解压时对原文件的还原。如果没有压缩时所使用的编码信息,那么压缩出的文件就是没有用的。
压缩后文件格式

压缩后文件格式

头文件编码格式
头文件编码格式

  频数的位置对应ASCII码,如第0个频数表示ASCII码为0字符的频数。读取这些频数,可以有效地恢复哈夫曼树,以达到对压缩文件的解压。如果频数用unsigned long格式的数据,就可以记录很大的数据,头文件仅占有2k。而利用该头文件格式,最大可压缩文件大小为:256 * 2^64 B = 2^52GB。

解压缩

解压大体过程:
1. 利用头文件中的字符频数构建原来额字母表。
2. 利用字母表构建哈夫曼树。
3. 以字符的方式读取原文件压缩内容为01串。
4. 利用哈夫曼树进行文件解压。

解压与压缩对比图

解压与压缩对比图

字母压缩 单词压缩 Zip压缩 时间效率 4.693 s 1.855 s 压缩效率 0.65336 0.303638

  由于我的笔记本没有安装WinRar,所以无法调用rar指令进行rar压缩,这里使用Zip压缩进行对比。使用的是360压缩。

程序执行基本界面

程序执行基本界面

#include<iostream>#include<cstdio>#include<cstring>#include<string.h>#include<string>#include<conio.h>#include<stdlib.h>#include<io.h>#include <ctime>using namespace std;// 256 个字符,最多有 2 * 256 - 1 = 511个节点,这里用 512 + 5#define MAXLEN 512+5#define ASCLLNUM 256int 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<<"功能:"<<endl;    cout<<"     1.压缩"<<endl;    cout<<"     2.解压缩"<<endl;    cout<<"     3.输出编码"<<endl;    cout<<"     4.测试ZIP"<<endl;    cout<<"     5.退出"<<endl;    cout<<endl;    cout<<"注意:使用本程序压缩后压缩文件拓展名为.gr。"<<endl;    cout<<"      压缩和解压时请输入完整的文件路径。"<<endl;    cout<<endl;    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<m-start;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<<"文件:"<<infileName<<"不存在..."<<endl;        cout<<"重新输入需压缩文件路径(1)或返回主菜单(2)?"<<endl;        char option;        cin>>option;        while(option != '1' && option != '2')        {            cout<<endl;            cout<<"无效输入!"<<endl;            cout<<"重新输入文件名(1)或返回主菜单(2)?"<<endl;            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<<"文件:"<<outfileName<<"已存在..."<<endl;        cout<<"是否替换原文件?(Y/N):";        char option;        cin>>option;        while(option != 'Y' && option != 'N' && option != 'y' && option != 'n')        {            cout<<"\n无效输入!"<<endl;            cout<<"请输入Y或者N:";            cin>>option;        }        if(option == 'Y' || option == 'y'){            break;        }        cout<<"请手动输入压缩文件文件路径(含拓展名):";        cin>>outfileName;        cout<<outfileName; //DEB    }    //判断是否可以创建该文件,如果不行,表示无法再文件系统中创建该文件。输入内容有误    outfile = fopen(outfileName,"wb");    if(outfile == NULL)    {        cout<<"\n无法创建该压缩文件..."<<endl;        cout<<"请输入任意键返回主菜单...";        _getch();        return;    }    cout<<"文件压缩中..."<<endl;    //[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"<<endl;        for(i = 0; i < 256; i ++){            if(ascll[i].count > 0){                cout<<ascll[i].alpha<<"\t"<<ascll[i].count<<"\t"<<hTable[ascll[i].alpha]<<endl;            }        }        // 关闭打开的文件        fclose(infile);        fclose(outfile);        //[TIME-END]        const double end=(double)clock()/CLK_TCK;        cout<<"编码耗时:"<<(end-begin)<<" s"<<endl;        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<<c<<endl;    //  cout<<hTable[c]<<endl;        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<<c<<endl;            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<<"压缩成功!"<<endl;    float s;    s=(float)charNum / (float)total;    cout<<"压缩率为:"<<s<<endl;    cout<<"耗时为:"<<(end-begin)<<" s"<<endl;    _getch();    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<<"文件"<<infilename<<"不存在...\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<<"解压文件中..."<<endl;    //[TIME-BEGIN]    const double begin=(double)clock()/CLK_TCK;    unsigned long total = 0;    // 将第一个 long 长度数据读入 tatol 中,为文件的总大小    fread(&total,sizeof(unsigned long),1,infile);    //cout<<"原来大小: "<<total<<endl;    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 = "<<leafNum<<endl;    if(test){        for(i = 0; i < 256;i ++){            if(ascll[i].count > 0){                cout<<ascll[i].alpha<<"\t"<<ascll[i].count<<endl;            }        }    }/*    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<<"第一个字符:"<<c<<endl;        }        // 从根节点往叶子走,读取到的是一个 字母(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<<"解压成功"<<endl;    float s;    s=(float)charNum / (float)total;    cout<<"完整度为:"<<s<<endl;    cout<<"耗时为:"<<(end-begin)<<" s"<<endl;    _getch();    return;}void shouEncode(){    compress(false);    cout<<"已显示完成"<<endl;    _getch();}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)<<endl;    _getch();}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<<"谢谢您的使用!"<<endl;                return 0;            }        }        system("cls");    }}
0 0