【C++实现文件压缩项目】基于Huffman树实现文件压缩和解压

首先,我们将文件压缩这个项目分为五个步骤:

  • 1. 统计字符出现的次数
  • 2. 构建HuffmanTree
  • 3. 生成哈夫曼编码 (Huffman Code)
  • 4. 压缩 (compress)
  • 5. 解压缩 (uncompress)

Huffman树 ,又称为最优二叉树,是加权路径长度最短的二叉树。

【贪心算法】是指在问题求解时,总是做出当前看起来最好的选择。也就是说贪心算法做出的不是整体最优的的选择,而是某种意义上的局部最优解。贪心算法不是对所有的问题都能得到整体最优解。使用贪心算法构建Huffman树.


文件压缩的过程:

Input.txt
内容:aaaabbbcccd

1. 统计字母出现的次数:

a ------ 4
b ------ 3
c ------ 2
d ------ 1

2. 构建huffmanTree:

这里写图片描述

3. 生成huffman code(编码):

a -> 0 (* 出现次数多的字符(数值大)–路径短–编码短)
b -> 11
c -> 101
d -> 100

4. 压缩(按位存储):

文件 :Input.txt.huffman
内容 : 00001111 11101101 100(00000) ----按位存储
第三个字节后面的五个0是补位
(原文件占用10个字节,压缩后占用3个字节)

5. huffman解压缩:

还原文件:Input.txt.unhuffman

思考: 如果一个文件中内容比较多,有的字符原本只占一个字节,但是压缩后会占用好几个字节,这样压缩后会有效果吗?

答:因为出现次数最多的那个字符只占用1个或者2个位,而字节最多的那个只会出现1次后或者2次,相对于而言,会节省很大的空间。


整个项目中我们会遇到的问题:

1.压缩带中文的文件,程序就会崩溃。

最后发现数组越界的问题.
因为char它的范围是-128127,程序中使用char类型为数组下标(0127),所以字符没有问题,但是汉字是占两个字节的,所以会出现越界的问题,解决的方法就是char类型强转为unsigned char,它的范围为0~255.

2.解压缩文件生成后会丢失很多内容.
1. 文件的打开方式.
   这里打开文件一定要用二进制形式,"wb","rb".因为二进制打开和文本打开其实是有区别的。
   1.文本方式打开:会对‘\n’进行特殊处理,那如果这个字符本身就是'\n'.这就会出现问题。
   2.二进制方式打开:不进行任何处理,是什么就是什么。
2. 文件结束符的问题。
   刚开始用的文件结束标志是EOF在ASII中它的编码是-1,随后我们用了 二进制方式打开文件,而二进制文件是会出现-1的,所以提前结束了,所以我们用二进制读取并且不能用EOF来判断结束。
   C中有一个函数叫做feof(),它可以检查流上文件的结束符,如果文件结束,则返回非0值,否则返回0

下面引用百科的解释: 
feof(fp)有两个返回值:如果遇到文件结束,函数feof(fp)的值为非零值,否则为0。

EOF是文本文件结束的标志。在文本文件中,数据是以字符的ASCⅡ代码值的形式存放,普通字符的ASCⅡ代码的范围是32到127(十进制),EOF的16进制代码为0x1A(十进制为26),因此可以用EOF作为文件结束标志。[1]

当把数据以二进制形式存放到文件中时,就会有-1值的出现,因此不能采用EOF作为二进制文件的结束标志。为解决这一个问题,ASCI C提供一个feof函数,用来判断文件是否结束。feof函数既可用以判断二进制文件又可用以判断文本文件。

3.为什么要使用配置文件?

在项目中,将字符对应的编码转化为位,在unsigned char中填充位,填满后就写入到压缩文件中。

问题1:最后一个字节是不是很有可能没有填满,该如何判断他是否填满以及填了几个字符的编码?
问题2:若依次压缩一些文件,压缩完后再去解压,那么编码此时已经没有了,该如何解压?
上面的两个问题可通过配置文件解决,假如要压缩的文件叫xxx,那么可生成一个xxx.config的配置文件,在该配置文件中写入<文件的总长度>(恢复时知道应该恢复多少个字符),(字符以及其出现的次数,用于解压时重建哈夫曼树),利用该配置文件即可解决这两个问题。


完整代码实现:

