Huffman编码压缩文件


前言

这个实验是我在学习信息论与编码时所做的课程实验,是用来学习Huffman编码的。整个项目是使用c++在VS2022下编写完成的。工程文件我已经发送我的仓库中,有需要的小伙伴可以通过文章最下面的链接自取。


一、Huffman编码是什么?

Huffman编码是一种无失真的信源编码方式。它通过根据字符出现的概率来分配长码与短码,从而达到降低平均码长的目的,实现压缩效果。

二、Huffman编码的实现方法

Huffman编码的具体实现方法是采用自底向上构造二叉树的方法,就是对所有信源符号按概率从高到低进行排序,每次合并概率最低的两个符号的概率,作为一个新的符号的概率,然后对所有的符号概率再重新排序合并概率最低的两个符号,这一过程一直持续,直到最后概率合并为1.

三、Huffman压缩文件

压缩文件由Encoder类进行实现,Encoder类的定义如下:

#pragma once
#ifndef ENCODER_H
#define ENCODER_H

#include"huffmantree.h"
#include<vector>
#include<fstream>
#include<iostream>
#include <algorithm>
using namespace std;


class Encoder 
{
private :
    const int size = 256;
	vector<int> counter;
	ifstream input;
	ofstream output;
	vector<HuffmanTree*> leaf;
	vector<vector<unsigned char>> p; // 码表
	HuffmanTree* tree_head;
	int file_size;



	bool openFile(string source_file, string target_file);
	void statistics();// 统计各个字符出现次数,并进行排序
	void createTree();// 生成huffman树
	void destroyTree(HuffmanTree* root);
	void creatTable(HuffmanTree* node, vector<char> temp, int len);
	
public:
	Encoder();
	void compress(string source_file, string target_file); // 压缩编码
	~Encoder();

};


#endif

HuffmanTree的定义如下

#pragma once
#ifndef HUFFMANTREE_H
#define HUFFMANTREE_H

struct  HuffmanTree
{
	HuffmanTree* lchild;
	HuffmanTree* rchild;
	int weight;  //出现次数
	unsigned char data;
	unsigned char code;  // 左1右0
};

#endif

下面我将重点说明一下statistics()、createTree()、creatTable()、和 compress()

1.统计文件个字符出现的次数

void Encoder::statistics()
{
		unsigned char buff;
		while (input >> noskipws >> buff)
		{
			counter.at(buff)++;
			file_size++;
		}
		cout << "the size of file is " << file_size << " byte"<<endl;
		input.clear();
		input.seekg(0, ios::beg);	//将文件指针指向开头
}

这里的文件需要用二进制打开,否则在Windows下会出现文档中0X1A被认作为EOF的情况,使得读取文件异常终止。

2.生成Huffman树

代码如下:

void Encoder::createTree()
{	
	for (int i = 0; i < size; ++i)
	{
		leaf.at(i) = new HuffmanTree;
		leaf.at(i)->weight = counter.at(i);
		leaf.at(i)->code = i;
		leaf.at(i)->data = i;
		leaf.at(i)->lchild = NULL;
		leaf.at(i)->rchild = NULL;
	} //初始化
	HuffmanTree* lnode;
	HuffmanTree* rnode;

	HuffmanTree *f;
	//f = new HuffmanTree[size];
	while (leaf.size() > 1)
	{
		f = new HuffmanTree;
		//从大到小排序,每次取最后两个
		sort(leaf.begin(), leaf.end(), [=](HuffmanTree* i1, HuffmanTree* i2) {
			return i1->weight > i2->weight;
			}); 
		lnode = leaf.back();
		leaf.pop_back();
		rnode = leaf.back();
		leaf.pop_back();

		f->lchild = lnode;
		lnode->code = 1; //左1右0
		f->rchild = rnode;
		rnode->code = 0;
		f->weight = rnode->weight + lnode->weight;
		f->code = 0;
		leaf.push_back(f);
		if (leaf.size() == 1)
		{
			tree_head = f;
			leaf.pop_back();
			return;
		}
		f++;
	}
	return ;
}

对所有信源符号按概率从高到低进行排序,每次合并概率最低的两个符号的概率,作为一个新的符号的概率,然后对所有的符号概率再重新排序合并概率最低的两个符号

3.生成码表

void Encoder::creatTable(HuffmanTree* node, vector<char> temp, int len)
{   

	if (node != NULL)
	{
		if (node->lchild == NULL && node->rchild == NULL)  
		{
			temp.at(len) = node->code;
			p.at(node->data).assign(temp.begin()+1, temp.begin()+len+1);//用来存放码表
		}
		else
		{
			temp.at(len++) = node->code;
			creatTable(node->lchild, temp, len);
			creatTable(node->rchild, temp, len);
		}
	}
}

