前言

哈夫曼编码是一种贪心算法和二叉树结合的字符编码方式,具有广泛的应用背景,最直观的是文件压缩。本文主要讲述如何用哈夫曼编解码实现文件的压缩和解压,并给出代码实现。

哈夫曼编码的概念

哈夫曼树又称作最优树,是一种带权路径长度最短的树,而通过哈夫曼树构造出的编码方式称作哈夫曼编码。

也就是说哈夫曼编码是一个通过哈夫曼树进行的一种编码,一般情况下,以字符 “0” 与 “1” 表示。编码的实现过程很简单,只要实现哈夫曼树,通过遍历哈夫曼树,这里我们从根节点开始向下遍历,如果下个节点是左孩子,则在字符串后面追加 “0”,如果为其右孩子,则在字符串后追加 “1”。结束条件为当前节点为叶子节点,得到的字符串就是叶子节点对应的字符的编码。

哈夫曼树实现

根据贪心算法的思想实现,把字符出现频率较多的字符用稍微短一点的编码,而出现频率较少的字符用稍微长一点的编码。哈夫曼树就是按照这种思想实现,下面将举例分析创建哈夫曼树的具体过程。下面表格的每一行分别对应字符及出现频率,根据这些信息就能创建一棵哈夫曼树。

字符 出现频率 编码 总二进制位数
a 500 1 500
b 250 01 500
c 120 001 360
d 60 0001 240
e 30 00001 150
f 20 00000 100

如下图,将每个字符看作一个节点,将带有频率的字符全部放到优先队列中,每次从队列中取频率最小的两个节点 a 和 b(这里频率最小的 a 作为左子树),然后新建一个节点R,把节点设置为两个节点的频率之和,然后把这个新节点R作为节点A和B的父亲节点。最后再把R放到优先队列中。重复这个过程,直到队列中只有一个元素,即为哈夫曼树的根节点。

由上分析可得,哈夫曼编码的需要的总二进制位数为 500 + 500 + 360 + 240 + 150 + 100 = 1850。上面的例子如果用等长的编码对字符进行压缩,实现起来更简单,6 个字符必须要 3 位二进制位表示,解压缩的时候每次从文本中读取 3 位二进制码就能翻译成对应的字符,如 000,001,010,011,100,101 分别表示 a,b,c,d,e,f。则需要总的二进制位数为 (500 + 250 + 120 + 60 + 30 + 20)* 3 = 2940。对比非常明显哈夫曼编码需要的总二进制位数比等长编码需要的要少很很多,这里的压缩率为 1850 / 2940 = 62%。哈夫曼编码的压缩率通常在 20% ~90% 之间。

下面代码是借助标准库的优先队列 std::priority_queque 实现哈夫曼树的代码简单实现,构造函数需要接受 afMap 入参,huffmanCode 函数是对象的唯一对外方法,哈夫曼编码的结果会写在 codeMap 里面。这部分是创建哈夫曼树的核心代码,为方便调试,我还实现了打印二叉树树形结构的功能,这里就补贴代码,有兴趣的同学可以到文末给出的 github 仓库中下载。

using uchar = unsigned char;

