基于哈夫曼树的任意文件解压缩实现

Posted 没头发的米糊

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于哈夫曼树的任意文件解压缩实现相关的知识,希望对你有一定的参考价值。

哈夫曼编码是一种基于二叉树生成的不等长编码,通过赋予高频次字符更短的编码来减小文件体积。本例中,将详细地讲解如何使用C++语言完成哈夫曼编码的压缩与解压,共包括以下六个部分:

一、编码的读取与写入

1.字节流

2.二进制文件的读取与写入

3.位运算

二、基于哈夫曼编码实现文件压缩

1.按字节统计种类和频度

2.根据频度构建哈夫曼树

3.通过哈夫曼树生成哈夫曼编码

4.文件头部信息的写入

5.创建字节编码和哈夫曼树结点索引的键值对

6.把新的编码信息写进文件中

三、文件的解压

1.特征字符串的校验

2.从文件中读取头部信息

3.通过哈夫曼树生成哈夫曼编码

4.解码文件

四、结果演示

五、总结

六、源码地址



一、编码的读取与写入

1.字节流

在信息理论中,位(Bit)是最小的信息单位,而在计算机中,字节(Byte)是存储数据的基本单位,并且是硬件所能访问的最小单位

内存里面存放的全是二进制代码。内存里面有很多“小格子”,每个“格子”中只能存放一个 0 或 1。一个“小格子”就是一位,所以“位”要么是 0,要么是 1,不可能有比位更小的单位。那么字节和位是什么关系呢?8 个“小格子”就是一字节,即一字节等于 8 位。

那么为什么硬件所能访问的最小单位是字节,而不是位呢?因为硬件是通过地址总线访问内存的,而地址是以字节为单位进行分配的,所以地址总线只能精确到字节。因此在二进制文件的读取中,只能以字节为单位进行读取和写入。

2.二进制文件的读取与写入

在C语言中,文件流有两种模式,分别是文本模式和二进制模式。

用文本方式存储信息不但浪费空间,而且不便于检索。例如,一个学籍管理程序需要记录所有学生的学号、姓名、年龄信息,并且能够按照姓名查找学生的信息。程序中可以用一个类来表示学生:

class CStudent
{
    char szName[20];  //假设学生姓名不超过19个字符,以 '\\0' 结尾
    char szId[l0];  //假设学号为9位,以 '\\0' 结尾
    int age;  //年龄
};

如果用文本方式将学生对象的三个信息依次存入硬盘中,那么在读取的时候,则其中的数据都经过二次编码,需要更多的空间和时间。

而如果使用二进制方式直接存储学生信息,即相当于把CStudent对象的内存信息直接存入文件,在该文件中,每个学生的信息都占用 sizeof(CStudent) 个字节。占用空间更少,最终读取时也可以整条对象直接读入。

读写二进制文件不能使用类似于 cin、cout 从流中读写数据的方法。这时可以调用 ifstream 类和 fstream 类的 read 成员函数从文件中读取数据,调用 ofstream 和 fstream 的 write 成员函数向文件中写入数据。


用 ostream::write 成员函数写文件
ofstream 和 fstream 的 write 成员函数实际上继承自 ostream 类,原型如下:

ostream & write(char* buffer, int count);

该成员函数将内存中 buffer 所指向的 count 个字节的内容写入文件,返回值是对函数所作用的对象的引用,如 obj.write(…) 的返回值就是对 obj 的引用。

write 成员函数向文件中写入若干字节,可是调用 write 函数时并没有指定这若干字节要写入文件中的什么位置。那么,write 函数在执行过程中到底把这若干字节写到哪里呢?答案是从文件写指针指向的位置开始写入。

文件写指针是 ofstream 或 fstream 对象内部维护的一个变量。文件刚打开时,文件写指针指向文件的开头(如果以 ios::app 方式打开,则指向文件末尾),用 write 函数写入 n 个字节,写指针指向的位置就向后移动 n 个字节

下面的程序从键盘输入几名学生的姓名和年龄(输入时,在单独的一行中按 Ctrl+Z 键再按回车键以结束输入。假设学生姓名中都没有空格),并以二进制文件形式存储,成为一个学生记录文件 students.dat。

