Haffman编码实现文本压缩-C语言-万字长文,绝对详细

目录

前言

一、实验目的

二、实验要求

三、设计思想

1 编码

1.1 生成Haffman编码

(1)统计字符频率

(2)构造Haffman树

1.2 文本编码

2 译码

2.1 读入译码信息

2.2 文本译码

2.3 测试结果

四、源码


前言:

        这是我在CSDN的第一篇文章,第一次不知道怎么写,所以直接把实验报告搬上来了。但是目前这篇文章的阅读量很高,并且有帮到了好几位小伙伴,同时也得到了他们对我创作内容的认可,我受宠若惊,所以决定更新一下这篇文章,以更好地帮助大家解决问题。

        我猜测许多小伙伴也是要写报告,所以我并不打算修改文章的整体框架,依旧以一份报告的格式来呈现,仅做小部分的改动:将图片型代码转为可以直接复制的代码块;修改部分措辞,便于理解;增加更多解释性语言,尽量把整个流程阐述地更加清楚。

        由于当初使用的是富文本编辑器来写的文章,所以无法使用Markdown的相关语法,排版方面可能有些许粗糙,还请见谅。

        我希望达到的目的是,仅仅通过这一篇博客,你便可以完全理解使用Haffman编码实现文本压缩中生成编码、压缩并写入二进制文件、读取文件并解码的整体流程,并能够轻易地在我的框架指导下写出自己的程序,不会出现修改时无从下手的情况。

目前有好几位小伙伴反馈了一些问题,汇总如下:

程序没有输出怎么办?

  1. 程序仅支持ASCII码字符,即英文文档。如果出现控制台没有任何输出,可以先去排查下文章中是否有中文的,。、?【】();等字符。可以在文章末尾源码部分下载我上传的测试文本,如果程序正常的话,那应该就是你源文本有中文字符了(目前有好几位小伙伴都是因为这个原因)。
  2. 如果控制台输出 “Failed to open files!” ,则需要检查一下在main函数开始是否修改了文件路径。相对路径注意将源文本与程序放在同目录下;绝对路径注意是否含有中文,某些编译器可能会出问题。同时,仅需创建"original_text.txt"源文本即可,压缩后的文本与解码后生成的文本会自动生成在同目录下。

压缩后的文件如何打开?

        压缩后生成的"compressed_text.txt"文件,虽然后缀是txt文本文档,但其实是二进制文件,无法通过普通的文本编辑器打开。大家可以自行去网上查询有关的二进制文本编辑器来打开(推荐以十六进制显示)。

        我使用的是VScode中的Hex Editor编辑器,十分方便。

如果有其他问题,或者说上述问题的其他情况,可以随时评论区、私信我交流,一起解决问题。

同时我的主页还有更有优质文章,希望可以帮到你,感谢大家的点赞、收藏和关注~

以下为正文部分:


一、实验目的

哈夫曼编码是一种以哈夫曼树(最优二叉树,带权路径长度最小的二叉树)为基础变长编码方法。其基本思想是:将使用次数多的代码转换成长度较短的编码,而使用次数少的采用较长的编码,并且保持编码的唯一可解性。在计算机信息处理中,经常应用于数据压缩。是一种一致性编码法(又称"熵编码法"),用于数据的无损压缩。

要求实现一个完整的哈夫曼编码与译码系统。


二、实验要求

  1. 从文件中读入任意一篇英文文本文件,分别统计英文文本文件中各字符(包括标点符号和空格)的使用频率;
  2. 根据已统计的字符使用频率构造哈夫曼编码树,并给出每个字符的哈夫曼编码(字符集的哈夫曼编码表);
  3. 将文本文件利用哈夫曼树进行编码,存储成二进制压缩文件(哈夫曼编码文件);
  4. 计算哈夫曼编码文件的压缩率
  5. 将哈夫曼编码文件译码为文本文件,并与原文件进行比较。

三、设计思想

程序主要分为编码与译码两部分,总体思路如下:

Haffman编码实现文本压缩-C语言-万字长文,绝对详细_第1张图片 图3.1 程序总体思路

相关存储结构及函数定义如下:

表3.1 函数定义

函数声明

作用

typedef struct HaffmanNode HaffmanNode, * HaffmanTree;

字符Haffman结点的存储结构

typedef struct Dic Dic;

存放字符及其编码结果的字典

typedef struct HeapStruct HeapStruct, * Heap;

堆的结构

HaffmanTree createHaffmanNode();

生成Haffman树结点

HaffmanTree createHaffmanTree();

构造Haffman树

Heap createHeap(int maxsize);

创建容量为maxsize的堆

void traverseHeap(Heap H);

堆的遍历

void insertMinHeap(Heap H, HaffmanTree data);

最小堆的插入

HaffmanTree deleteMinHeap(Heap H);