struct Node {
uchar c;
int freq;
Node *left;
Node *right;
Node(uchar _c, int f, Node *l = nullptr, Node *r = nullptr)
: c(_c), freq(f), left(l), right(r) {}
bool operator<(const Node &node) const { //重载,优先队列的底层数据结构std::heap是最大堆
return freq > node.freq;
}
}; class huffTree {
public:
huffTree(const std::map<uchar, int>& afMap) {
for (auto i : afMap) {
Node n(i.first, i.second);
q.push(n);
}
_makehuffTree();
}
~huffTree() {
Node node = q.top();
_deleteTree(node.left);
_deleteTree(node.right);
}
void huffmanCode(std::map<uchar, std::string>& codeMap) {
Node node = q.top();
std::string prefix;
_huffmanCode(&node, prefix, codeMap);
}
private:
static bool _isLeaf(Node* n) {
return n->left == nullptr && n->right == nullptr;
}
void _deleteTree(Node* n) {
if (!n) return ;
_deleteTree(n->left);
_deleteTree(n->right);
delete n;
}
void _makehuffTree() {
while (q.size() != ) {
Node *left = new Node(q.top()); q.pop();
Node *right = new Node(q.top()); q.pop();
Node node('R', left->freq + right->freq, left, right);
q.push(node);
}
}
void _huffmanCode(Node *root, std::string& prefix,
std::map<uchar, std::string>& codeMap) {
std::string tmp = prefix;
if (root->left != nullptr) {
prefix += '';
if (_isLeaf(root->left)) {
codeMap[root->left->c] = prefix;
} else {
_huffmanCode(root->left, prefix, codeMap);
}
}
if (root->right != nullptr) {
prefix = tmp;
prefix += '';
if (_isLeaf(root->right)) {
codeMap[root->right->c] = prefix;
} else {
_huffmanCode(root->right, prefix, codeMap);
}
}
}
private:
std::priority_queue<Node> q;
};

文件压缩实现

首先需要给出文件压缩和下面将要提到的文件解压缩的公共头文件,如下:

//得到index位的值,若index位为0,则GET_BYTE值为假,否则为真
#define GET_BYTE(vbyte, index) (((vbyte) & (1 << ((index) ^ 7))) != 0)
//index位置1
#define SET_BYTE(vbyte, index) ((vbyte) |= (1 << ((index) ^ 7)))
//index位置0
#define CLR_BYTE(vbyte, index) ((vbyte) &= (~(1 << ((index) ^ 7)))) using uchar = unsigned char; struct fileHead {
char flag[]; //压缩二进制文件头部标志 ycy
uchar alphaVariety; //字符种类
uchar lastValidBit; //最后一个字节的有效位数
char unused[]; //待用空间
}; //这个结构体总共占用16个字节的空间 struct alphaFreq {
uchar alpha; //字符,考虑到文件中有汉字,所以定义成uchar
int freq; //字符出现的频度
alphaFreq() {}
alphaFreq(const std::pair<char, int>& x)
: alpha(x.first), freq(x.second) {}
};

下面是文件压缩的代码具体实现。过程其实相对简单,理解起来不难。首先需要读取文件信息,统计每一个字符出现的次数,这里实现是从 std::map 容器以字符为 key 累加统计字符出现的次数。然后,用统计的结果 _afMap 创建哈夫曼树,得到相应的每个字符的哈夫曼编码 _codeMap。最后,就是将数据写入压缩文件,该过程需要先写入文件头部信息, 即结构体 fileHead 的内容,这部分解压缩的时候进行格式校验等需要用到。接着将 _afMap 的字符及频率数据依次写入文件中,这部分是解压缩时重新创建哈夫曼树用来译码。到这一步就依次读取源文件的每一个字符,将其对应的哈夫曼编码写进文件中去。至此压缩文件的过程结束。下面的代码不是很难,我就不加注释了。