#include <iostream>
#include <fstream>
using namespace std;
class CStudent
{
public:
    char szName[20];
    int age;
};
int main()
{
    CStudent s;
    ofstream outFile("students.dat", ios::out | ios::binary);
//指定文件的打开模式是 ios::out|ios::binary,即以二进制写模式打开。在 Windows平台中,用二进制模式打开是必要的,否则可能出错。
    while (cin >> s.szName >> s.age)
        outFile.write((char*)&s, sizeof(s));
//将 s 对象写入文件。s 的地址就是要写入文件的内存缓冲区的地址。但是 &s 不是 char * 类型,因此要进行强制类型转换。
    outFile.close();
//文件使用完毕一定要关闭,否则程序结束后文件的内容可能不完整。
    return 0;
}

输入:
Tom 60↙
Jack 80↙
Jane 40↙
^Z↙

则形成的 students.dat 总大小为 72 字节,用“记事本”程序打开呈现乱码:
Tom烫烫烫烫烫烫烫烫 Jack烫烫烫烫烫烫烫? Jane烫烫烫烫烫烫烫?


用 istream::read 成员函数读文件
ifstream 和 fstream 的 read 成员函数实际上继承自 istream 类,原型如下:

istream & read(char* buffer, int count);  

该函数从文件中读取count个字节的内容,并存放到buffer指向的缓冲区空间中。返回值是对函数所作用的istream对象的引用。

如果想知道一共成功读取了多少个字节(读到文件尾时,未必能读取 count 个字节),可以在 read 函数执行后立即调用文件流对象的 gcount 成员函数,其返回值就是最近一次 read 函数执行时成功读取的字节数。gcount 是 istream 类的成员函数,原型如下:

int gcount();

read 成员函数从文件读指针指向的位置开始读取若干字节。文件读指针是 ifstream 或 fstream 对象内部维护的一个变量。文件刚打开时,文件读指针指向文件的开头(如果以ios::app 方式打开,则指向文件末尾),用 read 函数读取 n 个字节,读指针指向的位置就向后移动 n 个字节。因此,打开一个文件后连续调用 read 函数,就能将整个文件的内容读取出来。

下面的程序将前面创建的学生记录文件 students.dat 的内容读出并显示。

#include <iostream>
#include <fstream>
using namespace std;
class CStudent
{
    public:
        char szName[20];
        int age;
};
int main()
{
    CStudent s;       
    ifstream inFile("students.dat",ios::in|ios::binary); //二进制读方式打开
    if(!inFile) {
        cout << "error" <<endl;
        return 0;
    }
//判断文件是否已经读完的方法和 while(cin>>n) 类似,归根到底都是因为 istream 类重载了 bool 强制类型转换运算符。
    while(inFile.read((char *)&s, sizeof(s))) { //一直读到文件结束
        int readedBytes = inFile.gcount(); //看刚才读了多少字节
        cout << s.szName << " " << s.age << endl;   
    }
    inFile.close();
    return 0;
}

程序的输出结果是:
Tom 60
Jack 80
Jane 40

3.位运算

前面已经提到,由于在计算机中,数据是以字节的方式进行存储的,因此我们选择对原始文件的每字节(即8位,0-255)数据进行编码,编码完毕后会生成不等长编码,为了能将哈夫曼编码写入I/O流中,我们需要进行更加精细的位操作,以生成编码后的字节,因此需要涉及到位运算的知识。

这里给出在本例中需要用到的位运算符号,关于位运算的具体使用,不清楚的小伙伴可以参考Matrix67《位运算简介及实用技巧博文》


本例中用到的位运算符:

& 按位与        | 按位或 

<< 位左移      >> 位右移 


基于上述的前置知识,我们可以写出下述文件操作的代码,下面这段代码将 text.txt 文件中的信息按字节读取到fileData中,接下来将谈谈如何使用哈夫曼编码对文件进行压缩与解压。

    char fileName[] = "test.txt";

    int amountOfBytes = 0;
    unsigned char c = '\\0';


    //To count the number of bytes in the file.
    ifstream Ifile;
    Ifile.open(fileName, ios::in | ios::binary);
    while (Ifile.read((char*)&c, sizeof(char)))
        amountOfBytes++;
    Ifile.close();


    //To write the fileData into memory.
    unsigned char* fileData = new unsigned char[amountOfBytes + 1];
    Ifile.open(fileName, ios::in | ios::binary);
    int index = 0;
    while (Ifile.read((char*)&c, sizeof(char)))
    {
        fileData[index] = c;
        index++;
    }

