- 前言
- 一、霍夫曼压缩
- 1.构造霍夫曼树
- 2.根据霍夫曼树构建编码表
- 3.将霍夫曼树以二进制方式写入文件(读取解压时需要)
- 4.将字符数量值以二进制写入文本
- 5.再次读取文本,编码为二进制写入文件
- 二、解压霍夫曼二进制文件
- 1.读取霍夫曼树
- 2.读取字母数量
- 3.根据霍夫曼树还原文本
- 三、Windows的两个坑
- 1.莫名奇妙的解压错误
- 2.解压后与源文件不同
- 3.解决方法
- 总结
前言
最近学到霍夫曼编码,算法并不难,只是比较复杂。
压缩需要三步:
-
构造霍夫曼树
-
将树写入文件
-
使用霍夫曼树编码数据并写入文件
解压需要两步:
-
读取霍夫曼树,保存在文件开始处
-
使用树解码其他字节流
在windows下遇到两个坑
-
导致莫名其妙的解压失败,
-
导致原文件与解压文件不相同。
一、霍夫曼压缩
这是一位天才发明的算法,打破固定位长字符存储方法,改用根据字符频率重新编码,高频字符短位,低频长位,用以压缩文件,不限于文本。
1.构造霍夫曼树这是一颗霍夫曼树,其对应文本是:
it was the best of times it was the worst of times
每个字母都对应唯一一个二进制数字,这是一个天才的构造方式,字母只在叶子节点,从root出发,所有左节点是 0,右节点是 1,因此除了节点的子节点,无法找到含有此节点的二进制数,而字母都在叶子节点,无子节点,所以没有字母对应的二进制数是其他任何字母的前缀。
我们通过递推的方法,根据字母频率构建节点,让频率最小的两个节点组成一个大节点,此大节点的频率是两个子节点频率的和,一直递推构造,直至成为霍夫曼树。
对应代码:
//构建霍夫曼树
static auto buildTrie(const std::vector &freq) -> Node *
{
std::priority_queue,
std::function>
pq(greatNode);
for (int32_t c = 0; c != alphaSize; ++c)
{
if (freq[c] != 0)
{
pq.push(
new Node(static_cast(c), freq[c], nullptr, nullptr));
}
}
while (pq.size() > 1)
{
Node *x = pq.top();
pq.pop();
Node *y = pq.top();
pq.pop();
Node *parent = new Node(' ', x->freq + y->freq, x, y);
pq.push(parent);
}
return pq.top();
}
2.根据霍夫曼树构建编码表
有了树,就有了编码,现在需要将编码和字符对应起来,我们通过将每个 char 映射为下标,对应 vector 中的 “0101” 类似这种表示二进制的编码。
对应代码:
//构建每个字母对应的二进制字符编码表
auto buildCode(Node *root) -> std::vector
{
std::vector encodeTable(alphaSize);
buildCode(encodeTable, root, "");
return encodeTable;
}
//构建每个字母对应的二进制字符编码表
void buildCode(std::vector &encodeTable, Node *x,
const std::string &codeStr)
{
if (x->isLeaf())
{
encodeTable[static_cast(x->ch)] = codeStr;
return;
}
buildCode(encodeTable, x->left, codeStr + '0');
buildCode(encodeTable, x->right, codeStr + '1');
}
3.将霍夫曼树以二进制方式写入文件(读取解压时需要)
遍历霍夫曼树,对于非叶子节点,输出 0,对于叶子节点,输出 1 后接字符原本的二进制编码。
对应代码
//将霍夫曼树以二进制方法写入标准输出
void writeTrie(Node *x)
{
if (x->isLeaf())
{
BIO::bout.writeBool(true);
BIO::bout.writeChar(x->ch);
return;
}
BIO::bout.writeBool(false);
writeTrie(x->left);
writeTrie(x->right);
}
4.将字符数量值以二进制写入文本
这个就不放代码了。
5.再次读取文本,编码为二进制写入文件现在每个字母都能对应 “0110” 这种二进制值,我们读取每个字母,进行转换,将二进制值写入文件。
//根据字符编码表和输入字符串进行压缩。
static void zip(const std::vector &encodeTable,
const std::vector &input)
{
for (char i : input)
{
std::string code = encodeTable[static_cast(i)];
for (char j : code)
{
if (j == '1')
{
BIO::bout.writeBool(true);
}
else
{
BIO::bout.writeBool(false);
}
}
}
}
二、解压霍夫曼二进制文件
解压就是压缩的反过程,理解了压缩,解压就比较简单了。
1.读取霍夫曼树 //从标准输入以二进制方法读入霍夫曼树
auto readTrie() -> Node *
{
bool bl;
if (BIO::bin.readBool(bl) && bl)
{
char ch;
BIO::bin.readChar(ch);
return new Node(ch, 0, nullptr, nullptr);
}
Node *left = readTrie();
Node *right = readTrie();
return new Node(' ', 0, left, right);
}
2.读取字母数量
不放代码了。
3.根据霍夫曼树还原文本 for (int64_t i = 0; i != charNum; ++i)
{
x = expRoot;
while (!x->isLeaf() && BIO::bin.readBool(bl))
{
if (bl)
{
x = x->right;
}
else
{
x = x->left;
}
}
BIO::bout.writeChar(x->ch);
}
三、Windows的两个坑
在Windows环境编程,坑不少,但如此CD的,不多。
1.莫名奇妙的解压错误当压缩正确,但解压莫名奇妙的只能解压一部分,不同的文本解压的完整度不同,但相同的文本都是断在一个地方。
一开始我是觉得我的二进制读写代码出问题,改了多次,没有任何逻辑问题,于是怀疑二进制文件问题。
用C++最原始的函数进行拷贝,也是只能拷贝一部分,但问题找到了:“ 1A ” ,这是 2 个十六进制数,代表一个字节,这个字节在Linux和Unix系统都很普通,且在文本文档中,不存在。
但是经霍夫曼压缩的二进制文件,可能出现这个字节,它在Windows的意思是,文档结束符号。标准输入碰到此字节直接 eof。
这个坑可以说是上世纪的遗留bug,具体原因不知,和系统有关,经 StackOverflow 证实,确实如此。
2.解压后与源文件不同如果是文本文档,不一定能看出来,但二进制分析,和源文件确实不同,换成非文本文件,容易摸不着头脑。
很简单,就是当你写入 ‘n’,结果系统帮你改成 ‘rn’,凭空多了一个字符。
在文本文档不成问题,都是换行,其他文件就问题大了。
3.解决方法具体解决方案很简单,用 fstream 方式二进制读取和写入。
但我的程序架构是用标准输入输出进行管道处理,如此一来,就无法完成了。
不得已,还得用微软的东西补窟窿,更改 cin cout 的读写模式:
#ifdef _WIN64 #include#include #endif //使用标准输入前 #ifdef _WIN64 int result = _setmode(_fileno(stdin), _O_BINARY); result = _setmode(_fileno(stdout), _O_BINARY); #endif //使用标准输入后 #ifdef _WIN64 result = _setmode(_fileno(stdin), _O_TEXT); result = _setmode(_fileno(stdout), _O_TEXT); #endif
这个bug藏得很深。
总结
霍夫曼编码是一个天才的发明,理解简单,实现容易,但防不住 Windows 的坑,就是程序员的问题了。
以下是多次改造后的二进制读写类,比前几个版本更 C++ 了,效率低了点,因为 buffer 是 1 字节。
改成 8 字节比较合适,毕竟 64 位系统,一次读 8 字节比较快。
#ifndef BINSTDIO #define BINSTDIO #include#include #include #include #include namespace BIO { struct binstream { //不允许进行构造 binstream(const binstream &) = delete; //不允许进行构造 binstream(binstream &&) = delete; //不允许进行拷贝 auto operator=(const binstream &) -> binstream & = delete; //不允许进行拷贝 auto operator=(binstream &&) -> binstream & = delete; //返回局部静态变量 static auto getbin() -> binstream & { static binstream bin; return bin; } //比特流是否为空 inline auto isEmpty() -> bool { return !std::cin.good(); } //读取1位数据并返回一个bool值 auto readBool(bool &bit) -> bool { if (N == 0) { fillBuffer(); } bit = inbuffer[--N]; return !isEmpty(); } //读取8位数据并返回一个char值 auto readChar(char &ch) -> bool { if (N == buffersize) { auto x = inbuffer; fillBuffer(); ch = static_cast (x.to_ulong()); return true; } if (N >= 0) { auto x = inbuffer; x <<= (buffersize - N); int oldN = N; fillBuffer(); N = oldN; x |= (inbuffer >> N); ch = static_cast (x.to_ulong()); return !isEmpty(); } return false; } //读取r(1~16)位数据并返回一个char值 auto readChar(char &ch, int r) -> bool { if (r < 1 || r > buffersize) { return false; } if (r == buffersize) { return readChar(ch); } char x = 0; for (int i = 0; i != r; ++i) { x <<= 1; bool bit; readBool(bit); if (bit) { x |= 1; } } ch = x; return !isEmpty(); } //读取int值 auto readInt(int &num) -> bool { int x = 0; char ch; for (int i = 0; i != 4; i++) { readChar(ch); x <<= buffersize; x |= ch; } num = x; return !isEmpty(); } //读取int64 auto readInt64(int64_t &num) -> bool { int64_t x = 0; char ch; for (int i = 0; i != 8; i++) { readChar(ch); x <<= buffersize; x |= ch; } num = x; return !isEmpty(); } //关闭比特流 void close() { std::cin.setstate(std::ios::eofbit); } //恢复流状态 void clear() { std::cin.clear(); } private: //私有化默认构造,用于单例模式 binstream() = default; //读流到缓冲区 void fillBuffer() { char ch; // if (fread(&ch, 1, 1, stdin) != 0U) if (std::cin.read(&ch, 1)) { inbuffer = ch; N = buffersize; } else { // inbuffer = -1; N = -1; } } static constexpr int buffersize = 8; //缓冲区 std::bitset inbuffer = 0; //缓冲区比特指针 int N = 0; }; auto &bin = BIO::binstream::getbin(); struct boutstream { boutstream(const boutstream &) = delete; boutstream(boutstream &&) = delete; auto operator=(const boutstream &) -> boutstream & = delete; auto operator=(boutstream &&) -> boutstream & = delete; //取得局部静态变量 static auto getbout() -> boutstream & { static boutstream bout; return bout; } //写入指定的比特 void writeBool(bool bit) { outbuffer <<= 1; if (bit) { outbuffer[0] = true; } N++; if (N == buffersize) { char ch = static_cast (outbuffer.to_ulong()); std::cout.write(&ch, 1); std::cout.flush(); N = 0; outbuffer = 0; } } //写入指定的8位字符 void writeChar(char ch) { if (N == 0) { std::cout.write(&ch, 1); return; } std::bitset bf = ch; for (int i = buffersize - 1; i != -1; --i) { writeBool(bf[i]); } } //写入指定字符的r(1~8)位 void writeChar(char ch, int r) { if (r == buffersize) { writeChar(ch); return; } if (r < 1 || r > buffersize) { return; } std::bitset bf = ch; for (int i = r - 1; i != -1; --i) { writeBool(bf[i]); } } //写入int值 void writeInt(int32_t x) { writeChar(static_cast (x >> 24)); writeChar(static_cast (x >> 16)); writeChar(static_cast (x >> 8)); writeChar(static_cast (x >> 0)); } //写入int64类型值 void writeInt64(int64_t x) { writeChar(static_cast (x >> 56)); writeChar(static_cast (x >> 48)); writeChar(static_cast (x >> 40)); writeChar(static_cast (x >> 32)); writeChar(static_cast (x >> 24)); writeChar(static_cast (x >> 16)); writeChar(static_cast (x >> 8)); writeChar(static_cast (x >> 0)); } //关闭比特流, 刷新缓冲流 void close() { clearBuffer(); std::cout.flush(); } private: boutstream() = default; void clearBuffer() { if (N <= 0) { return; } if (N > 0) { outbuffer <<= (buffersize - N); } char ch = static_cast (outbuffer.to_ulong()); std::cout.write(&ch, 1); N = 0; outbuffer = 0; } static constexpr int buffersize = 8; //缓冲区 std::bitset outbuffer = 0; //缓冲区比特指针 int N = 0; }; auto &bout = BIO::boutstream::getbout(); inline void BinaryDump(char *argv[]) { int width = std::stoi(argv[1]); if (width == 0) { return; } int cnt = 0; bool bl; while (bin.readBool(bl)) { if (cnt != 0 && cnt % width == 0) { std::cout << 'n'; } if (bl) { std::cout << '1'; } else { std::cout << '0'; } ++cnt; } std::cout << std::endl; std::cout << cnt << " bits" << std::endl; } } // namespace BIO #endif



