C++文件压缩及解压缩小程序的实现

基于HuffmanTree的文件压缩以及解压缩小程序

前言

本程序的设计初衷旨在复习,熟悉数组方式创建HuffmanTree,同时练习C++文件操作相关语法,C++声明与定义分离的特性,并尽力实现识别所有特殊字符,在此基础上,模拟实现现在正常软件的压缩和解压缩的最基础功能。事实上,直接真正创建树结构的难度反而低一些,且部分操作会简略不少,也更方便检查,代码中有注释,并且实现的整体难度也不大,这里直接给出源码

源码

HuffmanTree.h

cpp 复制代码
//思路的话,有两种,一种是直接创建真正的二叉树,但因为这种思路过于简单无趣,暂且搁置,最终决定使用法二:使用vector模拟二叉树
#pragma once
#include<iostream>
#include<unordered_map>
#include<string>
#include<vector>
#include<unordered_set>
//using namespace std;
namespace dzh
{
	enum STATUS
	{
		EMPTY = 1,
		FULL = 2
	};
	//小堆的比较用仿函数(小堆用大于)
	struct Compare
	{
		bool operator()(std::pair<STATUS, std::pair<char, int>> a, std::pair<STATUS, std::pair<char, int>> b);
	};
	struct HuffmanNode
	{
		HuffmanNode(int weight, char data, STATUS status = EMPTY, int left = -1, int right = -1, int parent = -1);
		int _weight = -1;
		int _left = -1;
		int _right = -1;
		int _parent = -1;
		char _data = ' ';
		STATUS _status = EMPTY;
	};
	std::ostream& operator<<(std::ostream& os, const HuffmanNode& t);
	class HuffmanTree
	{
	public:
		HuffmanTree(std::string filename);//创建HuffmanTree以及编码表的构造函数
		void EnCode(std::string codeFileName);//压缩文件
		void DeCode(std::string codeFileName, std::string deCodeFileName);//解压文件
		void PrintTree();//打印HuffmanTree的信息
		void PrintTable();//打印编码表
		size_t UniqueCharCount();//获取唯一字符的个数
		size_t SourceCharCount();//获取源文件的字符个数
	private:
		std::vector<HuffmanNode> _root;
		std::unordered_map<char, std::string> _table;
		std::unordered_map<std::string, char> _retable;
		size_t _uniqueCharCount = 0;
		size_t _sourceCharCount = 0;
		std::string _fileName = "";
		std::unordered_set<std::string> _codeFileName;
	};

	void Random();//实现的ABCD按给定权值随机生成至文件中的测试用函数
	void test1();
	void test2();
}

HuffmanTree.cpp

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
//遇到中文字符直接断言的问题已经解决,原因是微软的isspace函数具备断言检查,详情见同目录下的"C++_char与unsignedchar_强转_字符操作函数相关.cpp"
//而文件操作(包括二进制文件)时就老老实实使用char类型,当需要检查时再用static_cast关键字强转为unsigned char
#include"HuffmanTree.h"
#include<iostream>
#include<queue>
#include<unordered_map>
#include<string>
#include<vector>
#include<fstream>
#include<assert.h>
#include<stack>
#include<unordered_set>
#include<limits>
#include<cctype>//isspace函数所在头文件
#include<random>
#include<algorithm>//shuffle函数所在头文件
using namespace std;

namespace dzh
{
	bool Compare::operator()(pair<STATUS, pair<char, int>> a, pair<STATUS, pair<char, int>> b)
	{
		return a.second.second > b.second.second;
	}

	HuffmanNode::HuffmanNode(int weight, char data, STATUS status, int left, int right, int parent)
		:_weight(weight)
		, _left(left)
		, _right(right)
		, _parent(parent)
		, _data(data)
		, _status(status)
	{
	}