包括:
Filecompress.h Heap.h HuffmanTree.h test.cpp

Filecompress.h:
#pragma once 

#include<iostream>
#include<assert.h>
#include<algorithm>
#include<string>
#include"HuffmanTree.h" 

struct CharInfo 
{ 
	unsigned char _ch;	// 字符 
	long long _count; // 文件中出现的次数 
	string _code;	// huffman code
	CharInfo(unsigned char ch = 0)
		:_ch(ch)
		,_count(0)
	{}
	bool operator!=(const CharInfo& info)
	{
		return _count!=info._count;
	}
	bool operator<(const CharInfo& info)
	{
		return _count<info._count ;
	}

	CharInfo operator+(const CharInfo& info)
	{
		CharInfo tmp;
		tmp._count = _count+info._count ;
		return tmp;
	}

}; 

class FileCompress 
{ 
typedef HuffmanTreeNode<CharInfo> Node; 
struct ConfigInfo
{
	unsigned char _ch;
	long long _count;
};
public: 
	//无构造函数的话就是随机值
	FileCompress() 
	{ 
		for (int  i = 0; i < 256; ++i) 
		{ 
			_infos[i]._ch = i; 
			_infos[i]._count = 0; 
		}
	}

	void GetHuffmantree(Node* root,string code)
	{
		if(root == NULL)
		{
			return ;
		}
		
		if(root->_left == NULL && root->_right==NULL)
		{
			_infos[(unsigned char)root->_w._ch]._code= code ;
			return;
		}
		GetHuffmantree(root->_left,code+'0');
		GetHuffmantree(root->_right,code+'1');
	}
	
    void GetHuffmanCode(Node* root)
    {
        if (root == NULL)
        {
            return;
        }
        if (root->_left == NULL&&root->_right == NULL)
        {
            string &code = _infos[(unsigned char)root->_w._ch]._code;
            Node* cur = root;
            Node* parent = cur->_parent;
            while (parent)
            {
                if (cur == parent->_left)
                {
                    code.push_back('0');
                }
                else if (cur == parent->_right )
                {
                    code.push_back('1');
                }
                else
                {
                    break;
                }
                cur = parent;
                parent = cur->_parent;
            }

            reverse(code.begin(), code.end());
            return;
        }
        GetHuffmanCode(root->_left);
        GetHuffmanCode(root->_right);

    }
	// Input.txt->Input.txt.huffman 压缩
	void Compress(const char* file) 
	{
		assert(file);

		//1,统计字符出现的次数
		FILE* fout = fopen(file,"rb");
		assert(fout);
		char ch = fgetc(fout);
		while(!feof(fout))
		{
			_infos[(unsigned char)ch]._count++; //这个字符的哪个位置的个数加一
			ch = fgetc(fout);
		}
		//2,构建哈夫曼树
		CharInfo invalid;
		invalid._count = 0;
		HuffmanTree<CharInfo> tree(_infos,256,invalid);
		Node* root = tree.GetRoot ();
		//3,生成哈夫曼编码 
		string code;
		GetHuffmantree(root,code);
		//4,压缩文件
        // Input.txt->Input.txt.huffman 
		string compressfile = file;
		compressfile += ".huffman";
		FILE* fin = fopen(compressfile.c_str(),"wb");
  
		for(size_t i=0;i<256;i++)
		{
			ConfigInfo info;
			if(_infos[i]._count)
			{
				info._ch = _infos[i]._ch ;
				info._count = _infos[i]._count ;
				fwrite(&info,sizeof(ConfigInfo),1,fin);
			}
		}

		ConfigInfo info;
		info._count = 0;
		fwrite(&info,sizeof(ConfigInfo),1,fin);
		fseek(fout,0,SEEK_SET);
		 ch = fgetc(fout);
		char value = 0;
		char pos = 0;
		while(!feof(fout))
		{
			string& code = _infos[(unsigned char)ch]._code;
			for(size_t i = 0;i<code.size();++i)
			{
				//将编码变成二进制
				if(code[i] == '1')
				{
					value |= (1<<pos);
				}
				else if(code[i] == '0')
				{
					value &= (~(1<<pos));
				}
				else
				{
					assert(false);
				}
				++pos;
				if(pos == 8)
				{
					fputc(value,fin);
					value = 0;
					pos = 0;
				}
			}
			ch = fgetc(fout);
		}
		if(pos)//补位
		{
			//value <<= (8-pos);
			fputc(value,fin);
		}
		fclose(fout);
		fclose(fin);
	}

