基于哈夫曼树的文件压缩与解压
什么是哈曼夫树
哈夫曼树(Huffman Tree)是一种带权路径长度最短的树,也称最优二叉树,是经典的数据压缩算法中的核心思想。
哈夫曼树的构建过程:
-
根据给定的n个权值 {w1, w2, …, wn},构造n棵只有一个节点的二叉树(这些节点我们可以视为叶子节点),每棵二叉树的根节点的权值已经设定为对应权值。
-
在森林里选取两棵根节点的权值最小的树进行合并。得到一棵新树,该树的根节点权值为原来的两棵树的根节点权值之和。
-
将新树放回森林中,并将原来两棵树从森林列表中删除。
-
重复执行步骤2和3,直到森林里只有一棵树为止。该树即为哈夫曼树。
通过这种构建方法,权值较小的节点会离根节点较近,权值较大的节点会离根节点较远,因此该树的带权路径长度(WPL)是最小的,具有最优性。
在数据压缩中,利用哈夫曼树的这种带权路径长度最短的特点,可以将原始数据中出现较多的符号用较短的编码表示,而将出现较少的符号用较长的编码表示,从而有效地减少了存储或传输的数据量。
HaffmanTree.h
#pragma once
/*
哈夫曼树
*/
#include<queue>
#include<vector>
using namespace std;
template <class T>
struct HuffmanTreeNode //构建哈夫曼树的节点
{
HuffmanTreeNode<T>* left; //左指针域
HuffmanTreeNode<T>* right; //右指针域
HuffmanTreeNode<T>* parent;//双亲节点指针域
T weight; //权值
HuffmanTreeNode(const T& w) //构造函数
: left(nullptr)
, right(nullptr)
, parent(nullptr)
, weight(w)
{}
};
//因为使用默认的优先级队列采用的是less的比较方式,建的是大堆
//因此用到仿函数的方式给priority_queue的第三个参数传参,需要自定义比较方式,按照节点中的权值大小来比较
template <class T>
struct Compare
{
typedef HuffmanTreeNode<T> Node;
bool operator()(const Node* left, const Node* right)
{
return left->weight > right->weight;
}
};
template <class T>
class HuffmanTree
{
typedef HuffmanTreeNode<T> Node;//重命名
public:
HuffmanTree() //构造函数
:root(nullptr)
{}
//析构函数
~HuffmanTree()
{
DestroyHuffmanTree(root);
}
//哈夫曼树的创建
void CreateHuffmanTree(const T array[], size_t size, const T& invalid)
{
//使用优先级队列来保存哈夫曼树
std::priority_queue<Node*, vector<Node*>, Compare<T>> q;
//1.使用权值创建二叉树森林
for (size_t i = 0; i < size; ++i)
{
//使用if判断节点是否为0(无效节点)
if (array[i] != invalid)
q.push(new Node(array[i]));
}
//循环进行以下步骤
while (q.size() > 1)
{
//1.取权值最小的两颗二叉树
Node* left = q.top();
q.pop();
Node* right = q.top();
q.pop();
//2.构建新二叉树,以左右子树中的权值之和作为新二叉树的权值
Node* parent = new Node(left->weight + right->weight);
parent->left = left;
parent->right = right;
left->parent = parent;
right->parent = parent;
//3.将新二叉树放入到二叉树森林中
q.push(parent);
}
root = q.top();
}
Node* GetRoot()
{
return root;
}
//销毁哈夫曼树
void DestroyHuffmanTree(Node*& proot)
{
if (proot != nullptr)
{
DestroyHuffmanTree(proot->left);
DestroyHuffmanTree(proot->right);
proot = nullptr;
}
}
private:
Node* root;//根节点
};
基于哈曼夫树的文件压缩和解压
File_Compress_UnCompress.h
#pragma once
#include<iostream>
#include<string>
#include"HaffmanTree.h"
using namespace std;
struct ByteInfo {
unsigned char ch;//字符
int count;//字符出现的次数
string strcode;//字符对应的哈夫曼编码
//构造函数
ByteInfo(int BIcount = 0)
:count(BIcount)
{}
//在使用ByteInfo实例化对象时,需要对+, >, !=, ==进行重载
ByteInfo operator+(const ByteInfo& b)const {
ByteInfo tmp;
tmp.count = count + b.count;
return tmp;
}
bool operator>(const ByteInfo& b)const {
if (count > b.count) {
return true;
}
else {
return false;
}
}
bool operator!=(const ByteInfo& b)const {
if (count != b.count) {
return true;
}
else {
return false;
}
}
bool operator==(const ByteInfo& b)const {
if (count == b.count) {
return true;
}
else {
return false;
}
}
};
class FileCompress {
public:
//构造函数
FileCompress();
//压缩方法
bool Compress(const string& filepath);
//解压缩方法
bool UnCompress(const string& filepath);
void menu();
private:
void GetLine(FILE* FIn, string& strContent);//C语言中没有获取一行的函数,需自己进行封装
void CreateHuffmanCode(HuffmanTreeNode<ByteInfo>* root);
void CompressInfo(FILE* fl, const string& filepath);
//文件在磁盘中是以字节方式存储的,因此使用
//包含256个ByteInfo类型的数组来保存字节出现的信息
ByteInfo filebyteinfo[256];
};
File_Compress_UnCompress.cpp
#define _CRT_SECURE_NO_WARNINGS
#include"File_Compress_UnCompress.h"
#include"HaffmanTree.h"
FileCompress::FileCompress() {
for (int i = 0; i < 256; i++) {
filebyteinfo[i].ch = i;
}
}
bool FileCompress::Compress(const string& filepath) {
//1.获取原文件中每个字节出现的次数
//打开文件
//不存在从string到constchar*的转换函数的解决方法
//使用cstr()函数,cstr函数的返回值是const
//c_str(()函数返回一个指向正规C字符串的指针..
FILE* fp = fopen(filepath.c_str(), "rb");
if (fp == nullptr) {//如果文件为空或者没有文件,输出"fopen: No such file or directory"
perror("fopen");
return false;
}
//读取文件内容,采用循环,一次读取1024个字节
unsigned char readbuff[1024];//设置缓冲区
while (1) {
//fread函数的返回值,表示成功读取的字节数量
//用read_size来接收成功读取的字节数
size_t read_size = fread(readbuff, 1, 1024, fp);
if (read_size == 0) {
break;//read_size = 0表示已经读到了文件末尾
}
//统计出现字符的次数
for (size_t i = 0; i < read_size; ++i) {
//利用字符的ASCII码值作为数组的下标统计次数
filebyteinfo[readbuff[i]].count++;
}
}
//2.根据字节出现的频次信息构建Huffman树
HuffmanTree<ByteInfo> ht;
ByteInfo invalid;
ht.CreateHuffmanTree(filebyteinfo, 256, invalid);
//3.获取Huffman编码
CreateHuffmanCode(ht.GetRoot());
string filename_Compress;
cout << "输入需要解压缩文件后的文件名" << endl;
cin >> filename_Compress;
//string PostFixs = ".bmp";
//filename_Compress += PostFixs;
//4.保存压缩信息(方便解压缩时对信息进行读取)
FILE* fl = fopen(filename_Compress.c_str(), "wb");//fl文件指针指向打开的文件2.txt
CompressInfo(fl, filepath);
//5.使用Huffman编码来改写文件
//在对文件进行读操作后,文件指针的位置指向文件末尾
//注意,这里需要将文件指针重置到文件头部
fseek(fp, 0, SEEK_SET);//使用rewind(fp)函数也行
unsigned char ch = 0;//记录按位或后的字符
int bitcount = 0;//记录按位或的比特位长度
//采用循环读文件内容
while (1) {
//fread函数的返回值,表示成功读取的字节数量
//用read_size来接收成功读取的字节数
size_t read_size = fread(readbuff, 1, 1024, fp);
if (read_size == 0) {
break;//read_size = 0表示已经读到了文件末尾
}
for (size_t i = 0; i < read_size; ++i) {
//获取filebyteinfo数组中的编码
string& strcode = filebyteinfo[readbuff[i]].strcode;
for (size_t j = 0; j < strcode.size(); j++) {
ch <<= 1;//先将ch左移一位
if (strcode[j] == '1') {//再让strcode的每个比特位与1进行或操作
ch |= 1;
}
bitcount++;//每按位或一次就对bitcount++
if (bitcount == 8) {//一个字节放满8个比特位
fputc(ch, fl);//往文件中进行写操作
bitcount = 0;//比特位计数置0
}
}
}
}
//检测ch放置的比特位个数,不够8个继续往文件中写
if (bitcount > 0 && bitcount < 8) {
ch <<= (8 - bitcount);
fputc(ch, fl);
}
int fpSize = ftell(fl);
int outSize = ftell(fp);
cout << "\n***********************************\n" << endl;
cout << "压缩前:" << fpSize * 1024 << "字节" << endl;
cout << "压缩后:" << outSize * 1024 << "字节" << endl;
cout << "***********************************" << endl;
fclose(fp);//关闭文件
fclose(fl);
cout << "压缩成功" << endl;
return true;
}
bool FileCompress::UnCompress(const string& filepath) {
//1.获取压缩信息
FILE* FIn = fopen(filepath.c_str(), "rb");//以读的方式打开文件
if (FIn == nullptr) {//如果文件为空或者没有文件,输出"fopen: No such file or directory"
perror("fopen");
return false;
}
//①读后缀
string PostFix;
GetLine(FIn, PostFix);
//②读总行数,读取
string strContent;
GetLine(FIn, strContent);
size_t linecount = atoi(strContent.c_str());
strContent = "";//先将strContent清空
for (size_t i = 0; i < linecount; ++i) {
GetLine(FIn, strContent);
if (strContent == "") {//当读取到的是一个换行(对多行数据进行处理)
strContent += "\n";
GetLine(FIn, strContent);
}
//以strContent[0]即出现的字符作为filebyteinfo数组的下标
filebyteinfo[(unsigned char)strContent[0]].count = atoi(strContent.c_str() + 2);//+2的目的是向后偏移两个位置
strContent = "";
}
//2.还原哈夫曼树
HuffmanTree<ByteInfo> ht;
ByteInfo invalid;
ht.CreateHuffmanTree(filebyteinfo, 256, invalid);
//3.读取压缩数据,进行解压缩
string filename_UnCompress;
cout << "输入需要解压缩文件后的文件名" << endl;
cin >> filename_UnCompress;
//filename_UnCompress += PostFix;
FILE* fout = fopen(filename_UnCompress.c_str(), "wb");
unsigned char readbuff[1024];//缓冲区
unsigned char bitcount = 0;
HuffmanTreeNode<ByteInfo>* cur = ht.GetRoot();//cur指向哈夫曼树的根节点
const int FileSize = cur->weight.count;//根节点的权值即位原文件的大小
int CompressSize = 0;
while (1) {
size_t read_size = fread(readbuff, 1, 1024, FIn);
if (read_size == 0) {
break;
}
for (size_t i = 0; i < read_size; i++) {
unsigned char ch = readbuff[i];//ch来保存压缩后的每一个字节
bitcount = 0;
while (bitcount < 8) {
if (ch & 0x80) {//每个比特位与1000 0000按位与
cur = cur->right;
}
else {
cur = cur->left;
}
if (cur->left == nullptr && cur->right == nullptr) {//走到了叶子节点的位置
fputc(cur->weight.ch, fout);
cur = ht.GetRoot();//回到根节点的位置
CompressSize++;
if (CompressSize == FileSize) {//判断解压缩后的结果与原文件大小是否相同
break;
}
}
bitcount++;
ch <<= 1;
}
}
}
int fpSize = ftell(fout);
int outSize = ftell(FIn);
cout << "\n***********************************\n" << endl;
cout << "解压缩前:" << fpSize * 1024 << "字节" << endl;
cout << "解压缩后:" << outSize * 1024 << "字节" << endl;
cout << "***********************************" << endl;
fclose(FIn);
fclose(fout);
cout << "解压缩成功" << endl;
return true;
}
void FileCompress::GetLine(FILE* FIn, string& strContent) {
unsigned char ch;
while (!feof(FIn)) {
ch = fgetc(FIn);
if (ch == '\n') {
break;
}
else {
strContent += ch;
}
}
}
void FileCompress::CreateHuffmanCode(HuffmanTreeNode<ByteInfo>* root) {
if (root == nullptr) {
return;
}
//遍历到叶子节点所经过的路径就是Huffman编码
if (root->left == nullptr && root->right == nullptr) {
//使用从叶子节点找到根节点的方式获取哈夫曼编码
HuffmanTreeNode<ByteInfo>* cur = root;//root标记叶子节点
HuffmanTreeNode<ByteInfo>* parent = cur->parent;//parent为叶子节点的双亲
//这行代码可能不容易理解,分析:
//我们遍历Huffman树获取到的编码最终要保存到filebyteinfo数组,
//数组中每个元素为ByteIofo结构体,需要保存到结构体中的strcode中
//以当前节cur点中的权值所对应的字符下标作为filebyteinfo数组的下标
//这里使用string&是引用,相当于取了别名
string& strcode = filebyteinfo[cur->weight.ch].strcode;
//走到根节点时双亲为空
while (parent != nullptr) {
if (cur == parent->left) {
strcode += '0';
}
else {
strcode += '1';
}
//cur和双亲parent继续向上标记
cur = parent;
parent = cur->parent;
}
//从叶子节点向上走到根节点获取的Huffman编码为反的,需要进行逆置
reverse(strcode.begin(), strcode.end());
}
//递归遍历整个huffman树
CreateHuffmanCode(root->left);
CreateHuffmanCode(root->right);
}
void FileCompress::CompressInfo(FILE* fl, const string& filepath) {
//1.截取文件后缀,如:1.txt需要截取txt
string PostFix = filepath.substr(filepath.rfind('.'));//substr截取字串,rfind用来从后往前寻找.到末尾
PostFix += "\n";//截取完加上换行让一条信息占一行
fwrite(PostFix.c_str(), 1, PostFix.size(), fl);//往文件中写
//2.记录频次信息占的总行数及频次信息
size_t LineCount = 0;//记录行数
string chcount;//字符出现次数
for (size_t i = 0; i < 256; ++i) {
if (filebyteinfo[i].count > 0) {
chcount += filebyteinfo[i].ch;
chcount += ':';
chcount += to_string(filebyteinfo[i].count);//将数字转换成字符串
chcount += "\n";
LineCount++;
}
}
string TLcount = to_string(LineCount);
TLcount += "\n";
fwrite(TLcount.c_str(), 1, TLcount.size(), fl);
fwrite(chcount.c_str(), 1, chcount.size(), fl);
}
menu.cpp
#include"File_Compress_UnCompress.h"
#include<vector>
void FileCompress::menu() {
while (1) {
printf("===========欢迎使用本程序===========\n");
fflush(stdin);/*清空文件缓冲区*/
cout << "1.创建压缩文件" << endl;
cout << "2.解压缩文件" << endl;
cout << "3.退出" << endl;
printf("====================================\n");
printf("请输入要执行的步骤:\n");
char a = '0';
cin >> a;
switch (a) {
case '1': {
cout << "创建压缩文件" << endl;
FileCompress file;
cout << "输入需要压缩文件的文件名" << endl;
string filename_Compress_s;
cin >> filename_Compress_s;
file.Compress(filename_Compress_s);
break;
}
case '2': {
cout << "解压缩文件" << endl;
FileCompress file;
cout << "输入需要解压缩文件的文件名" << endl;
string filename_UnCompress_s;
cin >> filename_UnCompress_s;
file.UnCompress(filename_UnCompress_s);
break;
}
case '3': {
cout << "\n成功退出\n" << endl;
return;
}
default: {
cout << "\n========Error=======" << endl;
cout << "输入错误请重新输入\n" << endl;
break;
}
}
cin.ignore(100, '\n');//把回车(包扩回车)之前的所以字符从输入缓冲流中清除出去
}
}
main.cpp
#include "File_Compress_UnCompress.h"
int main() {
FileCompress H_file;
H_file.menu();
return 0;
}