2022-05-30 霍夫曼编码及在windows下的两个坑(C++)


前言

最近学到霍夫曼编码,算法并不难,只是比较复杂。

压缩需要三步:

  1. 构造霍夫曼树

  2. 将树写入文件

  3. 使用霍夫曼树编码数据并写入文件

解压需要两步:

  1. 读取霍夫曼树,保存在文件开始处

  2. 使用树解码其他字节流

在windows下遇到两个坑

  1. 导致莫名其妙的解压失败,

  2. 导致原文件与解压文件不相同。


一、霍夫曼压缩

这是一位天才发明的算法,打破固定位长字符存储方法,改用根据字符频率重新编码,高频字符短位,低频长位,用以压缩文件,不限于文本。

1.构造霍夫曼树

0
1
0
1
1
0
0
0
1
0
root
0
01_32
000_e
1
0010_o
0011_w
100_s
1011_i
10100_r
10101_m
111_t
11000_f
11001_h
11011_a
110100_10
110101_b

这是一颗霍夫曼树,其对应文本是:

it was the best of times it was the worst of times

每个字母都对应唯一一个二进制数字,这是一个天才的构造方式,字母只在叶子节点,从root出发,所有左节点是 0,右节点是 1,因此除了节点的子节点,无法找到含有此节点的二进制数,而字母都在叶子节点,无子节点,所以没有字母对应的二进制数是其他任何字母的前缀。

我们通过递推的方法,根据字母频率构建节点,让频率最小的两个节点组成一个大节点,此大节点的频率是两个子节点频率的和,一直递推构造,直至成为霍夫曼树。

对应代码:

    //构建霍夫曼树
    static auto buildTrie(const std::vector<int32_t> &freq) -> Node *
    {
        std::priority_queue<Node *, std::vector<Node *>,
                            std::function<bool(Node *, Node *)>>
            pq(greatNode);
        for (int32_t c = 0; c != alphaSize; ++c)
        {
            if (freq[c] != 0)
            {
                pq.push(
                    new Node(static_cast<char>(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('\0', x->freq + y->freq, x, y);
            pq.push(parent);
        }
        return pq.top();
    }

2.根据霍夫曼树构建编码表

有了树,就有了编码,现在需要将编码和字符对应起来,我们通过将每个 char 映射为下标,对应 vector 中的 “0101” 类似这种表示二进制的编码。

对应代码:

    //构建每个字母对应的二进制字符编码表
    auto buildCode(Node *root) -> std::vector<std::string>
    {
        std::vector<std::string> encodeTable(alphaSize);
        buildCode(encodeTable, root, "");
        return encodeTable;
    }
    
    //构建每个字母对应的二进制字符编码表
    void buildCode(std::vector<std::string> &encodeTable, Node *x,
                   const std::string &codeStr)
    {
        if (x->isLeaf())
        {
            encodeTable[static_cast<unsigned char>(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<std::string> &encodeTable,
                    const std::vector<char> &input)
    {
        for (char i : input)
        {
            std::string code = encodeTable[static_cast<unsigned char>(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', 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’,结果系统帮你改成 ‘\r\n’,凭空多了一个字符。

在文本文档不成问题,都是换行,其他文件就问题大了。

3.解决方法

具体解决方案很简单,用 fstream 方式二进制读取和写入。

但我的程序架构是用标准输入输出进行管道处理,如此一来,就无法完成了。

不得已,还得用微软的东西补窟窿,更改 cin cout 的读写模式:

#ifdef _WIN64
#include <fcntl.h>
#include <io.h>
#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 <bitset>
#include <cassert>
#include <cstdio>
#include <iostream>
#include <string>

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<char>(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<char>(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<buffersize> 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<char>(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<buffersize> 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<buffersize> bf = ch;
        for (int i = r - 1; i != -1; --i)
        {
            writeBool(bf[i]);
        }
    }
    //写入int值
    void writeInt(int32_t x)
    {
        writeChar(static_cast<char>(x >> 24));
        writeChar(static_cast<char>(x >> 16));
        writeChar(static_cast<char>(x >> 8));
        writeChar(static_cast<char>(x >> 0));
    }
    //写入int64类型值
    void writeInt64(int64_t x)
    {
        writeChar(static_cast<char>(x >> 56));
        writeChar(static_cast<char>(x >> 48));
        writeChar(static_cast<char>(x >> 40));
        writeChar(static_cast<char>(x >> 32));
        writeChar(static_cast<char>(x >> 24));
        writeChar(static_cast<char>(x >> 16));
        writeChar(static_cast<char>(x >> 8));
        writeChar(static_cast<char>(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<char>(outbuffer.to_ulong());
        std::cout.write(&ch, 1);
        N = 0;
        outbuffer = 0;
    }
    static constexpr int buffersize = 8;
    //缓冲区
    std::bitset<buffersize> 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
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不停感叹的老林_<C 语言编程核心突破>

不打赏的人, 看完也学不会.

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值