压缩原件原理
前言:
- 压缩的原理是,把文件中的字符通过特定的编码方式保存起来,将其写入到文件中。
- 解压原理:根据文件的压缩规则,读取压缩后的文件,将原来的字符还原出来
eg: 原文件内容为 abbccc
- a对应编码11
- b对应编码10
- c对应编码0
(编码的每一个数字代表一个比特位) 这样原文件内容经过压缩后变为 11101000 00000000 不足8比特的补足8比特,也就是只需要2字节就可以保存原文件的内容了
同时可以观察到,出现次数越多的字符,编码格式最好越短。
- 这样的编码方式与哈夫曼树类似,哈夫曼树权值越大的节点越靠近根。
- 哈夫曼树只有叶子节点保存数据,所以我们可以根据根节点到叶子节点的顺序对字符进行编码
eg: abbccc a出现次数1次 b出现次数2次 c出现次数3次 编码过程如下:
所以:我们设计压缩软件流程如下: 1编码表示数据 2 出现次数多编码短,出现次数少编码长
压缩流程
- 分析待压缩文件,获取文件中出现的字符,字符出现次数.将这些数据组织成字典
- 根据字典创建哈夫曼树(核心)
- 根据哈夫曼树获取哈夫曼编码并写入字典中
- 把哈夫曼编码写入文件中
解压过程
- 将文件中的字典读取出来,根据字典生成哈夫曼树
- 按照字节读取文件,按照二进制位分析,将对应的字符写入到解压文件
- 关闭文件
1. 利用哈夫曼树编码
项目需要的头文件及声明common.h
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<vector>
#include<string>
#include<stdio.h>
#include<assert.h>
#include<fstream>
自定义每个字符结构体charactor.h
每个字符结构体应该包括:
- 这个字符是什么
- 字符在要压缩的文件中出现次数
- 这个字符的哈夫曼编码
- 这个字符在字符字典中的位置
同时还需要一个字符字典,用于查阅每个字符对应的哈夫曼编码。同时,通过这个字典还可以找到文件中出现多少个字符。
#pragma once
#include"common.h"
struct Char {
char _character;
unsigned int _times;
std::string _code;
int _index;
Char() = default;
Char(char ch, int times, int index = -1)
:_character(ch), _times(times), _index(index)
{}
};
struct CharDict {
int _cout = 0;
std::vector<Char>_CharArray;
};
创建哈夫曼树hufftree.h
思路:假设字典中又5类字符
- 首先根据找到出现次数最小的字符节点,和出现次数第二小的字符节点,这两个节点构造父节点。
- 新构造的父节点覆盖出现次数最少节点,出现次数第二小的字符节点制空,不参与下次循环。
- 之后循环上面两布,循环4次(因为字典有5类字符)
#pragma once
#include"common.h"
#include"charactor.h"
struct TreeNode {
Char _data;
TreeNode* _left = nullptr;
TreeNode* _right = nullptr;
TreeNode* _parent = nullptr;
TreeNode(const Char&msg):
_data(msg)
{}
bool _isLeaf() {
return (_left == nullptr) && (_right == nullptr);
}
};
class HufTree {
typedef TreeNode Node;
friend class Encode;
friend class UnZip;
private:
Node* _root = nullptr;
public:
void creatHufTree(CharDict& dict) {
std::vector<Node*>NodeArray(dict._cout);
Node* node = nullptr;
for (int i = 0; i < dict._cout; i++) {
NodeArray[i] = new Node(dict._CharArray[i]);
NodeArray[i]->_data._index = i;
dict._CharArray[i]._index = i;
}
if (dict._cout == 1) {
_root = NodeArray[0];
}
else {
for (int time = 0; time < dict._cout - 1; time++) {
int minIndex = 0;
int minSecIndex = 0;
while (NodeArray[minIndex] == nullptr) {
minIndex++;
}
for (int i = 0; i < dict._cout; i++) {
if (NodeArray[i] != nullptr && NodeArray[i]->_data._times < NodeArray[minIndex]->_data._times) {
minIndex = i;
}
}
while (NodeArray[minSecIndex] == nullptr || minIndex == minSecIndex) {
minSecIndex++;
}
for (int i = 0; i < dict._cout; i++) {
if (i != minIndex) {
if (NodeArray[i] != nullptr && NodeArray[i]->_data._times < NodeArray[minSecIndex]->_data._times) {
minSecIndex = i;
}
}
}
_root = new Node(Char(0, NodeArray[minIndex]->_data._times + NodeArray[minSecIndex]->_data._times));
NodeArray[minIndex]->_parent = _root;
NodeArray[minSecIndex]->_parent = _root;
_root->_left = NodeArray[minIndex];
_root->_right = NodeArray[minSecIndex];
NodeArray[minIndex] = _root;
NodeArray[minSecIndex] = nullptr;
}
}
}
};
利用哈夫曼树创建哈夫曼编码encoding.h
思路:
- 打开原文件,按照字节读取文件,统计文件中每个字符出现的次数
- 根据每个字符出现次数调用HufTree类的方法创建哈夫曼树
- 根据字符在树的位置,倒着遍历到根节点,创建哈夫曼编码
#pragma once
#include"common.h"
#include"charactor.h"
#include"hufftree.h"
#include"Zip.h"
class Encode {
friend class GZip;
private:
HufTree _tree;
std::string _srcFile;
CharDict _dict;
public:
Encode(std::string&srcFile)
:_srcFile(srcFile)
{
_anylizeFile();
_tree.creatHufTree(_dict);
_creatHuffCode();
}
private:
void _TestDict() {
printf("共有%d个字符\n", _dict._cout);
for (int i = 0; i < _dict._cout; i++) {
printf("字符%c出现了%d次\n", _dict._CharArray[i]._character, _dict._CharArray[i]._times);
}
}
void _TestHuffCode() {
for (int i = 0; i < _dict._cout; i++) {
printf("字符%c的编码为%s\n", _dict._CharArray[i]._character,_dict._CharArray[i]._code.c_str());
}
}
int _findChar(const char ch) {
for (int i = 0; i < _dict._cout; i++) {
if (_dict._CharArray[i]._character == ch) {
return i;
}
}
return -1;
}
void _anylizeFile() {
FILE* fp = fopen(_srcFile.c_str(), "rb");
if (fp == nullptr) {
printf("%s文件打开失败", _srcFile.c_str());
}
assert(fp != nullptr);
char ch = ' ';
int readNum = 0;
while (true) {
readNum = fread(&ch, 1, 1, fp);
if (readNum != 1) {
break;
}
int pos = _findChar(ch);
if (pos == -1) {
_dict._cout++;
_dict._CharArray.push_back(Char(ch, 1));
}
else {
_dict._CharArray[pos]._times++;
}
}
fclose(fp);
}
void _setHuffCode(TreeNode* root) {
TreeNode* node = root;
std::string& code = root->_data._code;
while (node->_parent != nullptr) {
if (node == node->_parent->_left) {
code.push_back('1');
}
else {
code.push_back('0');
}
node = node->_parent;
}
std::reverse(code.begin(), code.end());
_dict._CharArray[root->_data._index]._code = code;
}
void _creatHuffCode() {
_creatHuffCode(_tree._root);
}
void _creatHuffCode(TreeNode* root) {
if (root != nullptr) {
if (root->_isLeaf()) {
_setHuffCode(root);
}
else {
_creatHuffCode(root->_left);
_creatHuffCode(root->_right);
}
}
}
std::string _getHufCharCode(char ch) {
for (int i = 0; i < _dict._cout; i++) {
Char msg = _dict._CharArray[i];
if (msg._character== ch) {
return msg._code;
}
}
return "-1";
}
};
2. 编码完成后将生成的字典和原文件写入压缩文件中Zip.h
注意:
- 将编码转化为比特位是这一步的重点
- 同时写入压缩文件的字典需要自定义协议,方便解压缩读取
#pragma once
#include"encoding.h"
#include"decoding.h"
class GZip {
private:
Encode _encode;
public:
GZip(std::string& srcFile)
:_encode(srcFile)
{}
void ZipFile(const std::string& destPath) {
std::fstream fpSrc(_encode._srcFile, std::ios::in | std::ios::binary);
if (!fpSrc.is_open()) {
printf("DEAD:待压缩文件丢失");
return;
}
std::ofstream fpDst(destPath, std::ios::out | std::ios::app | std::ios::binary);
if (!fpDst.is_open()) {
printf("DEAD:目标压缩路径丢失");
return;
}
for (int i = 0; i < _encode._dict._cout; i++) {
Char msg = _encode._dict._CharArray[i];
fpDst << msg._character << ";" << msg._code << ";" << msg._index << ";" << msg._times << ";\n";
}
fpDst << "-\n";
char ch = 0;
bool isEnd = false;
size_t indexHufCode = 0;
size_t indexWrite = 0;
std::string HufCode;
while (true) {
if (indexHufCode == HufCode.size()) {
fpSrc.read(&ch, 1);
HufCode = _encode._getHufCharCode(ch);
indexHufCode = 0;
}
indexWrite = 0;
int bite = 0;
char write = 0;
while (bite < 8) {
if (indexHufCode < HufCode.size()) {
if (HufCode[indexHufCode] == '0') {
write &= ~(1 << (7 - indexWrite));
}
else if (HufCode[indexHufCode] == '1') {
write |= (1 << (7 - indexWrite));
}
bite++;
indexWrite++;
indexHufCode++;
}
else {
if (fpSrc.read(&ch, 1)) {
HufCode = _encode._getHufCharCode(ch);
indexHufCode = 0;
}
else {
isEnd = true;
break;
}
}
}
fpDst << write;
char test_write = write;
for (int i = 0; i < 8; i++) {
if ((test_write & 0x80) == 0x80) {
printf("%d", 1);
}
else {
printf("%d", 0);
}
test_write <<= 1;
}
printf("\n");
if (isEnd == true) {
break;
}
}
fpSrc.close();
fpDst.close();
}
};
3. 读取压缩文件中的字段,还原哈夫曼编码decoding.h
按照自定义的协议从文件中读取字典即可
#pragma once
#include"hufftree.h"
#include"charactor.h"
class Decode {
friend class UnZip;
public:
Decode(std::string& srcFile) {
_anylizeFile(srcFile);
_tree.creatHufTree(_dict);
}
char findchar(std::string& key) {
for (int i = 0; i < _dict._cout; i++) {
if (_dict._CharArray[i]._code == key) {
return _dict._CharArray[i]._character;
}
}
return -1;
}
int allAlph() {
int ret = 0;
for (int i = 0; i < _dict._cout; i++) {
ret += _dict._CharArray[i]._times;
}
return ret;
}
private:
CharDict _dict;
std::string _srcFile;
HufTree _tree;
void _redictDict(CharDict& dict, std::string& readLine) {
assert(readLine.size() > 0);
Char msg;
int begin = 0;
int flag = 0;
for (int i = 0; i < readLine.size(); i++) {
if (readLine[i] != ';') {
continue;
}
else {
if (flag == 0) {
msg._character = readLine[0];
}
else if (flag == 1) {
msg._code = readLine.substr(begin, i - begin);
}
else if (flag == 2) {
msg._index = atoi(readLine.substr(begin, i - begin).c_str());
}
else {
msg._times = atoi(readLine.substr(begin, i - begin).c_str());
}
begin = i + 1;
flag++;
}
}
_dict._CharArray.push_back(msg);
_dict._cout++;
}
void _anylizeFile(std::string& srcFile) {
_srcFile = srcFile;
std::fstream fpSrc(_srcFile, std::ios::in | std::ios::binary);
if (!fpSrc.is_open()) {
printf("DEAD:压缩文件丢失");
return;
}
std::string readLine;
while (true) {
std::getline(fpSrc, readLine);
if (readLine=="-") {
break;
}
_redictDict(_dict,readLine);
}
fpSrc.close();
_TestDict();
_TestHuffCode();
}
public:
void _TestDict() {
printf("共有%d个字符\n", _dict._cout);
for (int i = 0; i < _dict._cout; i++) {
printf("字符%c出现了%d次\n", _dict._CharArray[i]._character, _dict._CharArray[i]._times);
}
}
void _TestHuffCode() {
for (int i = 0; i < _dict._cout; i++) {
printf("字符%c的编码为%s\n", _dict._CharArray[i]._character, _dict._CharArray[i]._code.c_str());
}
}
};
4. 利用还原的哈夫曼树和哈夫曼编码解压缩Zip.h
思路:
- 先统计原文件应该有多少个字符
- 定位压缩文件的内容部分,按照字节分析,如果能够走到哈夫曼树的叶子节点就可以还原这个字符。
class UnZip {
private:
Decode _decode;
public:
UnZip(std::string& destPath)
:_decode(destPath)
{}
void UnzipFile(const std::string& destPath) {
FILE* fp = fopen(destPath.c_str(), "rb");
char ch = 0;
int readNum = 0;
while (true) {
readNum = fread(&ch, 1, 1, fp);
assert(readNum != 0);
if (ch == '-') {
break;
}
}
readNum = fread(&ch, 1, 1, fp);
assert(readNum == 1);
HufTree tree = _decode._tree;
TreeNode* node = tree._root;
FILE* redict_fp = fopen("还原文件.txt", "w");
std::string key;
bool isFinish = false;
int allAlph = _decode.allAlph();
while (true) {
readNum = fread(&ch, 1, 1, fp);
if (readNum == 0) {
break;
}
int num = ch;
for (int i = 0; i < 8; i++) {
if ((ch & 0x80) == 0x80) {
node = node->_left;
key.push_back('1');
}
else if ((ch & 0x80) == 0) {
node = node->_right;
key.push_back('0');
}
ch <<= 1;
if (node->_isLeaf()) {
char write = _decode.findchar(key);
if (write == -1) {
break;
}
fwrite(&write, 1, 1, redict_fp);
node = tree._root;
key.clear();
allAlph -= 1;
if (allAlph == 0) {
isFinish = true;
break;
}
}
}
if (isFinish == true) {
break;
}
}
fclose(redict_fp);
}
};
测试结果
代码位置
Github
|