huffman压缩

基于Huffman编码的文件压缩算法详解

引言

文件压缩技术通过减少数据冗余提升存储和传输效率,其中Huffman编码作为一种经典的无损压缩算法,在文本、图像和通信领域广泛应用。本文详细介绍Huffman编码的核心原理、实现细节、工程挑战与优化方案,并提供完整的C++代码实现。


一、Huffman编码原理

 

1. 核心思想

变长编码:高频字符使用短编码,低频字符使用长编码,降低整体数据量。  

前缀码特性:任意字符编码不为其他编码的前缀,确保解码唯一性。  

 

2. 构建Huffman树

1. 统计字符频率:遍历文件,统计每个字符出现次数。  

2. 构建最小堆:以字符频率为权值,生成最小优先队列。  

3. 合并节点:重复选取权值最小的两个节点合并,直到形成完整二叉树。  

4. 生成编码表:从根节点到叶子路径,左分支标记0,右分支标记1。  

 

3. 压缩与解压流程

1. 压缩:  

        根据编码表替换源文件字符,生成二进制压缩文件。  

        保存字符频率表至压缩文件头部,供解压时重建Huffman树。  如果直接解压,那么多出来的几位比特位也被解压了,所以需要考虑总文件的大小,控制真正读取的位数。

//压缩文件格式:
txt(源文件后缀)

4(字符数量)

A:1

B:3

C:5

D:7

96 DF FC 00

2. 解压:  

        读取频率表重建Huffman树。  

        遍历二进制码流,从根节点按0/1走向叶子节点,还原原始字符。  

 

        形成编码文件后,可以使用unordered_map<string , char>根据key值也就是string查找到对应的char。

        也可以利用huffman树解压缩,从根节点走,左0右1,直到到达叶子结点,用这种方法就不用每次查表了,但是要在压缩文件里保存每个字符对应的次数。


二、代码实现解析

1. 核心数据结构

// Huffman树节点
template<class W>
struct HuffmanTreeNode {
    HuffmanTreeNode<W>* _left, *_right, *_parent;
    W _weight; // 权值(字符频率)
    HuffmanTreeNode(const W& weight = W()) 
        : _left(nullptr), _right(nullptr), _parent(nullptr), _weight(weight) {}
};

// 优先队列比较规则
template<class W>
struct CompareNode {
    bool operator()(const HuffmanTreeNode<W>* n1, const HuffmanTreeNode<W>* n2) {
        return n1->_weight > n2->_weight; // 小根堆
    }
};


2. 构建Huffman树

HuffmanTree(const std::vector<W>& vw, const W& valid) {
    std::priority_queue<Node*, std::vector<Node*>, CompareNode<W>> q;
    for (auto& e : vw) {
        if (valid != e) q.push(new Node(e));
    }
    while (q.size() > 1) {
        Node* left = q.top(); q.pop();
        Node* right = q.top(); q.pop();
        Node* parent = new Node(left->_weight + right->_weight);
        parent->_left = left; left->_parent = parent;
        parent->_right = right; right->_parent = parent;
        q.push(parent);
    }
    _root = q.top();
}

3. 压缩与解压实现

压缩:  

void CompressFile(const string& filePath) {
      // 统计字符频率 -> 构建Huffman树 -> 生成编码表 -> 写入压缩文件
      // ...
      for (size_t i = 0; i < rdSize; ++i) {
          string& strCode = _fileInfo[rdBuff[i]]._chCode;
          for (char bit : strCode) { // 按编码替换字符
              bits <<= 1;
              if (bit == '1') bits |= 1;
              if (++bitCount == 8) { // 满8位写入文件
                  fputc(bits, fout);
                  bits = bitCount = 0;
              }
          }
      }
  }


解压:  

void UnCompressFile(const string& filePath) {
      // 读取频率表 -> 重建Huffman树 -> 解析二进制码流
      HuffmanTreeNode<ByteInfo>* cur = ht.GetRoot();
      while (读取字节流) {
          for (int j = 0; j < 8; j++) {
              if (ch & 0x80) cur = cur->_right; // 按位遍历树
              else cur = cur->_left;
              if (cur是叶子节点) {
                  写入解码字符;
                  cur = 重置到根节点;
              }
              ch <<= 1;
          }
      }
  }