class huffEncode {
public:
bool encode(const char* srcFilename, const char* destFilename) {
if (!_getAlphaFreq(srcFilename)) return false;
huffTree htree(_afMap);
htree.huffmanCode(_codeMap);
return _encode(srcFilename, destFilename);
}
private:
int _getLastValidBit() {
int sum = ;
for (auto it : _codeMap) {
sum += it.second.size() * _afMap.at(it.first);
sum &= 0xFF;
}
sum &= 0x7;
return sum == ? : sum;
}
bool _getAlphaFreq(const char* filename) {
uchar ch;
std::ifstream is(filename, std::ios::binary);
if (!is.is_open()) {
printf("read file failed! filename: %s", filename);
return false;
}
is.read((char*)&ch, sizeof(uchar));
while (!is.eof()) {
_afMap[ch]++;
is.read((char*)&ch, sizeof(uchar));
}
is.close();
return true;
}
bool _encode(const char* srcFilename, const char* destFilename) {
uchar ch;
uchar value;
int bitIndex = ;
fileHead filehead = {'e', 'v', 'e', 'n'};
filehead.alphaVariety = (uchar) _afMap.size();
filehead.lastValidBit = _getLastValidBit(); std::ifstream is(srcFilename, std::ios::binary);
if (!is.is_open()) {
printf("read file failed! filename: %s", srcFilename);
return false;
}
std::ofstream io(destFilename, std::ios::binary);
if (!io.is_open()) {
printf("read file failed! filename: %s", destFilename);
return false;
} io.write((char*)&filehead, sizeof(fileHead));
for (auto i : _afMap) {
alphaFreq af(i);
io.write((char*)&af, sizeof(alphaFreq));
} is.read((char*)&ch, sizeof(uchar));
while (!is.eof()) {
std::string code = _codeMap.at(ch);
for (auto c : code) {
if ('' == c) {
CLR_BYTE(value, bitIndex);
} else {
SET_BYTE(value, bitIndex);
}
++bitIndex;
if (bitIndex >= ) {
bitIndex = ;
io.write((char*)&value, sizeof(uchar));
}
}
is.read((char*)&ch, sizeof(uchar));
} if (bitIndex) {
io.write((char*)&value, sizeof(uchar));
}
is.close();
io.close();
return true;
}
private:
std::map<uchar, int> _afMap;
std::map<uchar, std::string> _codeMap;
};

文件解压缩实现

文件解压缩其实就是哈夫曼编码的译码过程,处理过程相对于压缩过程来说相对复杂一点,但其实就是将文件编码按照哈夫曼编码的既定规则翻译出原来对应的字符,并将字符写到文件中的过程。较为详细的过程是先读取文件头部信息,校验文件格式是否是上面压缩文件的格式(这里是flag的四个字符为even),不是则返回错误。然后根据头部信息字符种类 alphaVariety(即字符的个数)依次读取字符及其频率,并将读取的内容放到  _afMap 中,然后创建哈夫曼树,得到相应的每个字符的哈夫曼编码 _codeMap,并遍历 _codeMap 创建以字符编码为 key 的译码器 _decodeMap,主要方便是后面译码的时候根据编码获取其对应的字符。然后读取压缩文件剩余的内容,每次读取一个字节即 8 个二进制位,获取哈夫曼树根节点,用一个树节点指针pNode指向根节点,然后逐个读取二进制,每次根据二进制位的值,当值为 0 指针走左子树,当值为 1 指针走右子树,并将值添加到 std::string 类型的字符串 code 后面,直到走到叶子结点位置为止。用 code 作为 key 可在译码器 _decodeMap 中取得对应的字符,将字符写到新文件中去。然后清空 code,pNode重新指向根节点,继续走上面的流程,直到读完文件内容。文件最后一个字节的处理和描述有点不一样,需根据文件头信息的最后一位有效位 lastValidBit 进行特殊处理,这里特别提醒一下。