最小堆的删除

int countChFrequency(FILE* fp);

统计字符出现频率

void genHaffmanCode(HaffmanTree root);

生成Haffman编码

void encoded(FILE* source, FILE* output);

编码,生成二进制文件

void decoded(FILE* source, FILE* output);

解码,生成文本文件

1 编码

1.1 生成Haffman编码

对一棵具有n个叶子结点的Haffman树,将树中的每个左分支赋0,右分支赋1,则从根到每个叶子的通路上各个分支的赋值分别构成一个二进制串,该二进制串就称为哈夫曼编码。哈夫曼编码是最优前缀编码,能使各种报文对应的二进制串的平均长度最短。

因此,为生成Haffman编码,首先要对文本中每个字符出现的频率进行统计,并利用堆的结构构造Haffman树,并生成编码。

Haffman树结点的结构定义如下。

/* 字符Haffman结点 */
typedef struct HaffmanNode
{
    char character;             // 字符
    long count;                 // 出现频数
    char code[MAXBIT];          // 编码
    struct HaffmanNode *lchild; // 左孩子
    struct HaffmanNode *rchild; // 右孩子
    struct HaffmanNode *parent; // 父结点
} HaffmanNode, *HaffmanTree;

由于ASCII码值有128个字符,因此创建一个大小为128的Haffman结点指针数组,其下标分别对应各个字符的ASCII码值,并创建全局变量dic作为字典。

/* 存放字符及其编码结果的结构体 */
typedef struct Dic
{
    HaffmanTree charNode[128]; // 字符结点数组,起到字典的作用(只适用于ASCII码表)
} Dic;

(1)统计字符频率

使用countChFrequency函数进行统计,具体流程如下:

Haffman编码实现文本压缩-C语言-万字长文,绝对详细_第2张图片 图3.2 统计字符流程题

(2)构造Haffman树

最小堆的根节点为所有元素中最小值,操作简单,时间复杂度低,用于构造Haffman树十分合适,因此使用最小堆的存储结构来构造。(没有学过堆的小伙伴可以采用别的方案,这里不是重点)

构造哈夫曼树的算法步骤如下:

① 预处理操作:由所有在文本中出现过的字符节点生成最小堆,并以各字符出现的频率(count)作为其权重。

② 依次从最小堆里取出两个结点(孩子结点),并生成一个新的父结点,将其插入最小堆中。其中父结点的count值为两孩子的count值求和。

③ 重复操作②,直至最小堆中只有一个结点为止。

④ 取出最小堆中结点,即为Haffman树的根节点root。

(3)生成Haffman编码

下面对各个字符进行编码,编码原则是:从树的根节点开始,进入左子树则编码加0,进入右子树则编码加1,就可以得到对应字符的二进制编码。

Haffman编码实现文本压缩-C语言-万字长文,绝对详细_第3张图片 图3.3 生成Haffman编码示例

具体编程实现采用从叶子结点逆向遍历至根结点的方法:

即对于每一个叶子结点来说,如果它是父结点的左儿子,则将'0'存入临时字符数组中,如果是右儿子,则存入'1',如此循环直至到达根节点。

最后将临时字符数组逆置,便得到字符的编码,将其存入对应结点的code字符数组中。

结果示例(部分):

Haffman编码实现文本压缩-C语言-万字长文,绝对详细_第4张图片 图3.4 Haffman编码结果部分示例

1.2 文本编码

接下来便是将source文件里的文本按照编码规则以二进制的形式写入到output文件中。

采取以下策略:

将存储内容分为三部分:

        1. 文件基本信息

        2. 正文

        3. 字典(Haffman编码表)

结构如图所示,其中每一个小方格代表一个字节。

Haffman编码实现文本压缩-C语言-万字长文,绝对详细_第5张图片 图3.5 二进制文本存储结构

(1)写入操作

三部分内容写入方式大同小异,以正文内容写入为例。

首先需要一个暂存编码的字符数组tempCode。读取正文内容,不断地将字符对应的编码追加到tempCode的末尾。当发现tempCode的长度大于8位时,将进行写入(逐字节写入)操作。

声明一个char类型字符ch(大小为1个字节),首先将其置为0,即将二进制码置为0000 0000。从tempCode的首字符开始读取8位,如果是'1',则将ch左移一位,然后+1;如果是'0',则仅将ch左移一位。然后将ch写入output文件中即可,同时将tempCode中的内容向前移动8位,如此循环。

当文本读取完毕,发现tempCode中还有不足8位的编码,则在后面补0至8位,然后写入。

这里用到了位操作,估计有部分小伙伴不太理解,这里稍加解释,并不严谨,仅用于更好地辅助理解。(理解的小伙伴可以直接看代码)