	ostream& operator<<(ostream& os, const HuffmanNode& t)
	{
		//使用isspace函数判断空白字符,若为真则输出" "
		if (isspace(static_cast<unsigned char>(t._data)))
		{
			os << "权重:" << t._weight << " 左孩子:" << t._left << " 右孩子:" << t._right << " 父母:" << t._parent << " 字符:" << " " << endl;
		}
		else
		{
			os << "权重:" << t._weight << " 左孩子:" << t._left << " 右孩子:" << t._right << " 父母:" << t._parent << " 字符:" << t._data << endl;
		}
		return os;
	}
	HuffmanTree::HuffmanTree(string filename)
		:_fileName(filename)
	{
		unordered_map<char, int> m;
		//		ofstream ofs(filename);
		ifstream ifs(filename, ios_base::binary | ios_base::in);
		//读取文件,并使用哈希计算每个字符出现的次数
		if (ifs.is_open())
		{
			char c = ' ';
			while (ifs.read(reinterpret_cast<char*>(&c), 1))
			{
				++_sourceCharCount;
				++m[c];
			}
		}
		else
		{
			assert(false);
		}
		ifs.close();

		//注意判断一下文件中没有数据的情况,直接停止程序
		if (m.empty())
		{
			cout << "文件中没有数据!" << endl;
			assert(false);
		}
		_uniqueCharCount = m.size();//将哈希中的size赋值给_uniqueCharCount

		//将哈希中的数据入小堆
		priority_queue<pair<STATUS, pair<char, int>>, vector<pair<STATUS, pair<char, int>>>, Compare> q;
		for (const auto& e : m)
		{
			q.emplace(pair<STATUS, pair<char, int>>(FULL, e));
		}
		//将小堆中的数据依次存放入vector,即创建仿二叉树
		//此过程共分为两步,第一步是将创建的所有节点全部放入vector
		//第二步是为vector中的每个节点找到对应的parentnode

		//第一步
		while (q.size() > 1)//当堆中数据个数为1时,说明树已经创建完毕了
		{
			//	cout << q.size() << endl;
			HuffmanNode n1(q.top().second.second, q.top().second.first, q.top().first);
			q.pop();
			HuffmanNode n2(q.top().second.second, q.top().second.first, q.top().first);
			q.pop();
			pair<STATUS, pair<char, int>> parentnode(EMPTY, pair<char, int>(' ', n1._weight + n2._weight));
			q.emplace(parentnode);
			_root.emplace_back(n1);
			_root.emplace_back(n2);
		}
		assert(!q.empty());
		_root.emplace_back(q.top().second.second, q.top().second.first, q.top().first);//将堆中剩下的根节点放到vector中
		q.pop();

		//第二步
		//由于上面使用优先级队列进行vector的插入,所以,现在vector中的数据应是根据weight的值进行从小到大排列的,因此,现在仅需要每次循环都依次访问两个数据
		for (size_t i = 0; _root.size() != 0 && i < _root.size() - 1; i += 2)//最先要注意的就是size==0的问题,由于返回值是size_t,所以要极力避免size == 0 && size - 1的情况
		{
			for (size_t j = i + 2; j < _root.size(); ++j)//由于vector是按照从小到大的顺序排列的,所以直接从i+2的位置开始搜索权重合适且data为空的节点
			{
				if (_root[j]._weight == _root[i]._weight + _root[i + 1]._weight && _root[j]._status == EMPTY && _root[j]._left == -1 && _root[j]._right == -1)
				{
					_root[i]._parent = j;
					_root[i + 1]._parent = j;
					_root[j]._left = i;
					_root[j]._right = i + 1;
					break;
				}
			}
		}

		//建好模拟树后,开始建立编码表与反编码表
		_table.reserve(_uniqueCharCount * 4 / 3);//利用_uniqueCharCount提前扩充好哈希表(注意,一般负载因子是0.75,所以这里使用负载因子的倒数),避免后续可能的多次扩充造成的资源浪费
		_retable.reserve(_uniqueCharCount * 4 / 3);
		string codes = "";
		stack<char> sk;
		char mid = '0';
		//assert(_root.empty());
		for (size_t i = 0; i < _root.size(); ++i)
		{
			const auto& e = _root[i];
			//cout << e._data << endl;
			if (e._status != EMPTY)
			{
				//cout << 1 << endl;
				int child = i;
				int parent = e._parent;
				while (parent != -1)
				{
					if (&_root[(_root)[parent]._left] == &_root[child])//使用地址进行左右节点的比较,先开始使用的data进行比较,但因为中间节点的data均为' ',会导致无法正确区分,想到了可以直接使用地址进行比较
					{
						mid = '0';
					}
					else
					{
						mid = '1';
					}
					//		cout << mid << endl;
					sk.emplace(mid);
					child = parent;
					parent = _root[parent]._parent;
				}
				while (!sk.empty())
				{
					codes += sk.top();
					//	cout << sk.top();
					sk.pop();
				}
				//cout << codes << endl;
				_table.emplace(e._data, codes);
				_retable.emplace(codes, e._data);
				codes = "";
			}
		}
	}

