哈夫曼压缩
哈夫曼树的构造
总体思路
将输入的字符串中出现的不同字符连同其频数作为一个整体,该整体即哈夫曼树的节点(HuffNode),其中“频数”即节点的权重(weight)。随后使用优先队列(priority_queue)存储这些节点,实现按节点权重的大小将各节点排序。
构造哈夫曼树时,先新建一个空节点作为新树的根节点,随后弹出该队列的前两个元素,分别作为该根节点的左子树和右子树,这样就新建了一棵树;随后把新建的树并入优先队列。重复该过程,当队列中仅含一个元素时,该剩余元素即最终构造好的哈夫曼树。
数据类型定义
出于学习和代码完备性的考虑,以下几种数据类型的定义中都包含有较多并不会在该应用实现过程中用到的函数。
-
哈夫曼树节点(
HuffNode):template <typename T> class HuffNode { private: HuffNode<T>* left; HuffNode<T>* right; int weight; T elem; void DeleteHuffNode(HuffNode<T>* curr) // 递归地删除该节点所连接的所有子节点。 { if (curr->IsLeaf()) { delete curr; return; } DeleteHuffNode(curr->Left()); curr->SetLeft(nullptr); DeleteHuffNode(curr->Right()); curr->SetRight(nullptr); delete curr; return; } public: HuffNode() { left = right = nullptr; weight = 0; } HuffNode(const T& e, int w, HuffNode<T>* l = nullptr, HuffNode<T>* r = nullptr) : elem(e), weight(w), left(l), right(r) { } int Weight() { return weight; } T& Element() { return elem; } HuffNode<T>* Left() { return left; } HuffNode<T>* Right() { return right; } bool IsLeaf() { return left == nullptr && right == nullptr; } void SetLeft(HuffNode<T>* v) { left = v; } void SetRight(HuffNode<T>* v) { right = v; } void DeleteThisNode() { DeleteHuffNode(this); } }; -
哈夫曼树(
HuffTree)template <typename T> class HuffTree { private: HuffNode<T>* root; public: HuffTree(HuffNode<T>* r = nullptr) : root(r) { } HuffTree(int weight, HuffTree<T>* left, HuffTree<T>* right) { root = new HuffNode<T>(false, weight, left->Root(), right->Root()); } HuffTree(HuffItem<T>& item) { root = new HuffNode<T>(item.elem, item.weight); } ~HuffTree() { DeleteTree(); } void DeleteTree() { if (root == nullptr) return; root->DeleteThisNode(); root = nullptr; } HuffNode<T>* Root() { return root; } int Weight() { return root->Weight(); } }; -
哈夫曼树节点的权值比较类(
HuffCmp,用于构造priority_queue):template <typename T> class HuffCmp { public: bool operator()(HuffTree<T>* h1, HuffTree<T>* h2) { return h1->Weight() > h2->Weight(); } }; -
为了更方便地构造哈夫曼树节点,这里使用如下结构体(
HuffItem):template <typename T> struct HuffItem { T elem; int weight; };
重要功能实现
-
根据所给的元素及其权重列表,构建哈夫曼树:
“元素及其权重”使用定义的结构体
HuffItem存储;“列表”使用vector<HuffItem>。template <typename T> HuffTree<T>* BuildHuffTree(vector<HuffItem<T>>& items) { priority_queue<HuffTree<T>*, vector<HuffTree<T>* >, HuffCmp<T>> ascendingTrees; for (ULL i = 0; i < items.size(); i++) ascendingTrees.push(new HuffTree<T>(items[i])); while (ascendingTrees.size() > 1) { HuffTree<T>* huffTree_newLeft = ascendingTrees.top(); ascendingTrees.pop(); HuffTree<T>* huffTree_newRight = ascendingTrees.top(); ascendingTrees.pop(); HuffTree<T>* huffTree_newTop = new HuffTree<T>(huffTree_newLeft->Weight() + huffTree_newRight->Weight(), huffTree_newLeft, huffTree_newRight); ascendingTrees.push(huffTree_newTop); } return ascendingTrees.top(); }其中,
ascendingTrees即存储各哈夫曼树/节点的优先队列。函数结束时(ascendingTrees仅剩余一个元素),返回优先队列的队首元素,即构造好的哈夫曼树。
哈夫曼树编码
总体思路
使用递归的先序遍历,可以获得每一个待编码元素(叶节点)的二进制码。此处二进制码使用 vector<bool> 存储(也可以使用足够长的 char*,或者使用 string),编码字典使用哈希表 map<T, vector<bool>> 存储。
递归地考虑,将哈夫曼树的左右子树分别看成一个整体(看成一个哈弗曼树节点),那么现在编码的流程即:
-
先判断当前节点是不是叶节点。如果是叶节点,则将已获得的二进制码(
vector<bool>)和该叶节点代表的数据值一并存到字典中。如果不是叶节点,那么: -
先前往左子树,顺便向二进制码表中推入一个“0”,即
false;随后将该节点作为新的根节点,从步骤 1. 递归进行下一步。
-
再前往右子树,顺便向二进制码表中推入一个“1”,即
true;随后将该节点作为新的根节点,从步骤 1. 递归进行下一步。
在“向二进制码表中推入”的过程中,注意每次递归的时候传递的二进制表参数不可以是指针或引用;因为在哈夫曼树中,其实每个节点都相当于有自己的编码;所以在递归进行下一层遍历时,应将当前节点的编码“复制”到下一层继续使用。
重要功能实现
-
根据哈夫曼树获取编码字典:
typedef vector<bool> HuffCode; template <typename T> bool GetHuffCode(HuffNode<T>* node, HuffCode code, map<T, HuffCode>& huffMap) { if (node == nullptr) return false; if (node->IsLeaf()) { huffMap[node->Element()] = code; return true; } if (node->Left() != nullptr) { HuffCode left_code = code; left_code.push_back(false); GetHuffCode(node->Left(), left_code, huffMap); } if (node->Right() != nullptr) { HuffCode right_code = code; right_code.push_back(true); GetHuffCode(node->Right(), right_code, huffMap); } return true; }此处使用
typedef vector<bool> HuffCode,方便使用。另外,此处还可以添加如下函数:
template <typename T> map<T, HuffCode> HuffTree2Code(HuffTree<T>* tree) { HuffCode huff_code; map<T, HuffCode> huff_map; if (!GetHuffCode(tree->Root(), huff_code, huff_map)) return huff_map; return huff_map; }这样哈夫曼树的编码在日后使用起来将更加方便。
文件的读写
总体思路
-
读(读待压缩的原文件):
使用二进制方式(
ios::binary)按字节读入目标文件,并将读入的字节当做字符串,根据其中“字符”出现的种类和频数新建vector<HuffItem>表,再使用定义过的BuildHuffTree(vector<HuffItem<T>>& )构造哈夫曼树。 -
写(写压缩后的文件):
将文件分为“文件头”和“内容”两部分。其中,文件头结构如下(括号中的数字代表占用的字节数):
[ 字符种类数(4) | 最后一字节有效位数(1) | 字符(1)&权重(4)交错串 | 压缩后的“字符”串 ]-
其中,“字符种类数”和“权重”所占的字节长度可以根据文件大小适当调整,避免文件“越压越大”的情况出现。
-
规定好文件头中每个元素的长度,在后续的文件读取工作中就会变得更简单。
-
存储压缩后的二进制表,可将每 8 位当做一个字符输出。
说明:
-
“字符&权重交错串”存储的结果示例如下:
A 4 B 1 C 25 D 3显然,上述串需要 1+4+1+4+1+4+1+4 = 20 个字节来存储。故可通过调整权重的字节长度来优化压缩后的文件大小(视被压缩文件的大小决定)。
-
“最后一字节有效位数”可作如下解释:
因为压缩后原文件中每个“字符”的编码长度是不同的,因此最后只把这些编码堆起来,很有可能凑不成 8 的整倍数,也就是凑不成完整的字节。因此,可以将最后不足 8 位的部分用“0”(
false)补全,并记录最后这一部分的原长度,也就是“最后一字节有效位数”。示例如下:
假定文件内容为
hello其对应编码如下(不唯一):
l = 0 o = 10 h = 110 e = 111那么原文件便由
0110 1000 0110 0101 0110 1100 0110 1100 0110 1111压缩为:
1101 1100 10明显这凑不够整字节。因此将上述编码补全为:
1101 1100 1000 0000同时要记录原编码中最后一部分的有效长度,为 2。
-
完整的一个压缩后的文件编码结构示例:
仍以上述“hello”文件作为例子,结合前两点说明,可得出最后压缩好的文件应该是如下结构:
[ 4 | 2 | e 1 h 1 l 2 o 1 | 1101 1100 1000 0000 ]可作如下解释:
[ 文件有4种字符 | 读文件时最后一个字节只读前2位 | 4种字符及其权重是什么 | 压缩后的文件编码 ]这个文件便可将“字符种类数”和“权重”所占的字节长度设为 1,压缩率更大。
-
-
读 / 写(读 / 写压缩后 / 解压后的文件):
仍是结合“hello”的例子,根据写压缩文件的规则,可以按如下规律读:
读四个字节 → 读一个字节 → (读一个字节 → 读四个字节) × 4 遍 → (读一个字节) 重复直到文件最后一个字节由此便分别得到了存储的不同信息。
说明:
-
在读“字符&权重交错串”的时候,每读到一个字符和一个整数,就构造一个新的
HuffItem,并将其存入vector<HuffItem<T>>中。读取结束后,便可使用BuildHuffTree(vector<HuffItem<T>>& )构造用于解码的哈夫曼树。 -
在读“压缩后的‘字符’串”区域时,上述例子获取的是一个长度为 2 的 char 数组:
{ -36, -128 }现将其转为
unsigned类型,得到{ 220, 128 },随后将其转为二进制类型,同时将两个结果连续地按位存入vector<bool>:{ 220, 128 } → { 1101 1100, 1000 0000 } → { T, T, F, T, T, T, F, F, T, F, F, F, F, F, F, F }这样以后,再读取
vector<bool>的前size() - (8 - 2)位即可。在读取该vector<bool>的时候,遇到true则走向右子树,遇到false则走向左子树。当遇到叶节点时,输出叶节点存储的数据值(字节)。最终则得到了原来完整的文件。
-
重要功能实现
-
读原文件:
bool ReadInFile(vector<char>& container, const char* path) { ifstream file(path, ios::in | ios::binary); if (!file.is_open()) { cout << " * 文件打开失败!\n"; return false; } ULL file_size = 0; file_size = GetFileSize(file); cout << " * 读取文件中...\n"; for (ULL i = 0; i < file_size; i++) { char value; file.seekg(i); file.read(&value, 1); container.push_back(value); } file.close(); return true; } -
根据读入的原文件的字节构建哈夫曼节点表(将字符及其频数存储进
vector<HuffItem<T>>):template <typename T> void InsertHuffItem(vector<HuffItem<T>>& items, const T& value) { bool have_item = false; for (ULL i = 0; i < items.size(); i++) if (items[i].elem == value) { have_item = true; items[i].weight++; break; } if (!have_item) { HuffItem<char> item_new; item_new.elem = value; item_new.weight = 1; items.push_back(item_new); } } template <typename T> void LoadHuffItem(vector<HuffItem<T>>& items, vector<T>& values) { for (ULL i = 0; i < values.size(); i++) InsertHuffItem(items, values[i]); }使用时只需调用
LoadHuffItem(vector<HuffItem<T>>& , vector<T>& )即可。随后便可使用构造好的节点表构建哈夫曼树。
-
每 8 位凑成一个字符:
vector<char> BoundBits2Char(vector<bool>& bits) { ULL pows[8] = { 1, 2, 4, 8, 16, 32, 64, 128 }; vector<char> chars; vector<bool> bits_full = bits; while (bits_full.size() % 8 != 0) bits_full.push_back(false); for (ULL i = 0; i < bits_full.size(); i += 8) { char ch = '\0'; for (int k = 0; k < 8; k++) { ch += pows[k] * bits_full[i + 7 - k]; } chars.push_back(ch); } return chars; }该函数中,现将原二进制表补足为 8 的整数长度,随后开始将二进制转化为十进制,再转为字符存放到
vector<char>中。 -
将一个字符拆成 8 位:
vector<bool> UnboundChar2BitInversed(char value) { int ivalue = (int)value; if (ivalue < 0) ivalue += 256; vector<bool> result; while (ivalue != 0) { result.push_back(ivalue % 2 == 1); ivalue /= 2; } if (result.size() <= 0) for (int i = 0; i < 8; i++) result.push_back(false); else while (result.size() % 8 != 0) result.push_back(false); return result; }该函数中,先将
value转为int型,之后等效地去掉符号,随后便将十进制转为二进制,按位存入一个vector<bool>中。*注意:*十进制转二进制的时候,一方面结果是按从低位到高位存入
vector<bool>中的,也就是说如果从第一个元素看向最后一个元素,实际上得到的是ivalue对应二进制的逆序;另一方面,结果并不是总有 8 位,比如 4 经上述转换获得的是一个长度为 3 的vector<bool>,即{ F, F, T }(逆序)。因此,有必要补全八位。此处一定要注意0 % 8 == 0这个事实,起初因为没有考虑到这一点,输出的文件常常会丢失几个字节(因为没有把'\0'换成 8 位的0000 0000而换成了 1 位的0)。当然,一种更好的方案便是直接使用bool[8]存储(注意初始化为八个false)。*剧透:*这里并没有将所有字符对应的二进制“连续地”存入同一个
vector<bool>,而是出于“逆序二进制”以及vector不支持“push_forward()”的原因,采用的一个字符返回一个vector<bool>的方法;同时在后面解压文件的实现中,单独有一部分代码会将这些分立的逆序表合成一个正序的大表。
一些小细节
-
因为 ifstream 和 ofstream 库中的
write()和read()函数只支持将字符串(char*)写到文件里,因此处理文件头里面的整数(int或其他类型)便成为需要特殊对待的事情。一种简单的解决办法就是使用
memcpy()函数。用例如下:-
四字节
int存入四个元素的char数组:int a = 123; char* a_char = new char[sizeof(int)]; // 也就是 new char[4] memcpy(a_char, a, sizeof(int)); // 最后得到的 a_char 如下: // { 11, 7, 0, 0 } // 注意:转换之后的 a_char 也是按 a 四个字节的逆序存储的。 -
四个元素的
char数组转为四字节的int:char b_char[4] = {'\11', '\7', '\0', '\0'} int b; memcpy(&b, b_char, sizeof(int)); // 最后得到 b = 123 // 由此可见,不需要对逆序存储的 char* 做任何操作,原样交给 memcpy() 即可。
-
最终的压缩和解压函数
压缩(ZipFile(const char* src, const char* dst))
template <typename A, typename B>
void ZipFile(const char* in_path, const char* out_path)
{
// 文件存储格式:字符种类数(A) | 尾字节读取位数(char) | 字符及权重/频数(char,B) | 字符串(char*)
vector<char> file_cont; // 按字节存储读入的文件内容
if (!ReadInFile(file_cont, in_path)) return; // 如果文件未找到,则跳出函数
vector<HuffItem<char>> huff_items;
LoadHuffItem(huff_items, file_cont); // 根据文件内容 file_cont,构造哈夫曼树节点表(字符种类&频数)
cout << " * 构建哈夫曼树中...\n";
HuffTree<char>* huff_tree = BuildHuffTree(huff_items); // 构造哈夫曼树
cout << " * 编码中...\n";
map<char, HuffCode> huff_code = HuffTree2Code(huff_tree); // 生成编码表
cout << " * 转比特中...\n";
int bit_num = 0;
vector<bool> bits;
for (ULL i = 0; i < file_cont.size(); i++) // 根据编码表,将文件内容的每一个字节重新编码,记录到 bits 内
{
HuffCode cur_code = huff_code[file_cont[i]];
for (ULL j = 0; j < cur_code.size(); j++)
{
bits.push_back(cur_code[j]);
bit_num++;
}
}
cout << " * 转字符中...\n";
vector<char> chars;
chars = BoundBits2Char(bits); // 每 8 个比特合并为一个字节
ofstream file_out(out_path, ios::out | ios::binary); // 开始写文件(将合并好的 chars 内元素逐一写出)
cout << " * 写文件中...\n";
A char_types = huff_code.size(); // 写文件头
char* char_types_c = new char[sizeof(A)];
char lastByte_validBits = bits.size() % 8;
memcpy(char_types_c, &char_types, sizeof(A));
file_out.write(char_types_c, sizeof(A));
file_out.write(&lastByte_validBits, sizeof(char));
for (ULL i = 0; i < huff_items.size(); i++)
{
char* char_weight = new char[sieof(B)];
// 这里的int类型可以根据原文件大小更改为short甚至char类型,
// 这取决于原文件中每个字符出现的最大频数。
memcpy(char_weight, &huff_items[i].weight, sizeof(B));
file_out.write(&huff_items[i].elem, sizeof(char));
file_out.write(char_weight, sizeof(B));
}
for (ULL i = 0; i < chars.size(); i++) // 写文件内容
{
file_out.write(&chars[i], sizeof(char));
}
file_out.close();
cout << " # 压缩完成!\n";
}
解压(UnzipFile(const char* src, const char* dst))
template <typename A, typename B>
void UnzipFile(const char* in_path, const char* out_path)
{
// 文件存储格式:字符种类数(A) | 尾字节读取位数(char) | 字符及权重/频数(char,B) | 字符串(char*)
ifstream file(in_path, ios::in | ios::binary);
if (!file.is_open())
{
cout << " * 文件打开失败!\n";
return;
}
ULL file_size = 0;
file_size = GetFileSize(file);
char* char_types = new char[sizeof(A)];
char lastByte_validBits;
vector<HuffItem<char>> huff_items; // 声明几个用于记录文件头所存储信息的变量
cout << " * 读文件中...\n";
int loc = 0; // 读文件头
file.seekg(loc);
file.read(char_types, sizeof(A));
loc += sizeof(A);
file.seekg(loc);
file.read(&lastByte_validBits, sizeof(char));
loc++;
A ichar_types;
int ilastByte_validBits = (lastByte_validBits < 0 ? (lastByte_validBits + 256) : lastByte_validBits);
memcpy(&ichar_types, char_types, sizeof(A));
for (int t = 0; t < ichar_types; t++)
{
char target_char;
B target_weight;
char* target_weight_c = new char[sizeof(B)];
file.seekg(loc);
file.read(&target_char, sizeof(char));
file.seekg(++loc);
file.read(target_weight_c, sizeof(B));
memcpy(&target_weight, target_weight_c, sizeof(B));
HuffItem<char> new_item;
new_item.elem = target_char;
new_item.weight = target_weight;
huff_items.push_back(new_item);
loc += sizeof(B);
}
cout << " * 构建哈夫曼树中...\n";
HuffTree<char>* huff_tree = BuildHuffTree(huff_items); // 文件头读取完毕,开始构造用于解码的哈夫曼树
cout << " * 解码中...\n";
vector<bool> codes;
for (; loc < file_size; loc++) // 将压缩文件第四个区域的内容按字节读入
{
char value;
file.seekg(loc);
file.read(&value, sizeof(char));
vector<bool> bit_inversed = UnboundChar2BitInversed(value); // 每个字节转 8 位二进制
for (ULL i = bit_inversed.size(); i >= 1; i--) // 防止死循环。
codes.push_back(bit_inversed[i - 1]); // 将每个字节对应的的二进制表合并入一个大表
}
cout << " * 写文件中...\n";
ofstream file_out(out_path, ios::out | ios::binary); // 开始写出解压的文件
HuffNode<char>* huff_ptr = huff_tree->Root();
for (ULL i = 0; i < (ULL)codes.size() - ((ULL)8 - (ULL)ilastByte_validBits); i++) // 避免算数溢出。
{
if (codes[i]) huff_ptr = huff_ptr->Right();
else huff_ptr = huff_ptr->Left();
if (huff_ptr->IsLeaf()) // **注意输出字符与移动指针的顺序!**
{
file_out.write(&huff_ptr->Element(), sizeof(char));
huff_ptr = huff_tree->Root();
continue;
}
}
file_out.close();
cout << " # 解压完成!\n";
}
源代码
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <queue>
#include <map>
#define ULL unsigned long long
using namespace std;
#pragma region Huffmann Tree
template <typename T>
struct HuffItem
{
T elem;
int weight;
};
template <typename T>
class HuffNode
{
private:
HuffNode<T>* left;
HuffNode<T>* right;
int weight;
T elem;
void DeleteHuffNode(HuffNode<T>* curr) // 递归地删除该节点所连接的所有子节点。
{
if (curr->IsLeaf())
{
delete curr;
return;
}
DeleteHuffNode(curr->Left());
curr->SetLeft(nullptr);
DeleteHuffNode(curr->Right());
curr->SetRight(nullptr);
delete curr;
return;
}
public:
HuffNode() { left = right = nullptr; weight = 0; }
HuffNode(const T& e, int w, HuffNode<T>* l = nullptr, HuffNode<T>* r = nullptr) : elem(e), weight(w), left(l), right(r) { }
// ~HuffNode() { delete this; }
int Weight() { return weight; }
T& Element() { return elem; }
HuffNode<T>* Left() { return left; }
HuffNode<T>* Right() { return right; }
bool IsLeaf() { return left == nullptr && right == nullptr; }
void SetLeft(HuffNode<T>* v) { left = v; }
void SetRight(HuffNode<T>* v) { right = v; }
void DeleteThisNode()
{
DeleteHuffNode(this);
}
};
template <typename T>
class HuffTree
{
private:
HuffNode<T>* root;
public:
HuffTree(HuffNode<T>* r = nullptr) : root(r) { }
HuffTree(int weight, HuffTree<T>* left, HuffTree<T>* right) { root = new HuffNode<T>(false, weight, left->Root(), right->Root()); }
HuffTree(HuffItem<T>& item) { root = new HuffNode<T>(item.elem, item.weight); }
~HuffTree() { DeleteTree(); }
void DeleteTree()
{
if (root == nullptr) return;
root->DeleteThisNode();
root = nullptr;
}
HuffNode<T>* Root() { return root; }
int Weight() { return root->Weight(); }
};
template <typename T>
class HuffCmp
{
public:
bool operator()(HuffTree<T>* h1, HuffTree<T>* h2)
{
return h1->Weight() > h2->Weight();
}
};
template <typename T>
HuffTree<T>* BuildHuffTree(vector<HuffItem<T>>& items)
{
priority_queue<HuffTree<T>*, vector<HuffTree<T>* >, HuffCmp<T>> ascendingTrees;
for (ULL i = 0; i < items.size(); i++)
ascendingTrees.push(new HuffTree<T>(items[i]));
while (ascendingTrees.size() > 1)
{
HuffTree<T>* huffTree_newLeft = ascendingTrees.top();
ascendingTrees.pop();
HuffTree<T>* huffTree_newRight = ascendingTrees.top();
ascendingTrees.pop();
HuffTree<T>* huffTree_newTop = new HuffTree<T>(huffTree_newLeft->Weight() + huffTree_newRight->Weight(), huffTree_newLeft, huffTree_newRight);
ascendingTrees.push(huffTree_newTop);
}
return ascendingTrees.top();
}
enum PrintOrder
{
pre_order, mid_order, post_order
};
template <typename T>
void PrintHuffNode(HuffNode<T>* curr, PrintOrder order)
{
if (curr == nullptr)
{
cout << "[Empty Tree]\n";
return;
}
if (curr->Left() == nullptr && curr->Right() == nullptr)
{
cout << curr->Element();
return;
}
if (order == PrintOrder::pre_order) cout << curr->Element();
PrintHuffNode(curr->Left(), order);
if (order == PrintOrder::mid_order) cout << curr->Element();
PrintHuffNode(curr->Right(), order);
if (order == PrintOrder::post_order) cout << curr->Element();
return;
}
template <typename T>
void PrintHuffTree_PreOrder(HuffTree<T>* tree)
{
PrintHuffNode(tree->Root(), PrintOrder::pre_order);
}
#pragma endregion
#pragma region Huffmann Coding
typedef vector<bool> HuffCode;
template <typename T>
bool GetHuffCode(HuffNode<T>* node, HuffCode code, map<T, HuffCode>& huffMap)
{
if (node == nullptr) return false;
if (node->IsLeaf())
{
huffMap[node->Element()] = code;
return true;
}
if (node->Left() != nullptr)
{
HuffCode left_code = code;
left_code.push_back(false);
GetHuffCode(node->Left(), left_code, huffMap);
}
if (node->Right() != nullptr)
{
HuffCode right_code = code;
right_code.push_back(true);
GetHuffCode(node->Right(), right_code, huffMap);
}
return true;
}
template <typename T>
map<T, HuffCode> HuffTree2Code(HuffTree<T>* tree)
{
HuffCode huff_code;
map<T, HuffCode> huff_map;
if (!GetHuffCode(tree->Root(), huff_code, huff_map)) return huff_map;
return huff_map;
}
#pragma endregion
#pragma region Filestream Operations
template <typename T>
void InsertHuffItem(vector<HuffItem<T>>& items, const T& value)
{
bool have_item = false;
for (ULL i = 0; i < items.size(); i++)
if (items[i].elem == value)
{
have_item = true;
items[i].weight++;
break;
}
if (!have_item)
{
HuffItem<char> item_new;
item_new.elem = value;
item_new.weight = 1;
items.push_back(item_new);
}
}
template <typename T>
void LoadHuffItem(vector<HuffItem<T>>& items, vector<T>& values)
{
for (ULL i = 0; i < values.size(); i++)
InsertHuffItem(items, values[i]);
}
ULL GetFileSize(ifstream& file)
{
ULL res = 0;
file.seekg(0, ios::end);
res = file.tellg();
return res;
}
bool ReadInFile(vector<char>& container, const char* path)
{
ifstream file(path, ios::in | ios::binary);
if (!file.is_open())
{
cout << " * 文件打开失败!\n";
return false;
}
ULL file_size = 0;
file_size = GetFileSize(file);
cout << " * 读取文件中...\n";
for (ULL i = 0; i < file_size; i++)
{
char value;
file.seekg(i);
file.read(&value, 1);
container.push_back(value);
}
file.close();
return true;
}
vector<char> BoundBits2Char(vector<bool>& bits)
{
ULL pows[8] = { 1, 2, 4, 8, 16, 32, 64, 128 };
vector<char> chars;
vector<bool> bits_full = bits;
while (bits_full.size() % 8 != 0)
bits_full.push_back(false);
for (ULL i = 0; i < bits_full.size(); i += 8)
{
char ch = '\0';
for (int k = 0; k < 8; k++)
{
ch += pows[k] * bits_full[i + 7 - k];
}
chars.push_back(ch);
}
return chars;
}
vector<bool> UnboundChar2BitInversed(char value)
{
int ivalue = (int)value;
if (ivalue < 0) ivalue += 256;
vector<bool> result;
while (ivalue != 0)
{
result.push_back(ivalue % 2 == 1);
ivalue /= 2;
}
if (result.size() <= 0)
for (int i = 0; i < 8; i++)
result.push_back(false);
else
while (result.size() % 8 != 0)
result.push_back(false);
return result;
}
template <typename A, typename B>
void ZipFile(const char* in_path, const char* out_path)
{
// 文件存储格式:字符种类数(A) | 尾字节读取位数(char) | 字符及权重/频数(char,B) | 字符串(char*)
vector<char> file_cont;
if (!ReadInFile(file_cont, in_path)) return;
vector<HuffItem<char>> huff_items;
LoadHuffItem(huff_items, file_cont);
cout << " * 构建哈夫曼树中...\n";
HuffTree<char>* huff_tree = BuildHuffTree(huff_items);
cout << " * 编码中...\n";
map<char, HuffCode> huff_code = HuffTree2Code(huff_tree);
cout << " * 转比特中...\n";
int bit_num = 0;
vector<bool> bits;
for (ULL i = 0; i < file_cont.size(); i++)
{
HuffCode cur_code = huff_code[file_cont[i]];
for (ULL j = 0; j < cur_code.size(); j++)
{
bits.push_back(cur_code[j]);
bit_num++;
}
}
cout << " * 转字符中...\n";
vector<char> chars;
chars = BoundBits2Char(bits);
ofstream file_out(out_path, ios::out | ios::binary);
cout << " * 写文件中...\n";
A char_types = huff_code.size();
char* char_types_c = new char[sizeof(A)];
char lastByte_validBits = bits.size() % 8;
memcpy(char_types_c, &char_types, sizeof(A));
file_out.write(char_types_c, sizeof(A));
file_out.write(&lastByte_validBits, sizeof(char));
for (ULL i = 0; i < huff_items.size(); i++)
{
char* char_weight = new char[sizeof(B)];
// 这里的int类型可以根据原文件大小更改为short甚至char类型,
// 这取决于原文件中每个字符出现的最大频数。
memcpy(char_weight, &huff_items[i].weight, sizeof(B));
file_out.write(&huff_items[i].elem, sizeof(char));
file_out.write(char_weight, sizeof(B));
}
for (ULL i = 0; i < chars.size(); i++)
{
file_out.write(&chars[i], sizeof(char));
}
file_out.close();
cout << " # 压缩完成!\n";
}
template <typename A, typename B>
void UnzipFile(const char* in_path, const char* out_path)
{
// 文件存储格式:字符种类数(A) | 尾字节读取位数(char) | 字符及权重/频数(char,B) | 字符串(char*)
ifstream file(in_path, ios::in | ios::binary);
if (!file.is_open())
{
cout << " * 文件打开失败!\n";
return;
}
ULL file_size = 0;
file_size = GetFileSize(file);
char* char_types = new char[sizeof(A)];
char lastByte_validBits;
vector<HuffItem<char>> huff_items;
cout << " * 读文件中...\n";
int loc = 0;
file.seekg(loc);
file.read(char_types, sizeof(A));
loc += sizeof(A);
file.seekg(loc);
file.read(&lastByte_validBits, sizeof(char));
loc++;
A ichar_types;
int ilastByte_validBits = (lastByte_validBits < 0 ? (lastByte_validBits + 256) : lastByte_validBits);
memcpy(&ichar_types, char_types, sizeof(A));
for (int t = 0; t < (int)ichar_types; t++)
{
char target_char;
B target_weight;
char* target_weight_c = new char[sizeof(B)];
file.seekg(loc);
file.read(&target_char, sizeof(char));
file.seekg(++loc);
file.read(target_weight_c, sizeof(B));
memcpy(&target_weight, target_weight_c, sizeof(B));
HuffItem<char> new_item;
new_item.elem = target_char;
new_item.weight = (int)target_weight;
huff_items.push_back(new_item);
loc += sizeof(B);
}
cout << " * 构建哈夫曼树中...\n";
HuffTree<char>* huff_tree = BuildHuffTree(huff_items);
cout << " * 解码中...\n";
vector<bool> codes;
for (; loc < file_size; loc++)
{
char value;
file.seekg(loc);
file.read(&value, sizeof(char));
vector<bool> bit_inversed = UnboundChar2BitInversed(value);
for (ULL i = bit_inversed.size(); i >= 1; i--) // 防止死循环。
codes.push_back(bit_inversed[i - 1]);
}
cout << " * 写文件中...\n";
ofstream file_out(out_path, ios::out | ios::binary);
HuffNode<char>* huff_ptr = huff_tree->Root();
for (ULL i = 0; i < (ULL)codes.size() - ((ULL)8 - (ULL)ilastByte_validBits); i++) // 避免算数溢出。
{
if (codes[i]) huff_ptr = huff_ptr->Right();
else huff_ptr = huff_ptr->Left();
if (huff_ptr->IsLeaf()) // **注意输出字符与移动指针的顺序!**
{
file_out.write(&huff_ptr->Element(), sizeof(char));
huff_ptr = huff_tree->Root();
continue;
}
}
file_out.close();
cout << " # 解压完成!\n";
}
#pragma endregion
int main()
{
string in_path;
string out_path;
char cmd, flag = 1;
while (flag)
{
char tmp;
cout << "1. 压缩文件\n2. 解压文件\n0. 退出程序\n请输入操作序号:";
cin >> cmd;
switch (cmd)
{
case '1':
cout << "请输入文件目录:"; cin.get();
getline(cin, in_path);
out_path = in_path + ".dat";
ZipFile<char, char>(in_path.c_str(), out_path.c_str());
break;
case '2':
cout << "请输入文件目录:"; cin.get();
getline(cin, in_path);
out_path = in_path + ".out";
UnzipFile<char, char>(in_path.c_str(), out_path.c_str());
break;
case '0':
flag = 0;
break;
default: break;
}
}
}
本文详细介绍了哈夫曼(霍夫曼)压缩的实现过程,包括哈夫曼树的构造、编码、文件的读写及解压。通过构建哈夫曼树,使用先序遍历来生成编码字典,对文件进行压缩。在读写文件时,通过特定格式存储压缩信息,以实现高效压缩和解压。
1885