我们采用的方案是逐字节写入,首先我们便需要获得一个1字节大小的“容器”,其实在计算机的眼里,没有什么int型、char型,不过是不同的连续大小的空间而已,int型就是4个字节,char型就是1个字节。

不知道怎么往文本中写二进制编码?但我们会写入char类型的字符呀。此处我们声明一个char类型的字符ch = 0,其对应的编码就是0000 0000。此时我们将这个char字符写入文本时,其实就是写入了8个为0的比特位。

那么现在我们就有办法写入0、1比特位了,如何将ch修改为我们想要的形式呢?这里就要用到左移加1的操作了。

ch << 1代表将整个字节的二进制位向左移动1位,溢出的舍弃,右边补0;ch +1 中加的是二进制下的1。这样,我们便可以在二进制串中写入0和1,也就可以构造我们的Haffman编码了。

即将ch“改造”成我们想要的二进制形式,再按照普通的文本写入方式即可。

此部分核心代码如下:

    // 写入文本
    char ch = fgetc(source);
    while (ch != EOF)
    {
        strcat(tempCode, dic.charNode[ch]->code); // 将字符的编码追加给tempCode暂存
        lenTempCode = strlen(tempCode);
        ch = 0;                  //二进制为00000000,作为写入文本的二进制编码的临时容器
        while (lenTempCode >= 8) // 暂存编码位数大于8时,执行写入操作
        {
            for (i = 0; i < 8; i++) // 八位二进制数写入文件一次
            {
                if (tempCode[i] == '1') // ch的二进制数左移并加1
                {
                    ch = ch << 1;
                    ch = ch + 1;
                }
                else
                    ch = ch << 1; // ch的二进制数左移
            }
            fwrite(&ch, sizeof(char), 1, output);
            lenCompressedFile++;
            strcpy(tempCode, tempCode + 8); // temoCode的值重新覆盖
            lenTempCode = strlen(tempCode);
        }
        ch = fgetc(source);
    }
    if (lenTempCode > 0) // 若最后不满8位,则补0
    {
        ch = 0;
        strcat(tempCode, "00000000");
        for (i = 0; i < 8; i++)
        {
            if (tempCode[i] == '1') // ch的二进制数左移并加1
            {
                ch = ch << 1;
                ch = ch + 1;
            }
            else
                ch = ch << 1; // ch的二进制数左移
        }
        fwrite(&ch, sizeof(char), 1, output); // 将最后一个字符写入
        lenCompressedFile++;
    }

同理,将文本基本信息以及字典也写入output文件中(原理相同,代码详细,不多赘述),最终得到二进制文本,实现了压缩的目的,示例如下。

Haffman编码实现文本压缩-C语言-万字长文,绝对详细_第6张图片 图3.6 二进制文本示例

2 译码

想要知道如何译码,就要知道是如何进行编码的,此时我们刚才在文件中存储的字典便派上了用场,就可以像人查字典一样,将二进制序列逐个与字典匹配,进行“翻译”。

2.1 读入译码信息

首先需要构造字典,将字符、对应Haffman编码存入。

字典结构如下图所示:

Haffman编码实现文本压缩-C语言-万字长文,绝对详细_第7张图片 图3.7 字典结构

操作如下:

首先读入文本开头12个字节的3个文件基本信息,依据信息定位至字典存储位置,进行读取。对于每一个字符结点来说,首先读入1个字节,值就是其ASCII码值。然后读取其编码长度,并在编码中读取对应位数,然后转换为字符数组并存入对应结点即可。

核心代码如下:

    // 读取相关文本信息
    int textLen, binLen, types, biFileLen, codeLen, byteNum;
    fseek(source, 0, SEEK_END);
    biFileLen = ftell(source); // 二进制文件总大小
    fseek(source, 0, SEEK_SET);
    fread(&textLen, sizeof(int), 1, source); // 源文本大小
    fread(&binLen, sizeof(int), 1, source);  // 压缩后的文件大小(不含字典)
    fread(&types, sizeof(int), 1, source);   // 字符种类数

    // 读取字典并转换成二进制码(char数组)
    fseek(source, binLen, SEEK_SET);
    for (i = 0; i < types; i++)
    {

        fread(&dic_decode.charNode[i]->character, 1, 1, source); // 读取字符(ASCII码)
        fread(&ch, 1, 1, source);
        dic_decode.charNode[i]->count = (long)ch;  // 读取字符编码长度 strlen(code)
        if (dic_decode.charNode[i]->count % 8 > 0) // 当前字符的编码占了几个字节
            byteNum = dic_decode.charNode[i]->count / 8 + 1;
        else
            byteNum = dic_decode.charNode[i]->count / 8;

        for (j = 0; j < byteNum; j++)
        {
            fread(&ch, 1, 1, source); // 此步读取的是单个字节对应的ASCII码
            temp = (int)ch;
            _itoa(temp, tempCode, 2); // 将ch的值转为二进制字符串存入tempCode
            temp = strlen(tempCode);
            for (k = 8; k > temp; k--)
            {
                strcat(dic_decode.charNode[i]->code, "0"); //位数不足,执行补零操作
            }
            strcat(dic_decode.charNode[i]->code, tempCode);
        }
        dic_decode.charNode[i]->code[dic_decode.charNode[i]->count] = 0; // 放上/0
    }