二、基于哈夫曼编码实现文件压缩

1.按字节统计种类和频度

代码分析

    //To count the frequency of the bytes in the file.
    int freq[256] = { 0 };
    int allCodes = 0;
    for (int i = 0; i <= amountOfBytes - 1; i++)
    {
        freq[(int)fileData[i]] ++;
    }

    for (int i = 0; i <= 255; i++)
    {
        if(freq[i] != 0) allCodes ++;
    }
    cout << allCodes << endl;
//allCodes用于记录所有出现的字节编码的种类树,这对应着相应的哈夫曼树将有多少叶子结点。

上述代码中,使用freq数组记录0-255的字节编码的种类和频度,用编码内容作为下标,频度对应当前下标的值。

2.根据频度构建哈夫曼树

①结构体定义

由于在哈夫曼树中,一旦知道总的叶子节点数为allCodes(即编码总数),那么所有的结点数将为 2*allCodes-1 个。结点数已知,建议使用顺序结构进行哈夫曼树的存储。以下为每个结点的结构,其中lChild、rChild分别为其左孩子和右孩子在顺序结构中的索引,huffmanCode用于保存叶子结点对应的编码。

typedef struct _HuffmanNode
{
    int freq = 0;//频度,即结点值
    unsigned char byteData = '\\0';
    int lChild = -1;
    int rChild = -1;
    char *huffmanCode = nullptr;
    bool hasParent = false;
}HuffmanNode;

 ②代码分析

    //Create a Huffman Tree with the freqs.
    HuffmanNode* huffmanTree = new HuffmanNode[allCodes * 2 - 1];
    int nodeAmount = 0;
    for (int i = 0; i <= 255; i++)
    {
        if (freq[i] != 0)
        {
            cout << i << endl;
            huffmanTree[nodeAmount].byteData = i;
            huffmanTree[nodeAmount].lChild = -1;
            huffmanTree[nodeAmount].rChild = -1;
            huffmanTree[nodeAmount].freq = freq[i];
            huffmanTree[nodeAmount].hasParent = false;
            nodeAmount ++;
        }
    }

在上述代码中, 首先将所有叶子结点加入到哈夫曼树中。

    while (nodeAmount < allCodes * 2 - 1)
    {
        int min1 = 2147483647, min2 = 2147483647;
        int minIndex1 = -1, minIndex2 = -1;
        for (int i = 0; i <= nodeAmount - 1; i++)
        {
            if (huffmanTree[i].hasParent == true) continue;
            if (huffmanTree[i].freq < min1)
            {
                min2 = min1;
                minIndex2 = minIndex1;
                min1 = huffmanTree[i].freq;
                minIndex1 = i;
            }
            else if (huffmanTree[i].freq < min2)
            {
                min2 = huffmanTree[i].freq;
                minIndex2 = i;
            }
        }
        assert(minIndex1 != -1 && minIndex2 != -1);
//断言:一定存在可以继续结合的子树。
        huffmanTree[nodeAmount].byteData = 0;
        huffmanTree[nodeAmount].freq = min1 + min2;

        huffmanTree[nodeAmount].lChild = minIndex1;
        huffmanTree[minIndex1].hasParent = true;

        huffmanTree[nodeAmount].rChild = minIndex2;
        huffmanTree[minIndex2].hasParent = true;

        huffmanTree[nodeAmount].hasParent = false;
        nodeAmount++;
    }

在上述代码中,我们循环找出根结点值最小的两个子树,将它们拼合成一个新子树,新的结点被加入到哈夫曼树中,直至总结点数量达到allCodes*2-1。

3.通过哈夫曼树生成哈夫曼编码

代码分析

void calculateHuffmanCode(HuffmanNode* huffmanTree, int root, int index, char* code)
{
    if (huffmanTree[root].lChild != -1 && huffmanTree[root].rChild != -1)
    {
        code[index] = '1';
        calculateHuffmanCode(huffmanTree, huffmanTree[root].lChild, index + 1, code);
        code[index] = '0';
        calculateHuffmanCode(huffmanTree, huffmanTree[root].rChild, index + 1, code);
    }
    else
    {
        code[index] = '\\0';
        huffmanTree[root].huffmanCode = new char[strlen(code)];
        strcpy(huffmanTree[root].huffmanCode, code);
    }
}