三、实现中的挑战与解决方案

1. 读不了换行后的数据

问题:如果文件存在换行,则会默认读取结束。

解决:在读取新一行数据时,判断如果取到的是空,说明此时是换行,那么手动加上'\n',然后再读一遍,取到 :次数,就可以继续正确读取了。

2. 汉字编码错误

问题:汉字采用多字节编码(如UTF-8),char类型处理符号位引发数据错误。  

描述:常见的汉字编码如 UTF - 8、GBK 等,通常使用多个字节来表示一个汉字。以 UTF - 8 为例,它是一种变长编码,一个汉字可能由 2 到 4 个字节组成,并且这些字节的最高位通常为 1,而char类型通常是 1 个字节,是有符号类型,其取值范围一般是 -128 到 127,也就是说这意味着char的最高位(第 7 位)被用作符号位,0 代表正数,1 代表负数。

因为unsigned char是无符号类型,取值范围是 0 到 255。它没有符号位,所有 8 位都用于表示数值。

解决:将char改用unsigned char类型,确保8位全用于数据存储。  

3. 文件解压不完整

问题:文本文件与压缩文件处理结束方式不同,EOF 与 “FF” 。

描述:打开、写入的是文本文件,检测有没有读到末尾,就是看是否到EOF。

而压缩文件本质是二进制类型的文件,一旦读到文本文件转换成的FF,就认为结束不往后走了。

解决:将读取、写入的方式从文本文件改成二进制文件,r/w改成rb/wb

4. 文件解压未考虑末尾填充位

问题:未处理压缩数据末尾不足8位的填充位,导致解码错误。  

解决:记录有效位数,解压时仅解析实际数据位。  


四、性能分析与优化

1. 压缩率对比

文本文件:37% ~ 75%       

二进制文件:24% ~ 41%  

压缩的结果取决于字符种类的多少,字符种类的多少会影响哈夫曼树的结构和数据分布的复杂性。其中,平均下来要是每个字节的编码小于8位,那么文件会变小,要是多余8位,就会变大。

2. 优化策略

内存管理:使用智能指针(unique_ptr)避免内存泄漏。  

I/O优化:引入缓冲区减少磁盘访问次数。  

多线程:并行处理文件分块,提升大文件压缩速度。  


五、扩展:范式Huffman树

核心改进:

        强制同一层叶子节点左对齐,按字符顺序排列,无需存储完整频率表。  

优势:  
        压缩头信息从O(n)降至O(1)。  

        解压时通过编码位长直接计算编码值,无需遍历树。  

具体:

        huffman树压缩完成后,必须在压缩文件中保存字节频率信息后,才可以解压缩,如果字节频率信息比较大,也会影响压缩率。且解压时需要通过不断遍历还原的huffman树,效率也会打折扣,因此在工程中一般很少使用huffman直接进行压缩,而是使用范式huffman树。

        范式huffman树,是在huffman树的基础之上,进行了一些强制性的约定,即:对于同一层节点中,所有的叶子节点都调整到左边,然后,对于同一层的叶子节点按照符号顺序从小到大调整,最后按照左0右1的方式分配编码。

        使用范式huffman树,可以提高压缩效率,提高不了压缩率,就不用从叶子往根去推,编码可以算出来,同一层的编码位长一样,从左往右编码加一,下一层编码加一,再左移层数差位。

        怎么得到编码表?排序字节频率表,以编码位长为第一字段,编码大小为第二字段,不用保存频率,也就是说只需要保存256个字节,同时也不用走递归了。

        解压缩时,也只需要编码和编码位长,算出编码,得出解码表。


六、结语

Huffman编码以简洁高效的特点成为无损压缩的基石。本次通过完整代码实现和问题剖析,展现了从理论到工程的完整过程。查阅资料了解到实际应用中可结合LZ77等算法(如ZIP)进一步提升压缩率。未来可探索范式Huffman树与多线程加速的深度融合。  


附录:完整代码与测试用例 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值