基于哈夫曼编码的文件压缩(c++版)

本博客由Rcchio原创

我了解到很多压缩文件的程序是基于哈夫曼编码来实现的,所以产生了自己用哈夫曼编码写一个压缩软件的想法,经过查阅资料和自己的思考,我用c++语言写出了该程序,并通过这篇文章来记录一下自己写该程序学到的东西。因为本人写的程序在压缩率上,还有提升的空间,所以本文将不定期更新,但程序整体的思路不会有较大的改动。

一、基于哈夫曼编码可实现压缩文件的原理分析

在计算机中,数据的存储都是二进制的,并且以字节作为基本的存储单位,像英文字母在文本中占一个字节,汉字占两个字节,我们把这种每一个字符都通过相同的字节数来表达的编码形式称为定长编码。哈夫曼编码是一种不定长编码方式,即每个字符的编码的长度可以相等也可以不相等。在哈夫曼编码中,出现的频率越高的字符,其哈夫曼编码的长度就越短,相反,出现频率低的字符,哈夫曼编码的长度就越长,因此字符的哈夫曼编码的平均长度是最小的,这就是基于哈夫曼编码可实现文件压缩的基本原理。

二、程序的功能描述

该程序可以用来压缩任何格式的文件生成压缩文件,也可以解压任何由本程序生成的压缩格式的文件。

三、完成该程序所需要的知识

1、哈夫曼树,哈夫曼编码

2、c++的文件操作

3、c++的位操作

四、压缩解压文件的过程详解

众所周知,压缩文件的目的是使该文件在不需要使用时占用的存储空间减小。当使用到该文件时,将压缩文件解压为原文件。因此该程序应具有压缩和解压这两个基本功能。

1、压缩过程:

(1)对原文件中出现的所有字符进行哈夫曼编码。

为了实现对各类型文件的压缩,我们要以二进制的方式而不是文本文件的方式打开文件。文件中数据的存储以字节为单位。一个字节有八位,因此该字节中0和1的排列方式共有2的八次方——256种,每一种排列方式都有一种字符与之对应,即一个字节最多可以表示256类字符,也就是说,原文件中字符的种类数最多只有256种 。将原文件以二进制的方式打开以后,统计原文件中字符的种类及每种字符出现的频度(次数)。将每一种字符作为一个叶节点,字符的频度作为叶节点的权值,就可以构造哈夫曼树,对原文件中出现的所有字符进行哈夫曼编码。将字符和字符对应的哈夫曼编码保存在一个结构体数组中(暂且将它称之为编码表),为下面的向压缩文件输入哈夫曼编码做准备。

如果对于二进制文件和文本文件的概念和区别不了解,可以点击这里

如果对怎样构造哈夫曼树生成哈夫曼编码不清楚可以参考该ppt:点击这里

这里我讲一下如何统计原文件中字符的种类及频度:可以定义一个unsigned char 的变量x,该变量占用一个字节,用变量x依次读取原文件中的字符。定义一个大小为256的结构体数组a,结构体有两个成员unsigned char name和unsigned int weight,  name用来存储字符,weight用来存储字符对应的频度。以字符的值为数组下标,例如变量x读取到字符‘b‘,此时x的值为‘b‘,‘b‘的ascall码为98,因此a[x].weight++就相当于a[98].weight++。x读取一个字符,就执行a[x].name=x;a[x].weight++。a[x].weight的值记录了字符x已经出现的频率,当原文件的所有字符被读取过一遍以后,字符的种类及对应的频率也就统计完毕

当然,原文件中有可能256种字符没有完全出现,构造哈夫曼树时只需要将权值(频度)不为0的字符作为叶节点就可以了。构造好哈夫曼树以后将结构体数组占用的内存释放掉。

(2)向压缩文件中输入解压必需的信息

这就需要考虑如何由压缩文件还原为原文件。整个还原的思路是这样的:将压缩文件的结构分为三部分。第一部分,存储原文件中字符的种类数。第二部分,存储原文件中的字符及对应的频度。第三部分,按照原文件中字符出现的顺序依次输入字符对应的哈夫曼编码。解压时,通过读取压缩文件的第一部分和第二部分的内容,得到原文件中字符的种类和频度,可以重建哈夫曼树。然后读取原文件中的字符对应的哈夫曼编码(具体的实现过程在下面的五-4详述了)。根据字符对应的哈夫曼编码(若干个0-1序列),从重建的哈夫曼树的根节点出发向叶结点的方向移动,0代表向节点的左孩子移动,1代表向节点的右孩子移动。当移动到哈夫曼树的叶节点时,我们就找到了该字符,然后将它输入到解压以后的文件中去。然后再根据下一个字符对应的哈夫曼编码重复上述操作,直到原文件所有的字符都已经严格按顺序输入到解压的文件中去。