class huffDecode {
public:
huffDecode() : _fileHead(nullptr), _htree(nullptr) {
_fileHead = new fileHead();
}
~huffDecode() {
if (!_fileHead) delete _fileHead;
if (!_htree) delete _htree;
}
private:
static bool _isLeaf(Node* n) {
return n->left == nullptr && n->right == nullptr;
}
long _getFileSize(const char* strFileName) {
std::ifstream in(strFileName);
if (!in.is_open()) return ; in.seekg(, std::ios_base::end);
std::streampos sp = in.tellg();
in.close();
return sp;
}
bool _getAlphaFreq(const char* filename) {
std::ifstream is(filename, std::ios::binary);
if (!is) {
printf("read file failed! filename: %s", filename);
return false;
} is.read((char*)_fileHead, sizeof(fileHead));
if (!(_fileHead->flag[] == 'e' &&
_fileHead->flag[] == 'v' &&
_fileHead->flag[] == 'e' &&
_fileHead->flag[] == 'n')) {
printf("not support this file format! filename: %s\n", filename);
return false;
}
for (int i = ; i < static_cast<int>(_fileHead->alphaVariety); ++i) {
alphaFreq af;
is.read((char*)&af, sizeof(af));
_afMap.insert(std::pair<char, int>(af.alpha, af.freq));
}
is.close();
return true;
}
bool _decode(const char* srcFilename,
const char* destFilename) {
long fileSize = _getFileSize(srcFilename); std::ifstream is(srcFilename, std::ios::binary);
if (!is) {
printf("read file failed! filename: %s", srcFilename);
return false;
}
is.seekg(sizeof(fileHead) + sizeof(alphaFreq) * _fileHead->alphaVariety); Node node = _htree->getHuffTree();
Node* pNode = &node; std::ofstream io(destFilename, std::ios::binary);
if (!io) {
printf("create file failed! filename: %s", destFilename);
return false;
} uchar value;
std::string code;
int index = ;
long curLocation = is.tellg();
is.read((char*)&value, sizeof(uchar));
while () {
if (_isLeaf(pNode)) {
uchar alpha = _decodeMap[code];
io.write((char*)&alpha, sizeof(uchar));
if (curLocation >= fileSize && index >= _fileHead->lastValidBit) {
break;
}
code.clear();
pNode = &node;
} if (GET_BYTE(value, index)) {
code += '';
pNode = pNode->right;
} else {
pNode = pNode->left;
code += '';
}
if (++index >= ) {
index = ;
is.read((char*)&value, sizeof(uchar));
curLocation = is.tellg();
}
} is.close();
io.close();
return true;
}
public:
bool decode(const char* srcFilename, const char* destFilename) {
if (!_getAlphaFreq(srcFilename)) return false;
long fileSize = _getFileSize(srcFilename);
_htree = new huffTree(_afMap);
_htree->watch();
_htree->huffmanCode(_codeMap); for (auto it : _codeMap) {
_decodeMap.insert(std::pair<std::string, uchar>(it.second, it.first));
} return _decode(srcFilename, destFilename);
}
private:
fileHead *_fileHead;
huffTree *_htree;
std::map<uchar, int> _afMap;
std::map<uchar, std::string> _codeMap;
std::map<std::string, uchar> _decodeMap;
};

总结

利用哈夫曼编解码实现文件的解压缩其实原理不是很难,但其需要用的编程知识其实相对较多,有优先队列、位运算、满二叉树、容器及文件操作等,想要实现的优雅其实不是很容易。而我在网上查到的 C++ 实现都不甚满意,所以决定自己实现,个人觉得还算比较满意,但因个人水平有限肯定会存在某些问题,请发现的朋友留言探讨。我觉得这个过程还是比较非常能锻炼自己的编程能力,作为一个小项目来练手再合适不过,不仅能够加深自己对位运算、C++标准库、二叉树及文件操作的理解,而且能够锻炼面向对象的编程思维。对了,不能忘记了,我代码实现的主要思想主要参考这位兄弟的文章,他是用 C 语言实现的,其实已经非常优雅,文章链接:https://blog.csdn.net/weixin_38214171/article/details/81626498

最后给出实现的源码链接:https://github.com/evenleo/huffman