如此,便根据文本内存储的基本信息与字典,还原了该文本Haffman编码。

2.2 文本译码

由于Haffman编码不等长,所以编码的匹配策略对匹配效率影响很大。首先对字典进行按照编码长度升序排序,在之后遍历时便可以以最小代价匹配出正确字符。

采用如下的匹配策略:

首先找出各字符编码中的最长编码数maxLen,声明字符数组codeToBeDecoded存放待译码编码。逐字节将二进制文件中的内容读取到codeToBeDecoded数组中(其中需要进行十进制转二进制字符串,并补0操作)。当发现codeToBeDecoded数组的长度大于maxLen时,将其编码与字典各字符的编码进行逐个内存匹配。匹配成功后,将译码得到的字符写入output文件,并将codeToBeDecoded中编码向前覆盖,如此循环。

当写入的字符数与原文本字符数相同时,则译码完成,此时再往后读取便是字典的信息。

如此,我们便得到了译码后的结果。匹配过程的流程图如下:

Haffman编码实现文本压缩-C语言-万字长文,绝对详细_第8张图片 图3.8 译码匹配流程图

同时,将Haffman编码读入至字典中后,我们可以按照其长度对其进行升序排序,这样在译码查找的时候就可以从小到大依次匹配,方便处理、提高效率。

我的代码中使用的是冒泡排序,其时间复杂度为O(n^2),大家可以根据自己需要进行优化。

相关的核心代码如下:

int maxLen = strlen(dic_decode.charNode[types - 1]->code); // 最长编码数
    fseek(source, 12, SEEK_SET);                               // 指向正文开头
    tempCode[0] = 0;
    while (1)
    {
        while (strlen(codeToBeDecoded) < maxLen) // 确保一定找到一个字符
        {
            fread(&ch, 1, 1, source);
            temp = (int)ch;
            _itoa(temp, tempCode, 2); // 将ch的值转为二进制字符串存入tempCode
            temp = strlen(tempCode);
            for (k = 8; k > temp; k--) // 转十进制再转二进制会丢掉0,进行补0操作
                strcat(codeToBeDecoded, "0");
            strcat(codeToBeDecoded, tempCode);
        }
        // Haffman码与字符的匹配
        for (i = 0; i < types; i++)
        {
            if (memcmp(dic_decode.charNode[i]->code, codeToBeDecoded, dic_decode.charNode[i]->count) == 0)
                break;
        }
        strcpy(codeToBeDecoded, codeToBeDecoded + dic_decode.charNode[i]->count); // 将已经解译的编码覆盖
        ch = dic_decode.charNode[i]->character;
        fwrite(&ch, 1, 1, output); //写入解码的文件
        writeCount++;
        if (writeCount == textLen) // 限定写入只写到正文内容,再往后存储的就是字典
            break;
    }

此处匹配方案使用的是memcmp函数,进行内存匹配。

2.3 测试结果

以”瓦尔登湖(英文版).txt”为例,运行结果如下:

Haffman编码实现文本压缩-C语言-万字长文,绝对详细_第9张图片

图4.1 测试结果

对于一篇59w个字符的小说,压缩为55.65%,压缩耗时0.064秒,译码耗时0.069秒。


四、源码

在此给出整个程序的源代码,同时我也将.c文件与文章中”瓦尔登湖(英文版).txt”一同打包上传到CSDN资源板块,感兴趣的小伙伴可以直接下载下来直接使用。

资源地址:

Haffman编码实现文本压缩的源码及文本示例-C文档类资源-CSDN文库https://download.csdn.net/download/m15253053181/87166548程序全部源代码:

/*
* 程序:使用Haffman编码进行英文文档的编码、压缩、译码全过程
* 作者:友人帐_
* 文章地址:https://blog.csdn.net/m15253053181/article/details/127457700?spm=1001.2014.3001.5501
* 个人主页:https://blog.csdn.net/m15253053181?type=blog
* 希望可以帮助到大家,也欢迎进入我的主页查看更多优质文章~
*/

#define _CRT_SECURE_NO_WARNINGS
#include 
#include 
#include 
#include 

#define MAXSIZE 100
#define MAXBIT 256

/* 字符Haffman结点 */
typedef struct HaffmanNode
{
    char character;             // 字符
    long count;                 // 出现频数
    char code[MAXBIT];          // 编码
    struct HaffmanNode *lchild; // 左孩子
    struct HaffmanNode *rchild; // 右孩子
    struct HaffmanNode *parent; // 父亲
} HaffmanNode, *HaffmanTree;