在上述代码中,通过递归对哈夫曼树进行遍历,在遍历过程中记录叶子节点对应的哈夫曼编码,并在走到叶子结点时把编码存储到叶子节点的huffmanCode中。

    //Calculate the huffmanCode.
    char* tempStr = new char[nodeAmount];
    calculateHuffmanCode(huffmanTree, nodeAmount - 1, 0, tempStr);


    for (int i = 0; i < allCodes * 2 - 1; i++)
    {
        cout << (int)(unsigned char)huffmanTree[i].byteData << " " << huffmanTree[i].freq << " " << huffmanTree[i].hasParent
            << " " << huffmanTree[i].lChild << " " << huffmanTree[i].rChild
            << " " << ((huffmanTree[i].huffmanCode != nullptr)? huffmanTree[i].huffmanCode :"")<< endl;
    }

在上述代码中,调用calculateHuffmanCode函数计算哈夫曼编码,并输出整个哈夫曼树。

如test.txt文件中的数据为

Hello World!This is an blog by MiHu.

 经过上述代码可得到如下输出:

每行顺序分别为:字符 频度 是否有父节点 左孩子索引 右孩子索引 哈夫曼编码

  6 1 -1 -1 001
! 1 1 -1 -1 10101
. 1 1 -1 -1 10100
H 2 1 -1 -1 1101
M 1 1 -1 -1 10011
T 1 1 -1 -1 10010
W 1 1 -1 -1 10001
a 1 1 -1 -1 10000
b 2 1 -1 -1 1100
d 1 1 -1 -1 01111
e 1 1 -1 -1 01110
g 1 1 -1 -1 01101
h 1 1 -1 -1 01100
i 3 1 -1 -1 0001
l 4 1 -1 -1 111
n 1 1 -1 -1 01011
o 3 1 -1 -1 0000
r 1 1 -1 -1 01010
s 2 1 -1 -1 1011
u 1 1 -1 -1 01001
y 1 1 -1 -1 01000
  2 1 1 2
  2 1 4 5
  2 1 6 7
  2 1 9 10
  2 1 11 12
  2 1 15 17
  2 1 19 20
  4 1 3 8
  4 1 18 21
  4 1 22 23
  4 1 24 25
  4 1 26 27
  6 1 13 16
  8 1 14 28
  8 1 29 30
  8 1 31 32
  12 1 0 33
  16 1 34 35
  20 1 36 37
  36 0 38 39

前面所有带哈夫曼编码的为叶子节点,其余为非叶子结点,最后一个结点为根节点。

4.文件头部信息的写入

哈夫曼编码是一种基于字符频度的编码,因此在写入编码前,有必要将字符的频度信息写入,这样在解压缩时,只需要根据频度信息再次构建哈夫曼树,进行解压缩。


    //Write the head infomation (include byte frequencies) into file.
    ofstream Ofile("output.mihupack",ios::out|ios::binary);

    const char *headInfo = "[MIHUPACK]This is a MiHu Pack,please use MiHuPacker to unpack.\\n";
    Ofile.write((char*)headInfo, strlen(headInfo));

    Ofile.write((char*)&allCodes,sizeof(allCodes));
    Ofile.write((char*)&amountOfBytes, sizeof(amountOfBytes));

    int lengthOfFilename = strlen(fileName);
    Ofile.write((char*)&lengthOfFilename, sizeof(lengthOfFilename));
    Ofile.write((char*)&fileName, strlen(fileName));

    for (int i = 0; i <= allCodes - 1; i++)
    {
        Ofile.write((char*)&huffmanTree[i].byteData, sizeof(huffmanTree[i].byteData));
        Ofile.write((char*)&huffmanTree[i].freq, sizeof(huffmanTree[i].freq));
    }

在上述代码中,我们依次写入了一个特征字符串(用于识别是否为我们的压缩程序创建的特定格式,占空间大小与特征字符串长度有关)、所有的编码种类allCodes(int,占4字节)、整个文件的大小amountOfBytes(单位为字节,int,占4字节)、原文件名的长度lengthOfFileName(int,占4字节)、原文件名fileName、字符和对应频度(char + int,占allCode*(1+4)字节)。

写入上面的这些头部信息完全是为了能够进行正确的解压缩,虽然其中某些信息理论上可以省略,但为了能正确解压缩,更完整的信息有利于信息的正确性校验,因此是有必要的。

5.创建字节编码和哈夫曼树结点索引的键值对