因此,我们要向压缩文件中输入三部分信息。第一部分就是原文件中字符的种类数,第二部分就是原文件中的出现的字符及对应的频度,第三部分就是严格根据原文件中字符的顺序输入字符对应的哈夫曼编码。

第一部分和第二部分的内容的输入是很简单的,这里便不再详述。向压缩文件中输入第三部分的内容时,需要再次用unsigned char 类型的变量将原文件中的字符依次读取一遍,每读取到一个字符,就在上面求得的编码表中找到其对应的哈夫曼编码,然后将该字符的哈夫曼编码存到一个string类型的变量unit中,若unit中0和1的个数大于等于8时,就将unit中的前八位0-1序列通过位操作输入到一个unsigned char变量中(具体的实现过程在下面的五-3详述了),再将该变量输入到压缩文件中。这里需要注意,因为哈夫曼编码的0和1都是用字符保存的,如果直接存入压缩文件中,这样会使得压缩文件的存储空间比原文件的还要大,这就违背的压缩的含义。因此,需要通过通过上面的方式来存储哈夫曼编码。

2、解压过程

整个解压的思路已经在上面提到过了。下面我提一个细节问题。

根据某个字符的哈夫曼编码序列从重构的哈夫曼树的根节点出发向叶节点移动的过程中,如何判断已经到达叶节点?是这样的,在构造哈夫曼树 时,我们使用一个结构体数组来保存叶节点和其他双亲结点的,对于有n个叶节点的哈夫曼树,需要大小为2n-1的结构体数组来保存叶节点和其他结点。其中a[0]—a[n-1]都是存放叶节点,a[n]-a[2n-2]存放的是其他节点。所以当从哈夫曼树的根节点出发向叶节点移动时,当某节点的下标在范围0-n-1中时,就说明该结点已经是叶节点了。

五、一些需要注意的细节问题在编码时可能需要注意或者需要参考,我将自己想到的情况举例出来并通过代码来增强博客的可读性(给出的代码只是为了说明某一个知识,并不严格的遵从c++的编程风格,请读者注意)。

1、向压缩文件输入字符种类数,字符及对应的频度的细节问题

向压缩文件中输入字符的种类数,字符及对应的频度时,建议用ofstream类的write函数操作。相对应的,读取压缩文件的前两部分信息,即原文件的字符种类数,字符及其频度时,用ifsream类的read函数操作。而不是通过文件的流来实现。我举一个例子,假设压缩文件的名字为"1.txt",保存字符种类数的变量为H_number。

    ofstream out("1.txt");    int H_number=34;    unsigned char c=‘5‘;    out<<H_number<<c;
    ifstream in("1.txt");in>>H_number;

按我们希望的,H_number的值应为34,但通过流读取文件,H_number的值为345,这样一来,压缩文件就不能得到正确还原了。

如果代码改成这样就能避免上述问题:

    ofstream out("1.txt");    int H_number=34;    unsigned char c=‘5‘;    out.write((char*)&H_number,sizeof(int));    out.write((char*)&c,sizeof(char));
    ifstream in("1.txt");in.read((char*)&H_number,sizeof(int));

2、c++中如何判断文件结束

在c++中,判断文件的结束是用ifstream类的成员函数eof()。用一个unsigned char x变量读取文件的字符,当没有读到文件的结尾时,eof()函数的值为false,当读取文件的最后一个字符时,由于eof()并不知道这是文件的最后一个字符,所以eof()的值仍为false。此时如果继续读取文件中的字符,由于到达了文件的结尾,没有新的字符可以读取,eof()函数的值变为true,而变量x中保存的仍然是文件的最后一个字符。因此如果在统计原文件的字符种类及频度时,代码这样写:

ifstream in("file.txt");while(in.eof())
{
    x=in.get();
    a[x].name=x;
    a[x].weight++;
}

那么最后一个字符的频度就被多统计了一次,这显然错误的读取了原文件的信息,那么解压出来的文件的内容也会和原文件有出入。

该问题可以这样解决:

ifstream in("file.txt");while(true)
{
    x=in.get();
    if(in.eof()) break;
    a[x].name=x;
    a[x].weight++;
}

先读取字符再判断是否到达文件结尾,就避免了将最后一个字符的频度多统计依次。

3、如何将8位的0-1序列通过位操作存入到一个字节的unsigned char变量中

假设有一个string类型的变量unit,存储了8位的0-1序列,现将该序列通过|操作符存入unsigned char变量x中

for(int i=0;i<8;i++)
{
   x=x<<1;
   if(unit[i]==1)
   x=x|1;
}

经过上述代码,变量x便存储了8位0-1序列。

4、如何读取占一个字节的字符对应的二进制比特流(0-1)序列