/* 存放字符及其编码结果的结构体 */
typedef struct Dic
{
    HaffmanTree charNode[128]; // 字符结点数组,起到字典的作用(只适用于ASCII码表)
} Dic;

/* 堆的结构 */
typedef struct HeapStruct
{
    HaffmanTree *elem; // 存放堆元素(Haffman结点)的数组
    int size;          // 当前元素个数
    int capacity;      // 存储容量
} HeapStruct, *Heap;

HaffmanTree createHaffmanNode();              // 生成Haffman树结点
HaffmanTree createHaffmanTree();              // 构造Haffman树
Heap createHeap(int maxsize);                 // 创建容量为maxsize的堆
void traverseHeap(Heap H);                    // 堆的遍历
void insertMinHeap(Heap H, HaffmanTree data); // 最小堆的插入
HaffmanTree deleteMinHeap(Heap H);            // 最小堆的删除

int countChFrequency(FILE *fp);           // 统计字符出现频率
void genHaffmanCode(HaffmanTree root);    // 生成Haffman编码
void encoded(FILE *source, FILE *output); // 编码
void decoded(FILE *source, FILE *output); // 解码

Dic dic; // 字典,全局变量,用于编码压缩

int main()
{
    float begintime, endtime;
    // 编码
    FILE *originalText = fopen("./original_text.txt", "rb");     // 源文件
    FILE *compressedText = fopen("./compressed_text.txt", "wb"); // 压缩后的二进制文件
    if (!(originalText && compressedText))
        printf("Failed to open files!\n ");
    else
    {
        begintime = clock(); //计时开始
        encoded(originalText, compressedText);
        endtime = clock(); //计时结束
        printf("Program time consuming: %fs\n\n", (endtime - begintime) / ((double)CLOCKS_PER_SEC));
    }
    fclose(originalText);
    fclose(compressedText);

    // 解码
    FILE *binText = fopen("./compressed_text.txt", "rb");  // 压缩后的二进制文件
    FILE *decodedText = fopen("./decoded_text.txt", "wb"); // 解码后的文件
    if (!(binText && decodedText))
        printf("Failed to open files!\n");
    else
    {
        begintime = clock(); //计时开始
        decoded(binText, decodedText);
        endtime = clock(); //计时结束
        printf("Program time consuming: %fs\n\n", (endtime - begintime) / ((double)CLOCKS_PER_SEC));
    }
    fclose(binText);
    fclose(decodedText);

    system("pause");
    return 0;
}