	 //Input.txt.huffman -> Input.txt.unhuffman 
	void Uncompress(const char* file)
	{
		assert(file);
		string uncompressfile = file;
		size_t pos = uncompressfile.rfind('.');
		assert(pos != string::npos);
		uncompressfile.erase(pos,uncompressfile.size()-pos);
		uncompressfile += ".unhuffman";
		FILE* fin = fopen(uncompressfile.c_str(),"wb");
		assert(fin);
		FILE* fout = fopen(file,"rb");
		assert(fout);

		//读配置信息
		while(!feof(fout))
		{
			ConfigInfo info;
			info._count = 0;
			fread(&info,sizeof(ConfigInfo),1,fout);

			if(info._count )
			{
				_infos[info._ch ]._count = info._count ;
			}
			else
			{
				break;
			}
		}
		CharInfo invalid;
		invalid._count = 0;
		HuffmanTree<CharInfo> tree(_infos,256,invalid);
		Node* root = tree.GetRoot();
		Node* cur = root;
		long long charcount = 0; 
		charcount = tree.GetRoot ()->_w._count;
		 pos = 0;
		char value = fgetc(fout);
		while(!feof(fout))
		{
			if(value & (1<<pos))
				cur = cur->_right;
			else
				cur = cur->_left;
			if(cur->_left == NULL && cur->_right == NULL)
			{
				fputc(cur->_w._ch,fin);
				cur = root;

				if(--charcount == 0)
				{
					break;
				}
			}
			++pos;
			if(pos == 8)
			{
				value = fgetc(fout);
				pos = 0;
			}
		}
		

		fclose(fout);
		fclose(fin);
	}
protected: 
	CharInfo _infos[256]; 
}; 

Heap.h:
#pragma once
#include<iostream>
#include<assert.h>
#include<vector>
using namespace std;

template<class T>
struct Less
{
    bool operator()(const T& left, const T& right) const
    {
        return left < right;
    }
};

template<class T>
struct Greater
{
    bool operator()(const T& left, const T& right) const
    {
        return left > right;
    }
};


template<class T, class Compare=Less<T>>
class Heap
{
public:
    Heap()//无参的构造函数(系统不会给无参构造函数),开始堆是空的不需要做什么事
    {}
    Heap(T* a, size_t n)
    {
        _a.reserve(n);//开空间
        for (size_t i = 0; i < n; ++i)
        {
            _a.push_back(a[i]);

        }
        //建堆,找最后一个非叶子节点
        for (int i = (_a.size() - 2) / 2; i >= 0; --i)//不用size_t,因为i在这可能等于0,用size_t会死循环
        {
            AdjustDown(i);
        }
    }
    //向下调整
    void AdjustDown(int root)
    {
        Compare com;
        int parent = root;
        size_t child = parent * 2 + 1;//默认为左孩子
        while (child < _a.size())
        {
            //选出小孩子
            //if (child+1 > _a.size() && _a[child + 1]< _a[child])
            if (child + 1 < _a.size() && com(_a[child + 1], _a[child]))
            {
                ++child;
            }

            //if (_a[child] < _a[parent])
            if (com(_a[child], _a[parent]))
            {
                swap(_a[child], _a[parent]);//交换值
                parent = child;
                child = parent * 2 + 1;
            }
            else
            {
                break;
            }
        }
    }
    //向上调整
    void AdjustUp(int child)
    {
        Compare com;
        int parent = (child - 1) / 2;
        while (parent >= 0)
        {
            //if (_a[child] < _a[parent])
            if (com(_a[child], _a[parent]))
            {
                swap(_a[parent], _a[child]);
                child = parent;
                parent = (child - 1) / 2;
            }
            else
            {
                break;
            }
        }

    }
    //最后插入
    void Push(const T&x)
    {
        _a.push_back(x);
        AdjustUp(_a.size() - 1);
    }
    //删除最大数
    void Pop()
    {
        assert(!_a.empty());
        swap(_a[0], _a[_a.size() - 1]);
        _a.pop_back();
        AdjustDown(0);

    }
    //取顶元素
    T& Top()
    {
        assert(!_a.empty());
        return _a[0];
    }
    size_t Size()
    {
        return _a.size();
    }

