整体代码
#include <iostream>
#include <fstream>
#include <queue>
#include <vector>
#include <unordered_map>
#include <bitset>
#include <string>
using namespace std;
// Huffman树节点结构
struct HuffmanNode {
char data; // 数据
unsigned freq; // 频率
HuffmanNode *left, *right; // 左右子节点
HuffmanNode(char data, unsigned freq) {
left = right = nullptr;
this->data = data;
this->freq = freq;
}
};
// 比较函数,用于优先队列
struct compare {
bool operator()(HuffmanNode* l, HuffmanNode* r) {
return (l->freq > r->freq);
}
};
// 生成Huffman编码
void printCodes(struct HuffmanNode* root, string str, unordered_map<char, string> &huffmanCode) {
if (!root) return;
if (root->data != '$')
huffmanCode[root->data] = str;
printCodes(root->left, str + "0", huffmanCode);
printCodes(root->right, str + "1", huffmanCode);
}
// 编码函数
void encode(HuffmanNode* root, const string &data, unordered_map<char, string> &huffmanCode, string &encodedData) {
printCodes(root, "", huffmanCode);
for (char c : data) {
encodedData += huffmanCode[c];
}
}
// 将编码后的数据写入文件
void writeCompressedData(const string &encodedData) {
ofstream output("compressed.bin", ios::binary);
bitset<8> bits;
for (size_t i = 0; i < encodedData.length(); i += 8) {
string byteStr;
if (i + 8 <= encodedData.length()) {
byteStr = encodedData.substr(i, 8);
} else {
byteStr = encodedData.substr(i);
byteStr += string(8 - byteStr.length(), '0'); // 补足到8位
}
bits = bitset<8>(byteStr);
output.put(static_cast<unsigned char>(bits.to_ulong()));
}
output.close();
}
// 解码函数
void decode(HuffmanNode* root, const string &encodedData, string &decodedData) {
struct HuffmanNode* current = root;
for (char bit : encodedData) {
if (bit == '0')
current = current->left;
else
current = current->right;
if (!current->left && !current->right) {
decodedData += current->data;
current = root;
}
}
}
// 从压缩文件中读取数据
void readCompressedData(string &encodedData) {
ifstream input("compressed.bin", ios::binary);
unsigned char byte;
while (input.read(reinterpret_cast<char*>(&byte), 1)) {
encodedData += bitset<8>(byte).to_string();
}
input.close();
}
// 从文件中读取原始数据
string readFile(const string &fileName) {
ifstream file(fileName);
string content((istreambuf_iterator<char>(file)), istreambuf_iterator<char>());
file.close();
return content;
}
// 将解码后的数据写入文件
void writeFile(const string &fileName, const string &data) {
ofstream file(fileName);
file << data;
file.close();
}
int main() {
string inputFileName = "src.txt"; // 源文件名
string outputFileName = "decompressed.txt"; // 解压后的文件名
string data = readFile(inputFileName); // 读取原始数据
unordered_map<char, int> freqMap; // 统计字符频率
for (char c : data)
freqMap[c]++;
vector<char> chars;
vector<int> freq;
for (auto pair : freqMap) { // 创建字符和频率的向量
chars.push_back(pair.first);
freq.push_back(pair.second);
}
// 构建Huffman树
priority_queue<HuffmanNode*, vector<HuffmanNode*>, compare> minHeap;
for (int i = 0; i < chars.size(); ++i)
minHeap.push(new HuffmanNode(chars[i], freq[i]));
while (minHeap.size() != 1) {
HuffmanNode *left, *right, *top;
left = minHeap.top();
minHeap.pop();
right = minHeap.top();
minHeap.pop();
top = new HuffmanNode('$', left->freq + right->freq);
top->left = left;
top->right = right;
minHeap.push(top);
}
// 进行Huffman编码
unordered_map<char, string> huffmanCode;
string encodedData;
encode(minHeap.top(), data, huffmanCode, encodedData);
writeCompressedData(encodedData); // 写入压缩数据
// 读取并解码数据
string compressedData;
readCompressedData(compressedData);
string decodedData;
decode(minHeap.top(), compressedData, decodedData);
writeFile(outputFileName, decodedData); // 写入解压后的数据
cout << "解压完成,数据写入 " << outputFileName << endl;
return 0;
}
总结
-
bitset
构造问题:在尝试将encodedData
的子字符串转换为bitset<8>
时,如果子字符串的长度不足8位,则会触发错误。为解决这个问题,我在必要时通过添加额外的’0’字符来将子字符串长度扩展到8位。 -
读取压缩数据边界问题(卡了好长时间):在从压缩文件读取数据时,需要确保在字符串的末尾正确处理位数。这是通过在子字符串长度小于8位时添加’0’来实现的,确保每个
bitset
恰好为8位。 -
文件读写(非常不容易):正确地从文件中读取原始数据,并将压缩后的数据以及解压后的数据写回到文件中,是此过程的关键部分。这需要处理二进制文件和文本文件的不同读写方式。
-
代码中的
if (root->data != '$')
是用来检查当前的 Huffman 树节点是否是一个叶节点。在这个实现中,非叶节点(即内部节点)的data
字段被设定为特殊字符'$'
。这是一种常见的方法,用于区分内部节点和叶节点。在构建 Huffman 树的过程中,每当合并两个节点以创建一个新的内部节点时,这个新节点的
data
字段就被设置为一个特殊字符,这里使用的是'$'
。例如:top = new HuffmanNode('$', left->freq + right->freq); top->left = left; top->right = right;
在这个代码片段中,
'$'
表示一个内部节点,而不是原始数据中的一个字符。这样做的目的是为了确保在遍历 Huffman 树以生成编码时,只会为实际存在于原始数据中的字符(即叶节点)生成编码。 -
空格字符(如果存在于原始数据中)将被视为普通字符,并且会被分配一个 Huffman 编码。在读取原始数据并计算频率时,所有字符(包括空格、换行符等)都被考虑在内:
for (char c : data) freqMap[c]++;
在这里,
data
包含了原始文件的所有内容,包括空格。每个不同的字符(包括空格)都会在freqMap
中得到计数,并最终在 Huffman 树中表示为一个叶节点。因此,空格和其他所有字符一样,都会被正确处理并编码。
代码
-
Huffman树节点结构
struct HuffmanNode { char data; // 数据 unsigned freq; // 频率 HuffmanNode *left, *right; // 左右子节点 HuffmanNode(char data, unsigned freq) { left = right = nullptr; this->data = data; this->freq = freq; } };
-
比较函数,用于优先队列
struct compare { bool operator()(HuffmanNode* l, HuffmanNode* r) { return (l->freq > r->freq); } };
在 Huffman 编码算法中,我们需要频繁地选择频率最小的两个节点来合并它们,构建新的树节点。为了方便地实现这个操作,使用一个优先队列来存储所有的树节点。
struct compare
:定义了一个结构体compare
。这个结构体包含了一个重载的操作符operator()
。
bool operator()(HuffmanNode* l, HuffmanNode* r)
:这是一个函数运算符的重载。它使得compare
对象可以像函数一样被调用,并接受两个参数l
和r
,这两个参数都是指向HuffmanNode
的指针。
return (l->freq > r->freq);
:此行代码定义了比较的逻辑。当这个比较函数被用于优先队列时,队列会使用这个逻辑来维护其元素的顺序。这里的逻辑是:如果左侧节点l
的频率大于右侧节点r
的频率,则返回true
。这意味着在优先队列中,频率较高的节点会被排在后面。priority_queue<HuffmanNode*, vector<HuffmanNode*>, compare> minHeap;
-
生成Huffman编码
Huffman 编码算法通过构建一个二叉树来为每个字符生成一个唯一的二进制编码。在这棵树中,每个叶节点代表原始数据中的一个字符,而内部节点则用于导航。从根节点到任何叶节点的路径形成了该叶节点字符的 Huffman 编码:每当路径向左分叉时,编码添加 ‘0’;每当路径向右分叉时,编码添加 ‘1’。void printCodes(struct HuffmanNode* root, string str, unordered_map<char, string> &huffmanCode) { if (!root) return; if (root->data != '$') huffmanCode[root->data] = str; printCodes(root->left, str + "0", huffmanCode); printCodes(root->right, str + "1", huffmanCode); }
用于生成从每个字符到其对应的 Huffman 编码的映射。函数
printCodes
递归地遍历 Huffman 树,并为树中的每个叶节点(即代表原始数据中字符的节点)构建一个二进制编码字符串。- 这是一个递归函数,它接收三个参数:指向当前 Huffman 树节点的指针
root
、当前生成的编码字符串str
、以及一个引用到字符与编码映射的unordered_map
(huffmanCode
)。 if (!root) return;
:这是一个基本的递归退出条件。如果当前节点为空,函数将返回,不再进行进一步的递归调用。if (root->data != '$') huffmanCode[root->data] = str;
:这一行检查当前节点是否是一个叶节点(即代表一个字符的节点)。如果是,它将当前节点的字符(root->data
)和对应的编码(str
)添加到映射表huffmanCode
中。printCodes(root->left, str + "0", huffmanCode);
:递归调用自身来处理当前节点的左子节点。根据 Huffman 编码规则,向左移动意味着在当前编码字符串的末尾添加 ‘0’。printCodes(root->right, str + "1", huffmanCode);
:递归调用自身来处理当前节点的右子节点。向右移动意味着在当前编码字符串的末尾添加 ‘1’。
- 这是一个递归函数,它接收三个参数:指向当前 Huffman 树节点的指针
-
编码函数
这个
encode
函数的作用是将原始文本数据转换为由 Huffman 编码组成的字符串。这是通过替换数据中的每个字符为其对应的 Huffman 编码来实现的。最终,encodedData
包含了完整的 Huffman 编码字符串,这个字符串代表了经过压缩的原始数据。void encode(HuffmanNode* root, const string &data, unordered_map<char, string> &huffmanCode, string &encodedData) { printCodes(root, "", huffmanCode); for (char c : data) { encodedData += huffmanCode[c]; } }
Huffman 树的根节点
root
、要编码的原始文本数据data
、字符与其对应 Huffman 编码的映射表huffmanCode
,以及一个用于存储生成的编码字符串的引用encodedData
。printCodes(root, "", huffmanCode);
:这一行调用printCodes
函数来填充huffmanCode
映射表。这个映射表将每个字符映射到其对应的 Huffman 编码。函数开始时传入的是空字符串""
,因为初始时我们还没有任何编码。for (char c : data) { ... }
:这个循环遍历原始数据中的每个字符。encodedData += huffmanCode[c];
:对于数据中的每个字符c
,函数查找其在huffmanCode
映射表中对应的 Huffman 编码,并将这个编码添加到encodedData
字符串的末尾。通过这种方式,原始数据中的每个字符都被其对应的 Huffman 编码所替换。
-
将编码后的数据写入文件(思考难点)先使用的等长编码
以字符串形式表示的 Huffman 编码数据写入到一个二进制文件中。它的主要目的是将编码后的数据以二进制格式存储,以便更有效地利用空间。void writeCompressedData(const string &encodedData) { ofstream output("compressed.bin", ios::binary); bitset<8> bits; for (size_t i = 0; i < encodedData.length(); i += 8) { string byteStr; if (i + 8 <= encodedData.length()) { byteStr = encodedData.substr(i, 8); } else { byteStr = encodedData.substr(i); byteStr += string(8 - byteStr.length(), '0'); // 补足到8位 } bits = bitset<8>(byteStr); output.put(static_cast<unsigned char>(bits.to_ulong())); } output.close(); }
ofstream output("compressed.bin", ios::binary);
:这一行创建了一个用于写入二进制数据的文件流output
,打开名为 “compressed.bin” 的文件用于写入。bitset<8> bits;
:声明了一个bitset
变量bits
,它用于存储8位二进制数据。for (size_t i = 0; i < encodedData.length(); i += 8) { ... }
:这个循环以每8位为一组遍历整个 Huffman 编码字符串encodedData
。if (i + 8 <= encodedData.length()) { ... } else { ... }
:这个条件语句检查剩余的编码数据长度是否足够8位。如果足够,则直接截取8位;如果不足8位,则截取所有剩余的编码,并在后面补充0以使之达到8位。
bits = bitset<8>(byteStr);
:将8位编码字符串转换为bitset
。output.put(static_cast<unsigned char>(bits.to_ulong()));
:将bitset
转换为一个无符号字符(即一个字节),然后写入文件。output.close();
:关闭文件流。
由于 Huffman 编码通常会生成不同长度的编码串,因此在将这些编码写入文件之前,需要先将它们转换成固定长度的字节。这就是为什么要在不足8位的情况下用0来填充剩余的位数。这样,每8位编码就可以被转换成一个字节,并顺序写入二进制文件中。这种方法有效地压缩了原始数据,减少了所需的存储空间。(不然解码很难处理)
-
解码函数
void decode(HuffmanNode* root, const string &encodedData, string &decodedData) { struct HuffmanNode* current = root; for (char bit : encodedData) { if (bit == '0') current = current->left; else current = current->right; if (!current->left && !current->right) { decodedData += current->data; current = root; } } }
- 这个函数接受三个参数:Huffman 树的根节点
root
、编码后的数据字符串encodedData
和一个用于存储解码后文本的字符串引用decodedData
。 struct HuffmanNode* current = root;
:设置当前节点为根节点。在解码过程中,会从根节点开始遍历 Huffman 树。for (char bit : encodedData) { ... }
:这个循环遍历编码后的每一位。每一位都是 ‘0’ 或 ‘1’,代表 Huffman 编码中的每一步。if (bit == '0') current = current->left;
:如果当前位是 ‘0’,则向左移动到当前节点的左子节点。else current = current->right;
:如果当前位是 ‘1’,则向右移动到当前节点的右子节点。
if (!current->left && !current->right) { ... }
:检查当前节点是否是叶节点(即没有子节点)。在 Huffman 树中,叶节点代表原始数据中的字符。decodedData += current->data;
:如果当前节点是叶节点,则将其字符添加到解码后的数据字符串中。current = root;
:每当到达一个叶节点后,回到根节点开始解码下一个字符。
decode
函数的作用是读取由 ‘0’ 和 ‘1’ 组成的 Huffman 编码字符串,并根据这些编码在 Huffman 树中进行导航,以找到对应的字符。每当它到达一个叶节点时,就会找到一个原始数据中的字符,并将这个字符添加到解码后的数据中。 - 这个函数接受三个参数:Huffman 树的根节点
-
从压缩文件中读取数据
void readCompressedData(string &encodedData) { ifstream input("compressed.bin", ios::binary); unsigned char byte; while (input.read(reinterpret_cast<char*>(&byte), 1)) { encodedData += bitset<8>(byte).to_string(); } input.close(); }
ifstream input("compressed.bin", ios::binary);
:这一行创建了一个输入文件流input
,用于从名为 “compressed.bin” 的文件中读取数据。ios::binary
标志指明应以二进制模式打开文件。unsigned char byte;
:定义了一个unsigned char
类型的变量byte
,用于存储从文件中读取的每个字节。while (input.read(reinterpret_cast<char*>(&byte), 1)) { ... }
:这是一个循环,用于从文件中连续读取数据。input.read(reinterpret_cast<char*>(&byte), 1)
尝试从文件中读取一个字节的数据到byte
变量中。reinterpret_cast<char*>(&byte)
是将byte
的地址转换为char*
类型,因为read
函数需要一个char*
类型的参数。如果读取成功,循环继续;如果到达文件末尾或遇到读取错误,循环结束。encodedData += bitset<8>(byte).to_string();
:每次从文件中读取一个字节后,这一行将该字节转换为一个bitset
(一个8位的二进制数),然后转换为字符串并附加到encodedData
字符串的末尾。这样,encodedData
最终包含了文件中所有数据的二进制表示。input.close();
:最后,关闭文件流。
从一个二进制文件中读取压缩后的数据,并将其转换为一个二进制编码字符串。这是解压缩过程中的第一步,即从存储媒介中获取编码数据。这些编码数据随后可以被传递到 Huffman 解码函数中,以恢复原始文本数据。
-
从文件中读取原始数据(一次性读入只适合不太大的文件)
这个函数的功能是读取整个文件的内容并将其作为一个字符串返回。string readFile(const string &fileName) { ifstream file(fileName); string content((istreambuf_iterator<char>(file)), istreambuf_iterator<char>()); file.close(); return content; }
-
ifstream file(fileName);
:创建一个输入文件流file
,并打开名为fileName
的文件。这里fileName
是一个字符串,表示要打开的文件的名称。 -
string content((istreambuf_iterator<char>(file)), istreambuf_iterator<char>());
:这行代码创建了一个字符串content
,并使用了两个istreambuf_iterator<char>
对象来初始化它。istreambuf_iterator<char>(file)
创建了一个从file
文件流中读取字符的迭代器,而istreambuf_iterator<char>()
是一个“结束迭代器”,它表示一个默认初始化的迭代器,用于表示序列的末尾。这种初始化方法实际上是将文件流
file
中的所有内容一次性读取到字符串content
中。它利用了迭代器的范围初始化功能,从文件的开始到结束读取所有字符。 -
file.close();
:关闭文件流。 -
return content;
:返回包含文件内容的字符串。
对于非常大的文件,这种一次性读取所有内容到内存的方法可能会导致内存使用问题,但对于一般的应用场景,这是一种非常方便的读取文件的方法。(只能针对文件不太大的)
-
-
将解码后的数据写入文件
将data->写入文件中接受两个参数:要写入的文件的名称和要写入的数据。
void writeFile(const string &fileName, const string &data) { ofstream file(fileName); file << data; file.close(); }
ofstream file(fileName);
:这一行创建了一个ofstream
(输出文件流)对象file
。ofstream
用于写入文件。当创建ofstream
对象时,它会尝试打开指定的文件。这里fileName
是一个字符串,包含了要打开(或创建)的文件的名称。如果文件已经存在,这个操作通常会清空文件的当前内容(除非以特定模式打开文件,例如追加模式)。file << data;
:这行代码使用了文件流的插入操作符<<
来写入数据。这个操作会将data
字符串的内容写入到之前打开的文件中。数据会按照它在字符串中的顺序被写入。file.close();
:在数据写入完成后,这一行负责关闭文件流。
注意:它首先打开指定的文件(如果文件不存在,则创建文件),然后将传入的字符串数据写入文件,最后关闭文件。在该函数中,使用了
ofstream
,它是 C++ 标准库中用于文件写操作的一种基本工具,提供了一种方便的方式来向文件写入数据。 -
主函数
int main() { string inputFileName = "src.txt"; // 源文件名 string outputFileName = "decompressed.txt"; // 解压后的文件名 string data = readFile(inputFileName); // 读取原始数据 unordered_map<char, int> freqMap; // 统计字符频率 for (char c : data) freqMap[c]++; vector<char> chars; vector<int> freq; for (auto pair : freqMap) { // 创建字符和频率的向量 chars.push_back(pair.first); freq.push_back(pair.second); } // 构建Huffman树 priority_queue<HuffmanNode*, vector<HuffmanNode*>, compare> minHeap; for (int i = 0; i < chars.size(); ++i) minHeap.push(new HuffmanNode(chars[i], freq[i])); while (minHeap.size() != 1) { HuffmanNode *left, *right, *top; left = minHeap.top(); minHeap.pop(); right = minHeap.top(); minHeap.pop(); top = new HuffmanNode('$', left->freq + right->freq); top->left = left; top->right = right; minHeap.push(top); } // 进行Huffman编码 unordered_map<char, string> huffmanCode; string encodedData; encode(minHeap.top(), data, huffmanCode, encodedData); writeCompressedData(encodedData); // 写入压缩数据 // 读取并解码数据 string compressedData; readCompressedData(compressedData); string decodedData; decode(minHeap.top(), compressedData, decodedData); writeFile(outputFileName, decodedData); // 写入解压后的数据 cout << "解压完成,数据写入 " << outputFileName << endl; return 0; }
写到这里已经累死了!!!!!!