	//使用二进制形式写入01,是最终的结果函数
	void HuffmanTree::EnCode(string codeFileName)
	{
		//一个源文件可能需要被压缩成多份,因此使用哈希存储压缩文件的文件名,方便解压时判断文件是否是被该类压缩的
		while (codeFileName == _fileName)
		{
			cout << "不能将源文件压缩至源文件,请重新输入一个非源文件的文件名!";
			cin >> codeFileName;
		}
		_codeFileName.emplace(codeFileName);
		ifstream ifs(_fileName, ios_base::binary | ios_base::in);
		ofstream ofs(codeFileName, ios_base::binary | ios_base::out);
		char ch = ' ';//用于读取源文件中的字符
		unsigned char buf = 0;//用于记录二进制位
		string s = "";
		int count = 7;//用于记录当前char未被使用的比特位数(范围为0~7)
		while (ifs.read(reinterpret_cast<char*>(&ch), 1))
		{
			s = _table[ch];
			for (const auto& e : s)
			{
				size_t bitValue = e - '0';
				buf |= (bitValue << count);
				--count;
				if (count == -1)
				{
					ofs.write(reinterpret_cast<const char*>(&buf), 1);
					buf = 0;
					count = 7;
				}
			}
		}

		//记得处理最后存储在buf中但比特位还未使用完全的数据
		if (count != 7)
		{
			ofs.write(reinterpret_cast<const char*>(&buf), 1);
		}
		ofs.close();
		ifs.close();
		cout << "压缩成功!" << endl;
	}
	void HuffmanTree::DeCode(string codeFileName, string deCodeFileName)
	{
		if (_codeFileName.find(codeFileName) == _codeFileName.end())
		{
			cout << "所解压的文件不是由当前对象压缩的!" << endl;
			return;
		}
		ifstream ifs(codeFileName, ios_base::binary | ios_base::in);
		assert(ifs.is_open());
		ofstream ofs(deCodeFileName, ios_base::out);
		char ch = ' ';//用于将转码后的字符写入文件
		unsigned char buf = 0;//用于记录二进制位
		string decodedOutput = "";//用于读取二进制中的比特位
		int count = 7;//用于记录当前已经读取完毕的比特位(范围为0~7)
		size_t num = 0;//记录成功转换的字符个数,与压缩时的总字数对比,相等则停止转换
		while (ifs.read(reinterpret_cast<char*>(&buf), 1))
		{
			while (count > -1)
			{
				if (num == _sourceCharCount)//读取到最后一个字节,可能出现bit中遇到填充用的二进制的0的情况,此时仅需break内部循环即可,因为此时已是最后一次外部循环
				{
					break;
				}
				char binNum = ((buf >> count) & 1) + '0';//这里一定注意运算符优先级问题,要把1放到小括号中,先与&运算,再+
				--count;
				decodedOutput += binNum;
				if (_retable.find(decodedOutput) != _retable.end())
				{
					ch = (_retable)[decodedOutput];
					//			cout << ch << " ";
					ofs << ch;
					ch = ' ';
					decodedOutput = "";
					++num;
				}
			}
			count = 7;
		}
		if (ifs.eof())
		{
			cout << "文件解压成功!" << endl;
		}
		else if (ifs.fail())
		{
			cout << "文件解压失败!" << endl;
		}
		else if (ifs.bad())
		{
			cout << "ifstream出现系统级错误!" << endl;
		}
		else
		{
			assert(false);
		}
	}
	void HuffmanTree::PrintTree()
	{
		for (size_t i = 0; i < _root.size(); ++i)
		{
			cout << "当前节点的序号为:" << i << " " << _root[i] << endl;
		}
	}
	void HuffmanTree::PrintTable()
	{
		for (const auto& e : _table)
		{
			//使用isspace函数判断空白字符,若为真则输出" "
			if (isspace(static_cast<unsigned char>(e.first)))
			{
				cout << "字符为:" << " " << " 编码为:" << e.second << endl;
				continue;
			}
			cout << "字符为:" << e.first << " 编码为:" << e.second << endl;
		}
	}
	size_t HuffmanTree::UniqueCharCount()
	{
		return _uniqueCharCount;
	}
	size_t HuffmanTree::SourceCharCount()
	{
		return _sourceCharCount;
	}

	void Random()
	{
		std::ofstream ofs("chartest.txt", std::ios_base::out);
		std::random_device rd;
		std::mt19937 gen(rd());

		int num1, num2, num3, num4;
		cout << "请依次输入A,B,C,D的权值:";
		cin >> num1 >> num2 >> num3 >> num4;
		std::unordered_map<char, int> um{ {'A', num1}, {'B', num2}, {'C', num3}, {'D', num4} };
		std::vector<char> vc;

		for(const auto& e : um)
		{
			vc.insert(vc.end(), e.second, e.first);
		}

		std::shuffle(vc.begin(), vc.end(), gen);

		for (const auto& e : vc)
		{
			ofs << e;
		}
		//RAII
	}