利用递归遍历二叉树来生成码表

4.对文件进行压缩

void Encoder::compress(string source_file, string target_file)
{
	vector<char> temp(size, 0);
	unsigned char buff;  //存放读入的字符
	unsigned char out = 0; //输出的字符
	vector<char> code;  //存放字符的编码

	openFile(source_file, target_file); //打开输入输出文件,必须要用二进制打开
	statistics(); //统计各个字符出现次数
	createTree();//生成Huffman树
	creatTable(tree_head, temp, 0);//构造码表

	for (vector<int>::iterator it = counter.begin(); it != counter.end(); ++it)
	{
		output << *it << endl;
	}//写入每个字符出现的次数,解压时根据这个构建Huffman树
	while (input >> noskipws >> buff)
	{
		code.insert(code.end(), p[buff].begin(), p[buff].end());
		while (code.size() >= 8) //当编码中0和1位数超过8时,将他们转化成一个字符输出
		{
			out = 0;
			for (int j = 0; j < 8; ++j)
			{
				out = out ^ (code.at(j) << j);
			}
			output << out;

			vector<char>::const_iterator First = code.begin() + 8; 
			vector<char>::const_iterator Second = code.end();
			code.assign(First, Second);  //将输出的编码删除

		}
	}
	if (code.size() != 0) //多出来不足八位的编码进行补零操作
	{
		for (int j = 0; j < code.size(); ++j)
		{
			out = out ^ (code.at(j) << j);
		}
		output << out;
		output << code.size(); //这个记录的是最后字符的长度,解压其实并没有用到这个信息,可以删除
	}
	output.close();
	input.close();
}

压缩时一定要将码表先写入到压缩文件中,否者无法对文件进行解压缩,同时写入字符对应的编码时,编码中的0和1是要当作位信息写入的,我这里将编码后的0和1每八个为一组当成一个字节写入到文件中去的,最后不足8位的进行补零。

四、Huffman解压文件

void Decoder::deCompress(string sourece_file,string target_file) // 解压
{
	HuffmanTree* p;
	unsigned char buff;
	int count = 0;
	int temp;
	openFile(sourece_file, target_file);
	statistics();
	createTree();
	p = root;
	for (vector<int>::iterator it = counter.begin(); it != counter.end(); ++it)
	{
		file_size += *it;
	}
	input >> noskipws >> buff;
	while (input >> noskipws >> buff)
	{
	
		for (int i = 0; i < 8; ++i)
		{

			if (!(p->lchild == NULL && p->rchild == NULL))
			{
				temp = (buff & (1 << i)) ? 1 : 0;
				//cout << temp;
				if (temp == 0)
				{
					p = p->rchild;
				}
				else
					p = p->lchild;
			}
			else
			{
				output << p->data;
				p = root;
				count++;
				if (count == file_size)
				{
					cout << "decompress successfully";
					return;
				}
			}		
		}
	}
	input.close();
	output.close();
}

解压文件的代码大部分和压缩文件时的一样,这里比较重要的代码如上所示。主要是以二进制形式读取压缩文件中的每一个字符,并且将每一个字符按位拆分为01序列。从树的根开始,0寻找右子树,1寻找左子树,直到找到一个叶子节点,说明译码成功。将指针重新指向根节点重复该过程即可完成解压。

五、实验结果

#include"decoder.h"
#include"encoder.h"
//#include"vld.h"
int main()
{
	Encoder en;
	Decoder de;
	string source =R"(.\img\2.bmp)"; //目标文件
	string temp = R"(.\output\2.bin)"; //压缩后文件
	string target = R"(.\output\2.bmp)"; //解压后文件
	en.compress(source, temp);
	de.deCompress(temp, target);
	return 1;
}

在这里插入图片描述
该程序使用二进制读取文件,理论上来说任何格式的文件都可以进行压缩。如上图,对一个80M的mp4文件进行压缩,压缩出的文件只有18M,但是整个程序的运行时间大约需要三分钟!


总结

这次实验涉及的内容十分广泛:对文件的读取、排序算法、二叉树等等。
我将整个项目放在了我的仓库中,有需要的伙伴可以自取。
Huffman编码对不同的文件压缩效率也不一样,可以更换不同的文件试一试它的压缩效率。
https://github.com/logic8126/huffman-coding

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值