    bool Empty()
    {
        return _a.empty();
    }


private:
    vector<T> _a;

};
HuffmanTree.h:
#pragma once
#include<iostream>
#include<assert.h>
#include"heap.h"
using namespace std;

template<class W>
struct HuffmanTreeNode
{
    HuffmanTreeNode<W>* _left;
    HuffmanTreeNode<W>* _right;
    HuffmanTreeNode<W>* _parent;

    W _w;
    HuffmanTreeNode(const W& w)
        :_w(w)
        , _parent(NULL)
        , _left(NULL)
        , _right(NULL)
    {}

};

template <class W>
class HuffmanTree
{
    typedef HuffmanTreeNode<W> Node;
public:
    HuffmanTree()
        :_root(NULL)
    {}

    HuffmanTree(W* a, size_t n , const W& invalid)
    {
        assert(a);
        struct NodeCompare
        {
            bool operator()(Node *l, Node *r) //仿函数
            {
                return l->_w < r->_w;
            }
        };
        Heap<Node*, NodeCompare> minHeap;//构造结点但是比较的是权值
        for (size_t i = 0; i < n; ++i)
        {
            if (a[i] != invalid)
            {
                minHeap.Push(new Node(a[i]));
            }
        }

        while (minHeap.Size()>1)
        {
            Node* left = minHeap.Top();
            minHeap.Pop();
            Node* right = minHeap.Top();
            minHeap.Pop();
            Node* parent = new Node(left->_w + right->_w);
            parent->_left = left;
            parent->_right = right;

            left->_parent = parent;
            right->_parent = parent;

            minHeap.Push(parent);
        }
        _root = minHeap.Top();
    }
    size_t Size()
    {
        return minHeap.size();
    }

    Node* GetRoot()
    {
        return _root;
    }
private:
    Node* _root;
};
test.cpp
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include "FileCompress.h"
using namespace std;

void testHuffmanTree()  
{  
    int a[]={0,1,2,3,4,5,6,7,8,9};  
    int size=sizeof(a)/sizeof(a[0]);  
    HuffmanTree<int> ht(a,size,0);  
}  

void testFileCompress()
{
    FileCompress fc;
    fc.Compress("input.txt");
    fc.Uncompress("input.txt.huffman");

}

int main()
{
	testHuffmanTree() ;
	testFileCompress();
	return 0;
}


综合实验: 1. 问题描述 利用哈夫曼编码进行通信可以大大提高信道利用率,缩短信息传输时间,降低传输成本。这要求在发送端通过一个编码系统对待传输数据预先编码,在接收端将传来的数据进行译码(复原)。对于双工信道(即可以双向传输信息的信道),每端都需要一个完整的编/译码系统。试为这样的信息收发站编写一个哈夫曼码的编/译码系统。 2. 基本要求 一个完整的系统应具有以下功能: (1) I:初始化(Initialization)。从终端读入字符集大小n,以及n个字符和n个权值,建立哈夫曼树,并将它存于文件hfmTree中。 (2) E:编码(Encoding)。利用已建好的哈夫曼树(如不在内存,则从文件hfmTree中读入),对文件ToBeTran中的正文进行编码,然后将结果存入文件CodeFile中。 (3) D:译码(Decoding)。利用已建好的哈夫曼树文件CodeFile中的代码进行译码,结果存入文件Textfile中。 (4) P:印代码文件(Print)。将文件CodeFile以紧凑格式显示在终端上,每行50个代码。同时将此字符形式的编码文件写入文件CodePrin中。 (5) T:印哈夫曼树Tree printing)。将已在内存中的哈夫曼树以直观的方式(比如)显示在终端上,同时将此字符形式的哈夫曼树写入文件TreePrint 中。 3. 测试数据 用下表给出的字符集和频度的实际统计数据建立哈夫曼树,并实现以下报文的编码和译码:“THIS PROGRAME IS MY FAVORITE”。 字符 A B C D E F G H I J K L M 频度 186 64 13 22 32 103 21 15 47 57 1 5 32 20 字符 N O P Q R S T U V W X Y Z 频度 57 63 15 1 48 51 80 23 8 18 1 16 1
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值