假设用变量unsigned char x读取了压缩文件中的第三部分的某个字符,现在要获取该字符载有的比特流(8位0-1序列),并保存在一个string 类型的变量s中

for(int i=0;i<8;++i)
{
    if(x&128)
    {
         s[i]=1;
    }
    else
    {
         s[i]=0;
    }
    x=x<<1;
}

六、代码实现

下面我给出程序主类的声明,有助于理解程序的整体结构。

class Hnode  //哈夫曼树的存储结构
{
public:
    unsigned char name;    //8位的存储单元来存储字符(256种)
    unsigned  weight;  //存储字符的频度
    int p; //双亲节点
    int lchild; //左孩子
    int rchild; //右孩子
    int Hnodeindex;  //节点索引
    Hnode() //初始化数据成员
    {
        name = ‘\0‘;
        weight = 0;
        p = 0;
        Hnodeindex = 0;
        lchild = rchild = 0;
    }

class Huffmancode_node   //字符的哈夫曼编码的存储结构
{
public:
    unsigned char name;  //字符的名称
    vector<int> code;    //用vector容器存储哈夫曼编码
    Huffmancode_node()
    {
        name = ‘\0‘;
    }
};
//主类的声明
class ComAndEx   //压缩解压类的声明
{
public:
    void Compress(); //压缩文件的函数
    void Extract();  //解压文件的函数
    string ScanCharacter();//统计源文件种字符的种类及个数的函数
    void CreateHuffmanTree();//建立哈夫曼树的函数
    void CreateHuffmanCode();//生成哈夫曼编码的函数
protected:
    vector<Hnode> HuffmanTree;  //存储哈夫曼树的数组
    vector<Huffmancode_node>Huffmancode;  //存储哈夫曼编码的数组
    int H_number; //字符的种类数
};

这里讲一下为什么将构造哈夫曼树和生成哈夫曼编码分成两个函数实现。因为在压缩过程中需要构造哈夫曼树,生成原文件中出现的字符的哈夫曼编码,而在解压过程中只需要重建哈夫曼树,不需要编码。因此将这两个过程分为两个函数实  现。

完整的代码的链接点击这里

由于程序是在哈夫曼编码的基础上实现的,是学习哈夫曼编码的一个很好的实践机会。所以希望不懂哈夫曼编码的同学查阅相关的资料或下载我前面给的有关哈夫曼编码的ppt来自己学习一下,争取会手动实现这一部分的代码。其他部分的代码如有关文件操作或位操作的,大家可以有选择的借鉴一下。如有高见也请大家不吝指教。

七、程序的局限

上面所实现的程序,虽然能够压缩任何格式的文件,如.txt文件,.docx文件,.ppt文件,.jpg文件,.exe文件等,但是在压缩效率上,往往是.txt文件最高,其他类型的文件压缩效果不明显,甚至原文件和压缩文件的大小比例是1:1的情况也时常见到。究其原因,原文件中字符的种类数越多,哈夫曼树的叶节点个数越多,整个哈夫曼树的深度也越大,哈夫曼编码的平均长度也越长。一些文件中字符的种类数接近或达到256,哈夫曼编码的平均长度接近定长码的长度,故压缩效果不明显。对于在此基础上如何提高文件的压缩率,若大家有高见请不吝赐教。我也将在此基础上继续寻找提高文件压缩率的方法。

时间: 2024-10-14 05:01:12

基于哈夫曼编码的文件压缩(c++版)的相关文章

基于哈夫曼编码的压缩解压程序

这个程序是研一上学期的课程大作业.当时,跨专业的我只有一点 C 语言和数据结构基础,为此,我查阅了不少资料,再加上自己的思考和分析,实现后不断调试.测试和完善,耗时一周左右,在 2012/11/19 完成.虽然这是一个很小的程序,但却是我完成的第一个程序. 源码托管在 Github:点此打开链接 一.问题描述: 名称:基于哈夫曼编码的文件压缩解压 目的:利用哈夫曼编码压缩存储文件,节省空间 输入:任何格式的文件(压缩)或压缩文件(解压) 输出:压缩文件或解压后的原文件 功能:利用哈夫曼编码压缩解

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

文件压缩: 日常生活中有很多压缩的例子,比如给很长的名字取一个缩写--西安交通大学简称西交大,这样就给我们的生活提供了很大的便捷,那么什么又是文件压缩呢?文件压缩就是将文件通过一些方法变得更小,解压缩就是将文件还原,文件压缩将文件变得更小节省了内存,并且在网络上传输起来也变得很快,还具有一定的保密性,所以这个项目就是为了实现这个目的. 基于哈夫曼树的文件压缩 一.思想:众所周知在32位平台下一个字节占八个bit位,假如我们文件中的数据是abbbcccccddddddd时,每个字节占用八个比特位,

用哈弗曼编码实现文件压缩和解压

放假了把这个改一下,发现确实用单字节压缩的压缩率要高一些,暂时没去管为什么,不过放假静下心来写的话确实效率高很多. 新版详见:http://blog.csdn.net/tookkke/article/details/50575103 今天脑洞大开突然想写一下,明明都要考试了,唉,怎么就管不住这手啊  总之呢,就是根据每种编码的出现频率把等长的编码换成变长的,据说理论上压缩比率是比较高的,可是为什么经检验我这个大部分时候压缩出来的比源文件还大呢? 哈弗曼编码的时候要先做一颗字典树,查找的时候就按照

基于哈夫曼(haffuman)算法的文件压缩的实现(C语言)(转)

本文首先简要阐述哈夫曼算法的基本思想,然后介绍了使用哈夫曼算法进行文件压缩和解压缩的 处理步骤,最后给出了C语言实现的文件压缩和解压缩的源代码. 哈夫曼算法的主要思想是: ①首先遍历要处理的字符串,得到每个字符的出现的次数: ②将每个字符(以其出现次数为权值)分别构造为二叉树(注意此时的二叉树只有一个节点): ③取所有二叉树种种字符出现次数最小的二叉树合并为一颗新的二叉树,新二叉树根节点 的权值等于两个子节点的权值之和,新节点中的字符忽略: ④重复过程③直到所有树被合并为同一棵二叉树 ⑤遍历最后

基于python的二元霍夫曼编码译码详细设计

一.设计题目 对一幅BMP格式的灰度图像(个人证件照片)进行二元霍夫曼编码和译码 二.算法设计 (1)二元霍夫曼编码: ①:图像灰度处理: 利用python的PIL自带的灰度图像转换函数,首先将彩色图片转为灰度的bmp图像,此时每个像素点可以用单个像素点来表示. ②:二元霍夫曼编码: 程序流程图: 详细设计: 统计像素点频率,首先通过python自带的PIL库的图像像素点读取函数read()获取灰度图像的所有像素点,通过循环遍历每个像素点,将每个出现的像素点值以及其次数以键值对的形式放入到pyt

利用哈夫曼二叉树实现文件的压缩

首先,我们需要了解一下我们平时的文件是如何保存的.不难理解:不管是什么类型的文件都是以字节的形式存储在我们的各种储存器中的,以二进制的方式将数据储存起来.而我们需要找到一种能够占用内存更少的方式将我们的数据储存.下面我将以压缩字符串为例仔细探讨如何利用哈夫曼二叉树(最优二叉树)压缩文件. 首先需要一个字符串,String str=“QQAFDGGFDAAGFGFDHGFHG”;然后我们需要对每一个字符进行哈夫曼编码,即找到每一个出现的字符找个属于它的哈夫曼编码,当然这不是随便乱给的.下面来看一下

优先级队列优化的霍夫曼编码(带中文压缩)

利用STL中的优先级队列进行优化 我将压缩和解压分为两部分,其实一些还是是一样的 压缩的时候通过bitset将每8个01串压缩成一个字节,如果最后一个不满足8个,用0补齐,但是要记录最后一个字节实际有多少个有效位,将其存入文件最后一个字节,解压的时候先将文件定位到最后一个字节,取出有效位的个数,压缩文件真正有效的是倒数第二个字节,倒数第一个字节只是记录倒数第二个字节中有几位是有效的,解压的时候根据密码本(记录每个字节的权值)建立哈夫曼树,然后更具哈夫曼树解压文件 压缩代码部分: #include

数据结构:哈夫曼编码(php版)

演示网址:http://huffman.sinaapp.com/ 源文件下载地址:http://xiaocao.u.qiniudn.com/work/huffman-2013-12-19.zip 概述下: 哈夫曼树─即最优二叉树,带权路径长度最小的二叉树,经常应用于数据压缩. 在计算机信息处理中,"哈夫曼编码"是一种一致性编码法(又称"熵编码法"),用于数据的无损耗压缩.     简单的,就是靠权值排序,然后,转码,最优保存. 实现功能: 保存译码:在服务器端保存源

霍夫曼编码求节省空间

霍夫曼编码将频繁出现的字符采用短编码,出现频率较低的字符采用长编码.具体的操作过程为:i)以每个字符的出现频率作为关键字构建最小优先级队列:ii)取出关键字最小的两个结点生成子树,根节点的关键字为孩子节点关键字之和,并将根节点插入到最小优先级队列中,直至得到一棵最优编码树. 霍夫曼编码方案是基于______策略的.用该方案对包含a到f6个字符的文件进行编码,文件包含100000个字符,每个字符的出现频率(用百分比表示)如表1-3所示,则与固定长度编码相比,该编码方案节省了______存储空间.