/*---------------------------------------- 编码 ----------------------------------------*/
void encoded(FILE *source, FILE *output)
{
    int i, j;
    int types = 0;
    int textLength = countChFrequency(source); // 计算字符频率
    HaffmanTree root = createHaffmanTree();    // 由字符频率构造Haffman树
    genHaffmanCode(root);                      // 编码

    // 输出编码结果
    printf("   *------------> Huffman Code Table <-----------*\n");
    printf("    ____________________________________________\n      charcater |  frequency  |     code\n");
    printf("    ____________|_____________|_________________\n");
    for (i = 0; i < 128; i++)
        if (dic.charNode[i]->count > 0)
        {
            if (dic.charNode[i]->character == 10) // 换行符
                printf("       \\n\t|%8ld     |   %s\n", dic.charNode[i]->count, dic.charNode[i]->code);
            else if (dic.charNode[i]->character == 0) // 结束符
                printf("       \\0\t|%8ld     |   %s\n", dic.charNode[i]->count, dic.charNode[i]->code);
            else if (dic.charNode[i]->character == 9) // 制表符
                printf("       \\t\t|%8ld     |   %s\n", dic.charNode[i]->count, dic.charNode[i]->code);
            else if (dic.charNode[i]->character == 13) // 回车
                printf("       \\r\t|%8ld     |   %s\n", dic.charNode[i]->count, dic.charNode[i]->code);
            else if (dic.charNode[i]->character == 32) // 空格
                printf("     space      |%8ld     |   %s\n", dic.charNode[i]->count, dic.charNode[i]->code);
            else
                printf("       %c\t|%8ld     |   %s\n", dic.charNode[i]->character, dic.charNode[i]->count, dic.charNode[i]->code);
            types++;
        }
    printf("   _____________|_____________|_________________\n\n");

    // 压缩文件准备操作
    fseek(source, 0, SEEK_SET);  // 找到源文件开头
    fseek(output, 12, SEEK_SET); // 找到输出文件的12个字节后的位置,前12字节存相关信息
    int lenCompressedFile = 12;  // 压缩文件的长度,单位字节
    char tempCode[MAXBIT] = {0}; // 临时存放字符编码
    int lenTempCode = 0;         // 暂存的字符编码长度

    // 写入文本
    char ch = fgetc(source);
    while (ch != EOF)
    {
        strcat(tempCode, dic.charNode[ch]->code); // 将字符的编码追加给tempCode暂存
        lenTempCode = strlen(tempCode);
        ch = 0;                  //二进制为00000000,作为写入文本的二进制编码的临时容器
        while (lenTempCode >= 8) // 暂存编码位数大于8时,执行写入操作
        {
            for (i = 0; i < 8; i++) // 八位二进制数写入文件一次
            {
                if (tempCode[i] == '1') // ch的二进制数左移并加1
                {
                    ch = ch << 1;
                    ch = ch + 1;
                }
                else
                    ch = ch << 1; // ch的二进制数左移
            }
            fwrite(&ch, sizeof(char), 1, output);
            lenCompressedFile++;
            strcpy(tempCode, tempCode + 8); // temoCode的值重新覆盖
            lenTempCode = strlen(tempCode);
        }
        ch = fgetc(source);
    }
    if (lenTempCode > 0) // 若最后不满8位,则补0
    {
        ch = 0;
        strcat(tempCode, "00000000");
        for (i = 0; i < 8; i++)
        {
            if (tempCode[i] == '1') // ch的二进制数左移并加1
            {
                ch = ch << 1;
                ch = ch + 1;
            }
            else
                ch = ch << 1; // ch的二进制数左移
        }
        fwrite(&ch, sizeof(char), 1, output); // 将最后一个字符写入
        lenCompressedFile++;
    }

    // 写入源文件大小、压缩后文件大小和字符种类数(bytes)
    fseek(output, 0, SEEK_SET);                         // 输出文件指针指向开头,写入参数
    fwrite(&textLength, sizeof(int), 1, output);        // 写入源文件大小
    fwrite(&lenCompressedFile, sizeof(int), 1, output); // 写入压缩后文件大小(包括12字节的文本信息及正文,不包括字典)
    fwrite(&types, sizeof(int), 1, output);             // 写入字符种类数

    // 写入字典
    fseek(output, lenCompressedFile, SEEK_SET); // 找到output的末尾
    HaffmanTree tempNode;
    char codeLenBit;
    for (i = 0; i < 128; i++)
    {
        if (dic.charNode[i]->count > 0)
        {
            tempNode = dic.charNode[i];
            lenTempCode = strlen(tempNode->code);
            fwrite(&(tempNode->character), 1, 1, output); // 写入字符ASCII码
            lenCompressedFile++;
            codeLenBit = lenTempCode;
            fwrite(&codeLenBit, 1, 1, output); // 写入字符编码的长度-1个字节就够了
            lenCompressedFile++;

            while (lenTempCode % 8 != 0) // 当编码不够整数字节时,补0
            {
                strcat(tempNode->code, "0");
                lenTempCode = strlen(tempNode->code);
            }

            while (tempNode->code[0] != 0)
            {
                ch = 0;
                for (j = 0; j < 8; j++)
                {
                    if (tempNode->code[j] == '1')
                    {
                        ch = ch << 1;
                        ch += 1;
                    }
                    else
                        ch = ch << 1;
                }
                strcpy(tempNode->code, tempNode->code + 8);
                fwrite(&ch, 1, 1, output); // 将所得的编码信息写入文件
                lenCompressedFile++;
            }
        }
    }
    printf("\n>>> Original File Information\n");
    printf("- filename: original_text.txt\n- file length: %ld bytes\n", textLength);
    printf("\n>>> Compressed File Information\n");
    printf("- filename: decoded_text.txt\n- file length: %ld bytes\n", lenCompressedFile);
    printf("\nThe compress has finished! Compression ratio: %.2f%%\n", (float)lenCompressedFile / (float)textLength * 100);
}

