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

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

算法描述

哈夫曼编码算法的定义

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

哈夫曼编码编码方式

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

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

哈夫曼树创建图示
哈夫曼编码--压缩与解压_第1张图片

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

哈夫曼编码--压缩与解压_第2张图片
利用所创建的哈夫曼树对字母进行编码

压缩

压缩基本方法

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

abadeedcadf (共11个)

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

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

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

关于头文件

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

意义:由于不同的文件压缩所用编码不同,必须将对应的编码写入到压缩文件中,才能在解压时对原文件的还原。如果没有压缩时所使用的编码信息,那么压缩出的文件就是没有用的。
哈夫曼编码--压缩与解压_第3张图片

压缩后文件格式

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

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

解压缩

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

哈夫曼编码--压缩与解压_第4张图片

解压与压缩对比图

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

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

程序执行基本界面

哈夫曼编码--压缩与解压_第5张图片

#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");
    }
}

你可能感兴趣的:(数据结构,算法)