有了以上的前置准备,我们可以开始将真正的编码信息写入文件中了。但是,如何根据原文件的每一个字节编码,快速找到它所对应的哈夫曼编码呢?

一种方案是每次都根据当前的字节编码,查找哈夫曼树,如果字符编码匹配,即为我们需要的哈夫曼编码。

huffCode = nullptr;
for(int i=0;i< allCodes;i++)
{
    if(huffmanTree[i].byteData == curByteData)
        huffCode = huffmanTree[i].huffmanCode;
}
assert(huffCode != nullptr);
//这时一定会找到对应的哈夫曼编码,如果找不到,
//说明前面的字节编码频度统计和哈夫曼树的创建存在问题,跳出程序。

但这种方案每个编码都需要遍历编码表,很浪费时间。因此考虑建立一个键值对huffKey,用于保存字节编码和对应下标的关系,实现如下:

    //Create the key-value pairs of byteData-huffmantreeIndex.
    int HuffKey[256];
    memset(HuffKey, -1, sizeof(HuffKey));
    for (int i = 0; i <= allCodes - 1; i++)
    {
        HuffKey[(int)(unsigned char)huffmanTree[i].byteData] = i;
    }

6.把新的编码信息写进文件中

    //Write the huffmanCodes into file.
    char byte = '\\0';
    bool isByteEmpty = true;
    int bitIndex = 0;
    for (int i = 0; i <= amountOfBytes - 1; i++)
    {
        int codeIndex = 0;
        char tempChar = '\\0';
        //cout << (int)fileData[i] << endl;
        assert(huffmanTree[HuffKey[fileData[i]]].huffmanCode != nullptr);
        char* code = huffmanTree[HuffKey[fileData[i]]].huffmanCode;
        while (codeIndex < strlen(code))
        {
            char bit = code[codeIndex] - '0';
            //cout << (int)bit;
            byte = byte | (bit <<  (7 - bitIndex) );
            bitIndex ++;
            codeIndex ++;
            isByteEmpty = false;
            if (bitIndex == 8)
            {
                //cout << "!" << (int)(unsigned char)byte << endl;
                Ofile.write((char*)&byte, sizeof(byte));
                byte = '\\0';
                isByteEmpty = true;
                bitIndex = 0;
            }
        }
    }


    if(!isByteEmpty) Ofile.write((char*)&byte, sizeof(byte));

用 bitIndex 表示当前的字节已经写到了第几位,codeIndex 用于表示已经处理了多少字节编码,总字节编码数即待压缩文件的大小(字节)。

bitIndex每满8位就要将当前的字节写进文件中,并且清零。

在最后可能会有一个字节未写满的状况,依然需要写到文件中,注意:写入的最后一个字节可能会存在垃圾位。

    Ofile.close();

最后,压缩完毕,关闭文件流。

三、文件的解压

解压文件的程序与压缩文件相同,开始时需要通过文件流将文件读入到内存中,具体实现上方已有,不再赘述。

1.特征字符串的校验

    //To verify file format.
    const char* headInfo = "[MIHUPACK]This is a MiHu Pack,please use MiHuPacker to unpack.\\n";
    char* fileHead = new char[strlen(headInfo) + 1];

    for(index = 0 ; index <= min(strlen(headInfo) - 1,strlen((const char*)fileData)) ; index ++)
    {
    	fileHead[index] = fileData[index];
	}
	fileHead[index] = '\\0';
	if (strcmp(fileHead, headInfo) == 0)
    {
        cout << "Valid file format,decoding..." << endl;
    }
    else
    {
    	cout << "Invalid file format!" << endl;
    	return 0;
	}

接下来,需要对文件信息进行校验,校验特征字符串(用于识别是否为我们的压缩程序创建的特定格式,占空间大小与特征字符串长度有关),以确定是否是我们的压缩程序压缩出的文件。

