C语言实现哈夫曼压缩与解压缩的实现以及读取哈夫曼编码 万文长书,绝对详细哦

哈夫曼压缩与解压缩的实现

  • 开始之前,务必要看!看了能更好的理解代码
  • 一、整体的布局
  • 二、模块功能实现
    • 1、压缩
    • 2、解压缩
  • 三、尾记-主函数的详细介绍

开始之前,务必要看!看了能更好的理解代码

为了伙伴们更好的理解我们这个代码的实现,笔者先手动计算一个压缩过程,这样我们就能对整体的代码有一个了解了:
这一部分请真心想要做出哈夫曼压缩解压缩的伙伴们一定认真看,因为这里的逻辑就是代码的逻辑:
假设我们有一个字符串是:acefkmffmkfafkaffakaeef
C语言实现哈夫曼压缩与解压缩的实现以及读取哈夫曼编码 万文长书,绝对详细哦_第1张图片
那么它的哈夫曼树如图:
C语言实现哈夫曼压缩与解压缩的实现以及读取哈夫曼编码 万文长书,绝对详细哦_第2张图片
之后,我们进行计算:
C语言实现哈夫曼压缩与解压缩的实现以及读取哈夫曼编码 万文长书,绝对详细哦_第3张图片
这里我们说存入参数,其实就是存入了哈夫曼编码,实际上压缩是压缩了,但是参数比较多,就导致压缩后的文件反而比原文件大。

一、整体的布局

我们已经对整体的逻辑实现有了一个了解,现在就来看看具体的代码吧:

我们想要做到功能分模块,也就是我可以自主选择是压缩还是解压缩,并且查看情况,那么我们就把业务逻辑分离:
C语言实现哈夫曼压缩与解压缩的实现以及读取哈夫曼编码 万文长书,绝对详细哦_第4张图片
那么我们首先看看最简单的头文件huffman.h书写了什么东西吧!

#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include 
#include 
#include 
#include
#include
struct HuffmanNode
{
    int letter;						  //保存字符的映射
    long count;                   //文件中该字符出现的次数
    long parent, lchild, rchild;        //构建二叉树所需的数据
    char bits[256];               //对应的哈夫曼编码
};

struct HuffmanNode TreeNode[512], tmp, temp;  //节点树,TreeNode.count用来记录数据出现的次数
//函数compress(),读取文件内容并加以压缩,将压缩内容写入另一个文档
int compress(const char* filename, const char* outputfile);
//函数:uncompress(),作用:解压缩文件,并将解压后的内容写入新文件
int uncompress(const char* filename, const char* outputfile);

头文件主要就写了一些定义,并包含了其他可能需要的头文件。

现在来看看test.c文件
为了我们更好的互动,在测试文件中书写一个菜单,供选择,那么这个代码很简单:

#include"huffman.h"//注意头文件的包含
void menu()
{
    printf("\n1,压缩文件\n");
    printf("2,解压缩文件\n");
    printf("3,读取解压缩文件\n");
    printf("4,查看哈夫曼编码\n");
    printf("输入'q'退出程序\n");
    printf("请选择需要的操作:\n");
}

至于功能的实现,我们自然还是选择swtich语句,这里我们就需要考虑可能要写的功能了,比如,压缩文件,我们书写一个compress()函数,解压缩文件,我们书写一个uncompress()函数,读取文件的话,其实不需要再写函数,可以直接在uncompree()函数里实现解压缩文件的读取,而查看哈夫曼代码,我们既可以在编写哈夫曼树的时候去书写,也可以在解压缩的时候书写。

所以,简单的讲,我们只需要书写两个模块,就可以实现我们想要的功能,那么我们先看看逻辑主函数里都写了什么吧!

当然,这里的主函数会在尾记的时候详细的说一遍,这里可以选择不去理解。