/*---------------------------------------- 解码 ----------------------------------------*/
void decoded(FILE *source, FILE *output)
{
    int i, j, k;
    int temp;
    unsigned char ch;
    int writeCount = 0;                 // 写入字符数的统计
    char tempCode[MAXBIT] = {0};        // 临时存放字符编码
    char codeToBeDecoded[MAXBIT] = {0}; // 存放待译码的二进制序列(1个char)
    Dic dic_decode;                     // 解码用字典

    // 对dic进行初始化
    for (i = 0; i < 128; i++)
    {
        dic_decode.charNode[i] = (HaffmanTree)malloc(sizeof(HaffmanNode)); // 申请空间
        dic_decode.charNode[i]->character = 0;                             // 此时下标无含义
        dic_decode.charNode[i]->count = 0;                                 // 此时用于存放编码长度 strlen(code)
        dic_decode.charNode[i]->lchild = NULL;
        dic_decode.charNode[i]->rchild = NULL;
        dic_decode.charNode[i]->parent = NULL;
        dic_decode.charNode[i]->code[0] = 0;
    }

    // 读取相关文本信息
    int textLen, binLen, types, biFileLen, codeLen, byteNum;
    fseek(source, 0, SEEK_END);
    biFileLen = ftell(source); // 二进制文件总大小
    fseek(source, 0, SEEK_SET);
    fread(&textLen, sizeof(int), 1, source); // 源文本大小
    fread(&binLen, sizeof(int), 1, source);  // 压缩后的文件大小(不含字典)
    fread(&types, sizeof(int), 1, source);   // 字符种类数

    // 读取字典并转换成二进制码(char数组)
    fseek(source, binLen, SEEK_SET);
    for (i = 0; i < types; i++)
    {

        fread(&dic_decode.charNode[i]->character, 1, 1, source); // 读取字符(ASCII码)
        fread(&ch, 1, 1, source);
        dic_decode.charNode[i]->count = (long)ch;  // 读取字符编码长度 strlen(code)
        if (dic_decode.charNode[i]->count % 8 > 0) // 当前字符的编码占了几个字节
            byteNum = dic_decode.charNode[i]->count / 8 + 1;
        else
            byteNum = dic_decode.charNode[i]->count / 8;

        for (j = 0; j < byteNum; j++)
        {
            fread(&ch, 1, 1, source); // 此步读取的是单个字节对应的ASCII码
            temp = (int)ch;
            _itoa(temp, tempCode, 2); // 将ch的值转为二进制字符串存入tempCode
            temp = strlen(tempCode);
            for (k = 8; k > temp; k--)
            {
                strcat(dic_decode.charNode[i]->code, "0"); //位数不足,执行补零操作
            }
            strcat(dic_decode.charNode[i]->code, tempCode);
        }
        dic_decode.charNode[i]->code[dic_decode.charNode[i]->count] = 0; // 放上/0
    }

    // 按Haffman码长度从小到大排序,便于译码时查找 - 冒泡
    HaffmanTree tmp;
    for (i = 0; i < types; i++)
    {
        for (j = 0; j < types - i - 1; j++)
        {
            if (strlen(dic_decode.charNode[j]->code) > strlen(dic_decode.charNode[j + 1]->code))
            {
                tmp = dic_decode.charNode[j];
                dic_decode.charNode[j] = dic_decode.charNode[j + 1];
                dic_decode.charNode[j + 1] = tmp;
            }
        }
    }

    int maxLen = strlen(dic_decode.charNode[types - 1]->code); // 最长编码数
    fseek(source, 12, SEEK_SET);                               // 指向正文开头
    tempCode[0] = 0;
    while (1)
    {
        while (strlen(codeToBeDecoded) < maxLen) // 确保一定找到一个字符
        {
            fread(&ch, 1, 1, source);
            temp = (int)ch;
            _itoa(temp, tempCode, 2); // 将ch的值转为二进制字符串存入tempCode
            temp = strlen(tempCode);
            for (k = 8; k > temp; k--) // 转十进制再转二进制会丢掉0,进行补0操作
                strcat(codeToBeDecoded, "0");
            strcat(codeToBeDecoded, tempCode);
        }
        // Haffman码与字符的匹配
        for (i = 0; i < types; i++)
        {
            if (memcmp(dic_decode.charNode[i]->code, codeToBeDecoded, dic_decode.charNode[i]->count) == 0)
                break;
        }
        strcpy(codeToBeDecoded, codeToBeDecoded + dic_decode.charNode[i]->count); // 将已经解译的编码覆盖
        ch = dic_decode.charNode[i]->character;
        fwrite(&ch, 1, 1, output); //写入解码的文件
        writeCount++;
        if (writeCount == textLen) // 限定写入只写到正文内容,再往后存储的就是字典
            break;
    }
    printf("The decoded has finish!\n");
}

/*---------------------------------------- 编码所需操作 ----------------------------------------*/
/* 统计字符频率 */
int countChFrequency(FILE *fp)
{
    int i;
    int length = 0; // 统计文本的长度

    // 对dic进行初始化
    for (i = 0; i < 128; i++)
    {
        dic.charNode[i] = (HaffmanTree)malloc(sizeof(HaffmanNode)); // 申请空间
        dic.charNode[i]->character = i;                             // 将下标与ASCII码对应
        dic.charNode[i]->count = 0;
        dic.charNode[i]->lchild = NULL;
        dic.charNode[i]->rchild = NULL;
        dic.charNode[i]->parent = NULL;
    }
    // 对字符进行统计
    char ch = fgetc(fp);
    while (ch != EOF)
    {
        dic.charNode[(int)ch]->count++;
        ch = fgetc(fp);
        length++;
    }
    return length;
}