2.从文件中读取头部信息

    //To get the frequency of the bytes in the source file.
	int allCodes = 0;
	int amountOfSourceBytes = 0;
	memcpy(&allCodes,fileData + index,sizeof(int));
	index += sizeof(int);
	memcpy(&amountOfSourceBytes,fileData + index,sizeof(int));
	index += sizeof(int);

	int i = 0;
    int lengthOfFilename = 0;
    memcpy(&lengthOfFilename,fileData + index,sizeof(int));
    index += sizeof(int);
    char *sourceFileName = new char[lengthOfFilename + 1];
    for(i = 0;i < lengthOfFilename;i ++)
    {
        sourceFileName[i] = fileData[index + i];
    }
    sourceFileName[i] = '\\0';
    index += i;
	cout << allCodes << " " << amountOfSourceBytes << " " << sourceFileName << endl;

	int freq[256] = { 0 };
	for (int i = 0; i <= allCodes - 1; i++)
    {
    	unsigned char byteData = '\\0';
    	int frequency = 0;
    	memcpy(&byteData,fileData + index,sizeof(char));
    	index += sizeof(char);
    	memcpy(&frequency,fileData + index,sizeof(int));
    	index += sizeof(int);
    	freq[byteData] = frequency;
    }

通过index指向已经读取到的位置,依次使用 memcpy 读取所有的编码种类allCodes(int,占4字节)、整个文件的大小amountOfBytes(单位为字节,int,占4字节)、原文件名的长度lengthOfFileName(int,占4字节)、原文件名fileName、字符和对应频度(char + int,占allCode*(1+4)字节)。

这一部分没什么技术含量,因此请读者自行审阅代码。

3.通过哈夫曼树生成哈夫曼编码

接下来的操作与压缩时一致,通过已有的字节编码频度生成哈夫曼树、计算哈夫曼编码,代码可以直接复用。

4.解码文件

在完成一系列操作后,就可以解码文件。

    //Decode the file.
	ofstream Ofile(sourceFileName, ios::out | ios::binary);

	int sourceByteIndex = 0;
	int curByteIndex = index;
	int curBitIndex = 0;

	const int rootNode = allCodes * 2 - 1 - 1;
	int curTreeNode = rootNode;

	char curByte = '\\0';
	memcpy(&curByte,fileData + curByteIndex,sizeof(char));

	while(sourceByteIndex < amountOfSourceBytes)
	{
		int curBit = ( curByte >> (7 - curBitIndex) ) &  1 ;
		if(curBit == 1)
		{
			curTreeNode = huffmanTree[curTreeNode].lChild;
		}
		else
		{
			curTreeNode = huffmanTree[curTreeNode].rChild;
		}
		if(huffmanTree[curTreeNode].huffmanCode != nullptr)
		{
			//cout << huffmanTree[curTreeNode].byteData << " " << sourceByteIndex << " " << amountOfSourceBytes << endl;
			Ofile.write((char*)&huffmanTree[curTreeNode].byteData, sizeof(huffmanTree[curTreeNode].byteData));
			curTreeNode = rootNode;
			sourceByteIndex ++;
		}
		curBitIndex ++;
		if(curBitIndex == 8)
		{
			curByteIndex ++;
			curBitIndex = 0;
			memcpy(&curByte,fileData + curByteIndex,sizeof(char));
		}
	}

这一部分我们准备了一个curTreeNode,用于遍历哈夫曼树。当读取到1时向左子树走,读取到0时向右子树走,走到叶子结点时就将叶子结点存储的字符编码写进源文件中,同时curTreeNode回到根结点。如此往复,直到原文件中的字节编码全部被解码出来为止。

四、结果演示

压缩一张BMP图片时达到了53%的压缩效率。对于其他文件基本能稳定在50%-90%之间,某些特殊文件压缩后没有变化。

对于压缩包文件,很难有压缩效果,并且由于添加了头部信息,还变大了1-2字节。

五、总结

基于哈夫曼编码的文件解压缩是一个涉及知识很多的工程,总的来说还是对综合能力的一种检验吧......这也是我到目前为止遇到的内容最多最复杂的工程。

这个程序总的来说可以处理大部分的正常输入,但在测试过程中还遇到极端情况比如:一个全是a的txt文件就会被解压为全是t的txt文件。

本人能力有限,还请看到的大神和我沟通,争取把这个错误解决。

六、源码地址

以上是关于基于哈夫曼树的任意文件解压缩实现的主要内容,如果未能解决你的问题,请参考以下文章

关于文件压缩解压缩与文件加密解密的项目

项目实战——基于LZ77变形和哈夫曼编码的GZIP压缩

项目实战——基于LZ77变形和哈夫曼编码的GZIP压缩

霍夫曼树和霍夫曼编码以及霍夫曼编码的应用

基于哈夫曼树的数据压缩算法

哈夫曼编码器