void main()
{
    clock_t begin, end;
    double cost;
    FILE* fp;
    char ch, temparray[30];
    int ret1, ret2;
    int choice, count = 0, n;
    long f;
    memset(&TreeNode, 0, sizeof(TreeNode));
    memset(&tmp, 0, sizeof(tmp));

    menu();
    while (scanf("%d", &choice) == 1)
    {   
        system("cls");
        switch (choice)
        {
        case 1:
            ret1 = compress("data.txt", "data.zip.txt");
            menu();
            break;
        case 2:
            //开始记录
            begin = clock();
            ret2 = uncompress("data.zip.txt", "data.unzip.txt");
            //结束记录
            end = clock();
            cost = (double)(end - begin) / CLOCKS_PER_SEC;
            printf("解压缩耗时: %lf secs\n", cost);
            printf("压缩比率是%.3lf%%\n", ((double)ret1 / ret2) * 100);
            menu();
            break;
        case 3:
            printf("读取解压后的文件:\n");
            if ((fp = fopen("data.unzip.txt", "rt")) == NULL) {
                printf("读取失败!\n");
                exit(1);
            }
            ch = fgetc(fp);
            while (ch != EOF) {
                putchar(ch);
                ch = fgetc(fp);
            }
            fclose(fp);
            menu();
            break;
        case 4:
            if ((fp = fopen("data.zip.txt", "rb")) == NULL)
            {
                printf("读取失败!\n");
                exit(1);
            }
            fseek(fp, 4, SEEK_SET);
            fread(&f, sizeof(long), 1, fp);
            fseek(fp, f, SEEK_SET);
            fread(&n, sizeof(long), 1, fp);//读取字符种类总数
            for (int i = 0; i < n; i++)
            {
                printf("字符:%-5c频数:%-6ld哈夫曼编码:%-20s", TreeNode[i].letter, TreeNode[i].count, TreeNode[i].bits);
                count++;
                if (count % 4 == 0)
                    printf("\n");
            }
            menu();
            break;
        case 6:
            printf("欢迎再次使用!\n");
        default:
                break;
        }
    }
}

我们每次实现一个功能,再去选择另一个功能,就清理一下屏幕,所以这里书写了一个system("cls"),以便屏幕看着整洁一点。当然,可以根据个人的需要去更改。

由于我们希望计算压缩比率,那么我们书写模块的时候,还需要注意需要返回字节数。也就是书写一个有返回值的函数。

现在要看懂这些代码还是不行的,因为这里的功能的选择依赖于书写的模块的内容。所以我们还得继续看看模块都写了什么,才能理解这个main()函数的含义(并且在尾记里还会再介绍的,保证让每个人能有所收获)。放心,笔者会一一解释所有的可能的难点。

二、模块功能实现

1、压缩

压缩的功能就是读取原文件,进行压缩并写入新文件。这里几乎每一行代码我都给了注释,小伙伴们应该都可以看懂的,所以只挑几个必要的说一说了。

压缩文件的难点就在于写入压缩文件,也就是说,我们需要写入什么东西?因为SEEK_SET可以快速找到文件开头,这里我们在文件开头就写入原文件字节数和压缩文件字节数,为了防止丢失数据(如果不写在开头,就读不到这些数据)