/* 生成Haffman编码 */
void genHaffmanCode(HaffmanTree root)
{
    int i, j;
    int count = 0;
    char tempCode[256]; // 临时存放字符编码
    HaffmanTree pMove = NULL;

    // 生成Haffman编码
    for (i = 0; i < 128; i++)
    {
        if (dic.charNode[i]->count > 0) // 对于出现过的字符,即Haffman树的叶子结点
        {
            pMove = dic.charNode[i];
            // 从叶子逆序到根,将编码逆序存放在tempCode中
            while (pMove->parent)
            {
                if (pMove->parent->lchild == pMove)
                    tempCode[count] = '0'; // 左子树为0
                else
                    tempCode[count] = '1'; // 右子树为1
                count++;
                pMove = pMove->parent;
            }
            // 将tempCode编码逆序存放在字符结点中
            for (j = 0; j < count; j++)
                dic.charNode[i]->code[j] = tempCode[count - j - 1];
            dic.charNode[i]->code[j] = '\0';
            count = 0;
        }
    }
}

/* 生成Haffman树结点 */
HaffmanTree createHaffmanNode()
{
    HaffmanTree node = (HaffmanTree)malloc(sizeof(HaffmanNode));
    node->character = 0;
    node->count = 0;
    node->lchild = NULL;
    node->rchild = NULL;
    node->parent = NULL;
    return node;
}

/* 构造Haffman树 */
HaffmanTree createHaffmanTree()
{
    int i;

    // 由字典构造最小堆
    Heap H = createHeap(MAXSIZE);
    for (i = 0; i < 128; i++)
    {
        if (dic.charNode[i]->count > 0)
        {
            insertMinHeap(H, dic.charNode[i]);
        }
    }

    // 构造Haffman树
    while (H->size > 1)
    {
        // 创建新结点,值为两最小结点的和
        HaffmanTree newNode = createHaffmanNode();
        HaffmanTree left = deleteMinHeap(H);
        HaffmanTree right = deleteMinHeap(H);
        newNode->count = left->count + right->count;
        newNode->lchild = left;
        newNode->rchild = right;
        left->parent = newNode;
        right->parent = newNode;

        // 将新结点插入堆中
        insertMinHeap(H, newNode);
    }
    HaffmanTree root = deleteMinHeap(H);
    return root;
}

/* 创建容量为maxsize的堆 */
Heap createHeap(int maxsize)
{
    Heap H = (Heap)malloc(sizeof(struct HeapStruct));
    H->elem = (HaffmanTree *)malloc(sizeof(HaffmanTree) * (maxsize + 1)); // 从下标为1存放堆元素
    H->size = 0;
    H->capacity = maxsize;
    return H;
}

/* 堆的遍历 */
void traverseHeap(Heap H)
{
    int i;

    if (H->size == 0)
    {
        printf("Heap is empty!\n");
        return;
    }

    for (i = 1; i <= H->size; i++)
    {
        printf("%d ", H->elem[i]->count);
    }
    printf("\n");
}

/* 最小堆的插入 */
void insertMinHeap(Heap H, HaffmanTree data)
{
    int i;

    if (H->size == H->capacity) // 堆满
    {
        printf("Min heap is full!\n");
        return;
    }

    i = H->size + 1; // i指向插入元素后堆中最后一个元素的位置
    H->size++;

    while (1)
    {
        if (i <= 1) // 如果堆为空,直接退出去,插入元素
            break;
        if (!(H->elem[i / 2]->count > data->count)) // 已经找到位置
            break;
        H->elem[i] = H->elem[i / 2]; // 如果插入位置的父结点大于其值,则将其插入位置与父结点交换
        i /= 2;
    }
    H->elem[i] = data; // 将data插入
}

/* 最小堆的删除 */
HaffmanTree deleteMinHeap(Heap H)
{
    int parent, child;
    HaffmanTree min, temp;

    if (H->size == 0)
    {
        printf("Heap is empty!\n");
        return NULL;
    }

    min = H->elem[1]; // 取出最小值-根节点

    // 将堆最后一个元素作为树根,然后调整树的结构
    temp = H->elem[H->size]; // 存放堆的最后一个元素
    H->size--;
    for (parent = 1; parent * 2 <= H->size; parent = child)
    {
        child = parent * 2; // 先指向左子结点
        // child指向左右子结点的较小者
        if ((child != H->size) && (H->elem[child]->count > H->elem[child + 1]->count))
            child++;

        if (temp->count <= H->elem[child]->count) //找到位置了
            break;
        else // 将temp移动到下一层
            H->elem[parent] = H->elem[child];
    }
    H->elem[parent] = temp;
    return min;
}

CSDN统计整篇文章共18906字符,码字不易,如果这篇文章对你有用的话,麻烦留下一个大大的赞吧~非常感谢!

你可能感兴趣的:(数据结构,c语言,huffman,tree,霍夫曼树)