哈夫曼编解码压缩解压文件—C++实现的更多相关文章

  1. 基于哈夫曼编码的压缩解压程序(C 语言)

    这个程序是研一上学期的课程大作业.当时,跨专业的我只有一点 C 语言和数据结构基础,为此,我查阅了不少资料,再加上自己的思考和分析,实现后不断调试.测试和完善,耗时一周左右,在 2012/11/19 ...

  2. 通过SharpZipLib来压缩解压文件

    在项目开发中,一些比较常用的功能就是压缩解压文件了,其实类似的方法有许多 ,现将通过第三方类库SharpZipLib来压缩解压文件的方法介绍如下,主要目的是方便以后自己阅读,当然可以帮到有需要的朋友更 ...

  3. huffman压缩解压文件【代码】

    距离上次写完哈夫曼编码已经过去一周了,这一周都在写huffman压缩解压,哎,在很多小错误上浪费了很多时间调bug.其实这个程序的最关键部分不是我自己想的,而是借鉴了某位园友的代码,但是,无论如何,自 ...

  4. .NET使用ICSharpCode.SharpZipLib压缩/解压文件

    SharpZipLib是国外开源加压解压库,可以方便的对文件进行加压/解压 1.下载ICSharpCode.SharpZipLib.dll,并复制到bin目录下 http://www.icsharpc ...

  5. 【转载】.NET压缩/解压文件/夹组件

    转自:http://www.cnblogs.com/asxinyu/archive/2013/03/05/2943696.html 阅读目录 1.前言 2.关于压缩格式和算法的基础 3.几种常见的.N ...

  6. C#使用SharpZipLib压缩解压文件

    #region 加压解压方法 /// <summary> /// 功能:压缩文件(暂时只压缩文件夹下一级目录中的文件,文件夹及其子级被忽略) /// </summary> // ...

  7. linux压缩解压文件

    首先进入文件夹 cd /home/ftp2/1520/web 压缩方法一:压缩web下的888.com网站 zip -r 888.com.zip888.com 压缩方法二:将当前目录下的所有文件和文件 ...

  8. Freebsd下压缩解压文件详解

    压缩篇: 把/usr/webgames目录下的文件打包.命名为bak.tar.gz 放到/usr/db-bak目录里 下面命令可以在任意目录执行.无视当前目录和将要存放文件的目录.tar -zcvf ...

  9. 跨平台的zip文件压缩处理,支持压缩解压文件夹

    根据minizip改写的模块,需要zlib支持 输出的接口: #define RG_ZIP_FILE_REPLACE 0 #define RG_ZIP_FILE_APPEND 1 //压缩文件夹目录, ...

随机推荐

  1. Qt在pro中实现条件编译

    https://www.cnblogs.com/Braveliu/p/5107550.html https://blog.csdn.net/simonforfuture/article/details ...

  2. 定时器( setInterval和 setTimeout)

    一.定时器setInterval-------常用的,反复循环的 <input type="button" value="停止" id="btn ...

  3. leetcode解题报告(18):Contains Duplicate

    描述 Given an array of integers, find if the array contains any duplicates. Your function should retur ...

  4. 解决JavaWeb项目报错:The origin server did not find a current representation for the target resource or is not willing to disclose that one exists.

    明明有项目和文件,而且别的项目都可以运行,偏偏这个不能用,报错The origin server did not find a current representation for the targe ...

  5. [端口安全]Hydra密码爆破

    目录 0x01 简介 0x02 常见参数 0x03 使用案例 0x04 密码字典 0x01 简介 Hydra中文名:九头蛇,这是一款相当强大的爆破工具,它基本支持了所有可爆破协议,而且容容错率非常好 ...

  6. vue从一个组件跳转到另一个组件页面router-link的试用

    需求从helloworld.vue页面跳到good.vue页面 1.helloworld.vue页面代码 <template> <div class="hello" ...

  7. 在开发iOS程序时对日期处理的总结

    小贴士(Tips)-iOS 由于iOS的设备对应多国语言,用户也可以选择不同的日历模式.比如日本的和历,泰国日历等等. 用户也可以自行设定24小时制或者12小时制来显示时间.这些设置会直接影响应用程序 ...

  8. 2018-2019 20175234 实验三《敏捷开发与XP实践》实验报告

    目录 2018-2019 20175234 实验三<敏捷开发与XP实践>实验报告 实验内容 实验中的问题 码云链接 参考资料 2018-2019 20175234 实验三<敏捷开发与 ...

  9. swoole的websockte例子

    服务器的环境,建议用bt.cn的一键环境 服务端: <?php /** * Created by PhpStorm. * User: Administrator * Date: 2019\5\2 ...

  10. OpenJudge计算概论-奇数求和

    /*=================================================== 奇数求和 总时间限制: 1000ms 内存限制: 65536kB 描述 计算非负整数 m 到 ...