之后我们就存入压缩文件的容器(char类型,个数也就是存储的压缩文件字节数,所以现在理解为什么要存储原文件长和压缩文件长了吗?是为了不多读取,也不少读取

然后我们在末尾再存入自己编写的哈夫曼编码(实际上更像一个自己写的ASCII编码表),这些参数的个数我们也需要记录,不然不好读取,也就是记录一共统计了多少个字符。

#include"huffman.h"//务必注意包含头文件
int compress(const char* filename, const char* outputfile)
{
    char temparray[256];
    unsigned char c;
    long i, j, m, n, f;
    long lowest, pt, filelength;
    FILE* inputfilep, * outputfilep;
    int k, q;
    inputfilep = fopen(filename, "rb");//打开文件夹的原始文件
    if (inputfilep == NULL)
    {
        printf("打开文件失败\n");
        return 0;
    }
    outputfilep = fopen(outputfile, "wb");//打开压缩后存储信息的文件,如果没有就创建
    if (outputfilep == NULL)
    {
        printf("打开文件失败\n");
        return 0;
    }
    filelength = 0;
    while (!feof(inputfilep))//功能是检测流上的文件结束符,如果文件结束,则返回非0值,否则返回0
    {
        fread(&c, 1, 1, inputfilep);//从给定文件inputfilep 读取数据到c
        TreeNode[c].count++;//读文件,统计每个字符出现次数,并且使用是ASCII码
        filelength++;//记录文件的字符总数
    }
    filelength--;//去掉结束符
    TreeNode[c].count--;
    for (i = 0; i < 512; i++) //huffman算法中初始节点的设置
    {
        if (TreeNode[i].count != 0)//512个不一定都用了
            TreeNode[i].letter = i;//映射成ASCII码
        else    //赋值-1是为了避免哈夫曼编码时使用0出现冲突
            TreeNode[i].letter = -1;
        TreeNode[i].parent = -1;
        TreeNode[i].lchild = TreeNode[i].rchild = -1;
    }
    //将节点按出现次数排序(选择排序)
    for (i = 0; i < 256; i++)//每次都向后完成一次
    {
        k = i;//k用来记录变化的位置
        for (q = i + 1; q < 256; q++)//每次都在没有排序的数里进行循环
            if (TreeNode[k].count < TreeNode[q].count)
                k = q;//挑没排序的数里数据最大的
        temp = TreeNode[i];//用t来暂时存储数据(为了与后面的小数据交换)
        TreeNode[i] = TreeNode[k];//把数据最大的赋值
        TreeNode[k] = temp;//把暂存的数据赋值给原来值大的,实现交换
    }
    for (i = 0; i < 256; i++)//统计不同字符的数量
    {
        if (TreeNode[i].count == 0)
            break;
    }//统计出i的数值,以便下面使用
    n = i;
    m = 2 * n - 1;//m是总结点数
    for (i = n; i < m; i++)//使用空结点
    {
        lowest = 999999999;//保证后面的if一定开始执行
        for (j = 0; j < i; j++)
        {
            if (TreeNode[j].parent != -1) 
                continue;
            if (lowest > TreeNode[j].count)
            {
                lowest = TreeNode[j].count;
                pt = j;
            }
        }
        TreeNode[i].count = TreeNode[pt].count;
        TreeNode[pt].parent = i;
        TreeNode[i].lchild = pt;
        lowest = 999999999;
        for (j = 0; j < i; j++)
        {
            if (TreeNode[j].parent != -1) 
                continue;
            if (lowest > TreeNode[j].count)
            {
                lowest = TreeNode[j].count;
                pt = j;
            }
        }
        TreeNode[i].count += TreeNode[pt].count;
        TreeNode[i].rchild = pt;
        TreeNode[pt].parent = i;
    }
    for (i = 0; i < n; i++)//设置字符的编码
    {
        f = i;//从构造数的第一个结点开始,依次对结点进行编排
        TreeNode[i].bits[0] = 0;//初始化
        while (TreeNode[f].parent != -1)//只要有父节点,就进行编排,第一次没有父节点就是根节点
        {
            j = f;
            f = TreeNode[f].parent;
            if (TreeNode[f].lchild == j)//左孩子编0
            {
                j = strlen(TreeNode[i].bits);
                memmove(TreeNode[i].bits + 1, TreeNode[i].bits, j + 1);
                TreeNode[i].bits[0] = '0';
            }
            else//右孩子编1
            {
                j = strlen(TreeNode[i].bits);
                memmove(TreeNode[i].bits + 1, TreeNode[i].bits, j + 1);
                TreeNode[i].bits[0] = '1';
            }
        }
    }
    //下面的就是读原文件的每一个字符,按照设置好的编码替换文件中的字符
    fseek(inputfilep, 0, SEEK_SET);    //将指针定在文件起始位置,没有任何偏移
    fseek(outputfilep, 8, SEEK_SET);   //以8位二进制数为单位,每次移动偏移进行读取
    temparray[0] = 0;
    pt = 8;//后面写入了一共八字节的filelength和pt
    printf("读取将要压缩的文件:%s\n", filename);
    printf("当前文件有:%d字符\n", filelength);
    //压缩文件
    while (!feof(inputfilep))
    {
        c = fgetc(inputfilep);//获取下一个字符(一个无符号字符)
        for (i = 0; i < n; i++)//n是字符种类总数
        {
            if (c == TreeNode[i].letter) //如果读取的字符与我们字符种类映射的ACSCII某一个对应
                break;
        }
        strcat(temparray, TreeNode[i].bits);//那么就把此字符种类的编码追加给temparray来暂存
        j = strlen(temparray);//j用来暂存初始编码总长
        c = 0;//二进制是00000000
        if (j >= 8)//只要存的编码位数大于8,就执行操作写入文件
        {
            for (i = 0; i < 8; i++)//按照八位二进制数转化写入文件一次进行压缩
            {
                if (temparray[i] == '1') 
                {
                    c = c << 1;
                    c = c + 1;
                }//c的二进制数左移并加1
                else 
                    c = c << 1;//c的二进制数字左移
            }//实现二进制的读取和记录
            fwrite(&c, sizeof(char), 1, outputfilep);
            //printf("%s",temparray);//temparray数组存储的是char保存的二进制内容
            pt++;//pt使用的初始值是8,用于记录字节数
            strcpy(temparray, temparray + 8);//temparray的值重新覆盖
        }
    }
    if (j > 0)//当剩余字符数量少于8个时
    {
        strcat(temparray, "00000000");
        for (i = 0; i < 8; i++)
        {
            if (temparray[i] == '1')
            {
                c = c << 1;
                c += 1;//位运算
            }
            else 
                c = c << 1;//对不足的位数进行补零
        }
        fwrite(&c, 1, 1, outputfilep);
        pt++;
    }
    fseek(outputfilep, 0, SEEK_SET);//偏移量为零,重新找到开头位置写入参数
    //使用outputfilep指针来确定文件的开头,将编码信息写入存储文件
    fwrite(&filelength, 4, 1, outputfilep);
    //1是写入的元素大小,sizeof(filelength)是需要写入的长度。这些内容写入ouputfilep
    fwrite(&pt, 4, 1, outputfilep);//写入四字节
    fseek(outputfilep, pt, SEEK_SET);//从给定的开头位置,pt大小(压缩之后的位置),在outputfile里读取
    fwrite(&n, 4, 1, outputfilep);//写一个四字节的数据,要注意pt的累加
    //以上是写入压缩的各类参数:字符种类总数,字符总数
    for (i = 0; i < n; i++)
    {
        tmp = TreeNode[i];
        fwrite(&(TreeNode[i].letter), 1, 1, outputfilep);//写入各类字符
        pt++;
        c = strlen(TreeNode[i].bits);//映射
        fwrite(&c, 1, 1, outputfilep);
        pt++;
        j = strlen(TreeNode[i].bits);
        if (j % 8 != 0)//当位数不满8时,对该数进行补零操作(为了一字节八位数的保存)
        {
            for (f = j % 8; f < 8; f++)
                strcat(TreeNode[i].bits, "0");
        }
        while (TreeNode[i].bits[0] != 0)
        {
            c = 0;//赋值00000000
            for (j = 0; j < 8; j++)
            {
                if (TreeNode[i].bits[j] == '1')
                {
                    c = c << 1;
                    c += 1;//进行位运算
                }
                else 
                    c = c << 1;//不是1就补0
            }
            strcpy(TreeNode[i].bits, TreeNode[i].bits + 8);
            fwrite(&c, 1, 1, outputfilep);//将所得的编码信息写入文件
            pt++;
        }//以上写入各字符种类哈夫曼编码的数据

        TreeNode[i] = tmp;
    }
    fclose(inputfilep);
    fclose(outputfilep);//关闭文件
    printf("\n压缩后文件为:%s\n", outputfile);
    printf("压缩后文件有:%d字符\n", pt + 4);//之前写入了一个四字节的n,所以需要加4
    return pt + 4;//返回压缩成功信息
}

至于位运算,注释给的也很详细,哈夫曼树的编写,就读者自己看看书喽,我们日后再写一篇文章讲一讲哈夫曼树的编写。不然这篇文章就太长啦!

2、解压缩

至于解压缩,也就是读取压缩文件,并把读取结果写入新文件,所以压缩的时候,我们主要使用fseek和fwrite,解压缩的时候,我们就主要使用fseek和fread了。

同样的,几乎每一行代码都给了注释,认真看一看都能看懂。

现在就挑几个不好理解的说一说:

读取的时候,我们需要注意先读取原文件长和压缩文件长,这样我们才能让程序知道,它在读压缩文件的时候应该读多少个,不能读多了,读多了就读到参数了(就是后面存储的哈夫曼编码的数据,那就读多了)

之后是读取参数的数据(获取解压缩的数据,不然没有编码也解压不了)

最后一步就是利用读取到的参数进行转译了,这一步就是写入新文件的过程,还是需要使用fwrite函数的。

至此,我们的哈夫曼解压缩也就实现了。

int uncompress(const char* filename, const char* outputfile)
{
    char temparray[256], bx[256];
    unsigned char c;
    long i, j, m, n, k, f, p, l;
    long filelength;
    int len = 0;
    FILE* inputfilep, * outputfilep;
    char cname[512] = { 0 };
    inputfilep = fopen(filename, "rb");//打开压缩的文件
    if (inputfilep == NULL)
    {
        printf("文件打开失败!");
        return 0;//若打开失败,则输出错误信息
    }

    outputfilep = fopen(outputfile, "wb");//打开新文件,用于解压缩
    if (outputfilep == NULL)
    {

        return 0;
    }
    fseek(inputfilep, 0, SEEK_END);//定下指针位置
    len = ftell(inputfilep);//记录的是inputfilep的位置,也就是字节数
    
    fseek(inputfilep, 0, SEEK_SET);
    printf("将要读取解压的文件:%s\n", filename);
    printf("当前文件有:%d字符\n", len);
    fread(&filelength, sizeof(long), 1, inputfilep);//读取原文件长
    fread(&f, sizeof(long), 1, inputfilep);//f读取的是需要压缩文件的长度,不包括参数
    fseek(inputfilep, f, SEEK_SET);//从文件的f位置后开始读
    fread(&n, sizeof(long), 1, inputfilep);//读取字符种类总数
    for (i = 0; i < n; i++)//读取压缩文件内容并转换成二进制码
    {
        fread(&TreeNode[i].letter, 1, 1, inputfilep);//读取字符(ASCII码)
        fread(&c, 1, 1, inputfilep);//读取字符编码的数据strlen(TreeNode[i].bits))
        p = (long)c;//读的是编码的长度,编码的长度与count反相关
        TreeNode[i].count = p;
        TreeNode[i].bits[0] = 0;//赋初值
        if (p % 8 > 0) //判断字节
            m = p / 8 + 1;
        else 
            m = p / 8;
        for (j = 0; j < m; j++)
        {
            fread(&c, 1, 1, inputfilep);//此步读取的是单个字符类的二进制码
            f = c;
            _itoa(f, temparray, 2);//将f的值转为2进制存入temparray,此时temparray存的是二进制
            f = strlen(temparray);
            for (l = 8; l > f; l--)
            {
                strcat(TreeNode[i].bits, "0");//位数不足,执行补零操作
            }
            strcat(TreeNode[i].bits, temparray);
        }
        TreeNode[i].bits[p] = 0;
    }//接受解压所需要的参数,读取必要的文件

    for (i = 0; i < n; i++)//每次都向后完成一次
    {
        k = i;//k用来记录变化的位置
        for (j = i + 1; j < n; j++)//每次都在没有排序的组里进行循环
            if (strlen(TreeNode[k].bits) > strlen(TreeNode[j].bits))//长度最小的是出现次数最多的
                k = j;//取短的
        tmp = TreeNode[i];//用t来暂时存储数据
        TreeNode[i] = TreeNode[k];//k是略长的值,换到后面
        TreeNode[k] = tmp;//实现交换
    }//排序的目的是快速找到读取解压对应的字母

    p = strlen(TreeNode[n - 1].bits);//p取了一个长度最长的
    fseek(inputfilep, 8, SEEK_SET);//前八位存了两个四字节数据
    m = 0;
    bx[0] = 0;
    while (1)
    {
        while (strlen(bx) < (unsigned int)p)
        {
            fread(&c, 1, 1, inputfilep);//读取压缩的文件
            f = c;//f是一个数,转换十进制再转换二进制丢失了0
            _itoa(f, temparray, 2);//f转换成二进制暂存入temparray
            f = strlen(temparray);
            for (l = 8; l > f; l--)
            {
                strcat(bx, "0");
            }//如果不足八位,补零
            strcat(bx, temparray);
        }
        for (i = 0; i < n; i++)
        {
            if (memcmp(TreeNode[i].bits, bx, TreeNode[i].count) == 0)
                break;//比较字节数是为了保证数组保存的二进制数字相同
        }
        strcpy(bx, bx + TreeNode[i].count);//进行赋值覆盖
        c = TreeNode[i].letter;//写入ASCII码
        fwrite(&c, 1, 1, outputfilep);//写入解码的文件
        m++;
        if (m == filelength)//限定写入只写到原文件的内容,再往后就是存储的参数
            break;
    }//读取压缩文件内容,解压缩的过程
    fclose(inputfilep);
    fclose(outputfilep);
    printf("解压后文件为:%s\n", outputfile);
    printf("解压后文件有:%d字符\n", filelength);
    return filelength;//输出成功信息
}

把压缩的过程理解了,解压缩其实就没什么不能理解的了。

三、尾记-主函数的详细介绍

为什么写尾记呢?因为我们要在这个地方再看一看主函数的功能实现,这样就能让读者们真正的了解这些代码的运行了。
初始化内存就没什么好说的,不操作的话,可能运行不起来

    memset(&TreeNode, 0, sizeof(TreeNode));
    memset(&tmp, 0, sizeof(tmp));

这个代码 while (scanf("%d", &choice) == 1)

 while (scanf("%d", &choice) == 1)
    {......}

则实现了输入字母就退出,当然,我们也可以定向的作其他修改,看读者的需求了。
scanf函数的返回值是正确读取的个数,所以只要我们输入字母,就会退出循环,从而实现想要的功能
swtich语句的case1和case2没什么好讲的,就是基本的模块直接调用。
case3,我们是要实现读取解压缩的文件:

 case 3:
            printf("读取解压后的文件:\n");
            if ((fp = fopen("data.unzip.txt", "rt")) == NULL) {
                printf("读取失败!\n");
                exit(1);
            }
            ch = fgetc(fp);
            while (ch != EOF) {
                putchar(ch);
                ch = fgetc(fp);
            }
            fclose(fp);
            menu();
            break;

那么这里我们就需要打开文件,并且使用 end of file 结束标志。这一个读取还是很简单,不需要itoa转换二进制,因为解压缩的文件已经是字符文件。

case4,我们是要读取哈夫曼编码的一些数据,如图:
C语言实现哈夫曼压缩与解压缩的实现以及读取哈夫曼编码 万文长书,绝对详细哦_第5张图片
那么由于我们书写的文件里已经有参数的数据,我们直接找到压缩的文件,读取一下有多少个字符种类,然后用一个循环把结构体打印出来就可以了。

(当然,笔者认为自己使用的这种方法是一个笨方法,我们也可以直接定义一个全局变量,在压缩的时候就存储一下字符种类个数)

这里的找到文件的操作,需要使用SEEK_SET的一些知识,上面的代码已经都给出了使用的说明和注释。

 case 4:
            if ((fp = fopen("data.zip.txt", "rb")) == NULL)
            {
                printf("读取失败!\n");
                exit(1);
            }
            fseek(fp, 4, SEEK_SET);
            fread(&f, sizeof(long), 1, fp);
            fseek(fp, f, SEEK_SET);
            fread(&n, sizeof(long), 1, fp);//读取字符种类总数
            for (int i = 0; i < n; i++)
            {
                printf("字符:%-5c频数:%-6ld哈夫曼编码:%-20s", TreeNode[i].letter, TreeNode[i].count, TreeNode[i].bits);
                count++;
                if (count % 4 == 0)
                    printf("\n");
            }
            menu();
            break;

那么,现在,我们要实现的功能就都已经书写了,如果有小伙伴说,我想看到二进制的数据,怎么才能看到呢?

二进制文件,我们是打不开的,看到的都是乱码,不过我们可以使用itoa函数外加一个辅助数组,把写入到压缩文件里的容器一个一个读出来,这样得到的也是一个可以被我们看到的二进制的一些数字(但注意,这不是二进制哦)。有兴趣的小伙伴可以自己实现一下。

今天的分享就到这里了,希望老铁们有所收获,我们下次再见。

你可能感兴趣的:(huffman,tree,信息压缩,编码学,c语言,霍夫曼树)