	void test1()
	{
		Random();
		cin.ignore(numeric_limits<streamsize>::max(), '\n');

		HuffmanTree ht("chartest.txt");
		cout << "总字符数为:" << ht.SourceCharCount() << endl;
		cout << "HuffmanTree为:" << endl;
		ht.PrintTree();
		cout << "编码表为:" << endl;
		ht.PrintTable();
		cout << "请输入压缩文件的文件名:" << endl;
		string codeFileName;
		cin >> codeFileName;
		cin.clear();
		cin.ignore(numeric_limits<streamsize>::max(), '\n');
		ht.EnCode(codeFileName);
		cout << "请输入需要解压的文件的文件名以及接收解压后文件的文件名:" << endl;
		string deCodeFileName;
		cin >> codeFileName >> deCodeFileName;
		cin.clear();
		cin.ignore(numeric_limits<streamsize>::max(), '\n');
		ht.DeCode(codeFileName, deCodeFileName);
	}
	void test2()
	{
		Random();
		HuffmanTree ht("HuffmanTree_test.txt");
		string codeFileName;
		ht.PrintTable();
		ht.PrintTree();
		ht.EnCode("zip.txt");
		ht.DeCode("zip.txt", "yuanwenjian.txt");
		cout << ht.SourceCharCount() << "\n";
	}
}

test.cpp

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include"HuffmanTree.h"
#include<iostream>
using namespace std;
using namespace dzh;
int main()
{
	try
	{
		test1();
		//test2();
		return 0;
	}
	catch (const std::bad_alloc& errid)
	{
		cout << "内存申请失败!" << endl;
	}
	catch (...)
	{
		cout << "出现未知异常!" << endl;
	}
}

运行结果

可以发现,程序运行成功,结果也正确

注意点

  1. 程序运行第一个让输入的程序名是压缩文件的文件名,就是打算把源文件压缩到哪里,就像正常压缩软件,都是把源文件压缩成一个新文件,而非直接覆盖源文件
  2. 比如说源文件是test.txt,那可以把源文件压缩到zip.txt,然后,第二个输入是把压缩文件zip解压缩到一个如yuanwenjian.txt的新文件,然后程序的行为就是把压缩文件zip.txt解压到yuanwenjian.txt
  3. 使用时,特别需要注意文件里面的换行符以及制表符等空白字符,这种字符虽然在文件里面可能不容易发现,但是仍然会被编码,相应的进行压缩以及解压缩操作
  4. 对于中文字符,该程序也是可以进行处理的,不过,由于每个中文字符在windows下占据的是3字节,所以程序计算一个中文字符会按照+3来计算,要想解决这个问题,似乎需要编码相关的知识,由于目前这样计算字节,程序也不算错,且作者还未学习编码相关知识,就先这样放着了

结语

总结:通过这个项目,作者理解了Huffman编码的原理和C++文件操作的基础使用,虽然程序在极端情况下(如超大文件)还有优化空间,但核心功能已经完备。

欢迎交流:如果你在实现过程中遇到任何问题,或者有更好的优化思路,欢迎在评论区留言讨论!

相关推荐
渡我白衣2 小时前
计算机组成原理(5):计算机的性能指标
服务器·网络·c++·人工智能·网络协议·tcp/ip·网络安全
郝学胜-神的一滴2 小时前
设计模式依赖于多态特性
java·开发语言·c++·python·程序人生·设计模式·软件工程
ULTRA??2 小时前
判断水仙花数并输出,c++
c++·算法
_Voosk2 小时前
写了个开头的 C++ Tutorial
开发语言·c++
lingggggaaaa2 小时前
C2远控篇&C&C++&SC转换格式&UUID标识&MAC物理&IPv4地址&减少熵值
c语言·c++·学习·安全·web安全·网络安全·免杀对抗
fengGer的bugs2 小时前
从零到一全栈开发 | 跑腿服务系统:小程序+Vue3+Node.js
小程序·node.js·全栈开发·跑腿服务系统
小小8程序员2 小时前
Apache Doris的部署
apache
2501_916007472 小时前
没有 Mac,如何在 Windows 上架 iOS 应用?一套可落地的工程方案
android·macos·ios·小程序·uni-app·iphone·webview
不想吃菠萝2 小时前
pc端微信小程序post传递data是字符串,自动加了双引号问题修改方案
微信小程序·小程序