一、前言
如果你学习数据结构,就一定会学到Huffman树,而Huffman编码实际上上就是zip压缩的核心部分,所以,如果已经学习了Huffman树,为何不尝试写一个压缩程序出来呢?
如果你没有学习Huffman树,那咱们就一起先学习一下Huffman树吧。
二、Huffman树压缩文件
定义:Huffman树,又称为最优二叉树,是加权路径长度最短的二叉树。
建立:
这样建立的树,保证所有数据成员都在叶子节点上,且数越小,离根节点越远,越大,离根节点越近,那么这样的特点应用于压缩中是很关键的,我们可以让出现次数少的字符编码长一些,次数多的字符编码短一些。接下来我们看看压缩的步骤吧~
1>统计要压缩的文件中字符出现的次数。
遍历一遍文件,将字符出现的次数统计在一个结构体数组里,数组里包含字符,字符出现的次数,对该字符的编码。
2>用得到的数组构建一个Huffman树。
因为每次要取最小值,所以这里考虑建立一个小堆。
3>得到Huffman编码
怎么得到呢?向右为1,向左为0,就是这么简单,我画图示意一下:
原本用一个char表示的字符,现在只占了几个位,这就是为什么能将文件压缩。
4>向压缩文件里写入Huffman编码。
写入的时候,满8个位写进去,如果最后不足8个位,先补齐,解压的时候要注意,解压到源文件字符数的时候停止即可。源文件的总字符数可以在第一次遍历统计出现的字符个数时统计,还有一种方法就是,仔细观察Huffman树就知道,它的根节点的大小,其实就是所有叶子节点相加的和。所以,根节点的大小就是源文件里所有字符出现的总次数。
至此,压缩就结束了。
但是,怎么解压缩呢?解压缩至少也得已知这样的一颗树才行啊,所以,我们在压缩完成后要建立一个配置文件。
5>建立配置文件
配置文件里要存储源文件字符及出现的次数。有了这样的配置文件,就可以再次构建Huffman树!
三、解压缩
1.根据配置文件得出出现的字符和对应出现次数
四、我遇到的问题
1>编译时不通过,一大堆的错误,我找了半天!最后发现是一个很简单的问题,我的Huffman树使用的是C++模板实现的,模板不能分离编译,而我在压缩时建立Huffman树是在另一个文件中进行的,所以编译不通过。
解决方法:.h后缀改成.hpp,重新包一下头文件ok。
2>文件的打开方式。这里打开文件一定要用二进制形式,"wb","rb".因为二进制打开和文本打开其实是有区别的。文本方式打开,会对‘\n’进行特殊处理,那如果这个字符本身就是'\n'.这就会出现问题,所以使用二进制打开,特点:不进行任何处理,是什么就是什么。
3>压缩后解压缩的图片打不开,经过我反复查找,终于发现是配置文件里对‘\0’的处理问题,我在写配置文件起初是用一个string把字符和它出现的次数连接起来放进去。比如:a,3 这样带来的问题是 \0,200 写的时候是以c字符串的形式写的,所以遇见'\0'就终止了,那么在解压缩的时候就会出问题。
解决方法:先把字符放进去,再把剩下的构建成string对象放进去。
4>压缩汉字出现问题 汉字字符范围是0-255
5>二次压缩效率很低,压缩一次之后字符出现的次数相差不是很大
模板不可以分离编译:
五、源码
1>Huffman树
//构建Huffman树 #pragma once #include<iostream> #include"Heap.h" using namespace std; template<class W> struct HuffmanTreeNode { HuffmanTreeNode<W>* _left; HuffmanTreeNode<W>* _right; HuffmanTreeNode<W>* _parent; //里面存的是个W 结构的魅力 W _w; // Ȩֵ //三叉树 方便构建 HuffmanTreeNode(const W& w) :_left(NULL) ,_right(NULL) ,_parent(NULL) ,_w(w) {} }; template<class W> class HuffmanTree { public: typedef HuffmanTreeNode<W> Node; HuffmanTree() :_root(NULL) {} HuffmanTree(W* w, size_t n, const W& invalid) { //适配器 struct NodeComapre { bool operator()(Node* l, Node* r) { return l->_w < r->_w; } }; Heap<Node*, NodeComapre> minHeap; for (size_t i = 0; i < n; ++i) { //重载了!= if (w[i] != invalid) minHeap.Push(new Node(w[i])); // 一步顶两步 } while (minHeap.Size() > 1) { Node* left = minHeap.Top(); minHeap.Pop(); Node* right = minHeap.Top(); minHeap.Pop(); //重载了+ Node* parent = new Node(left->_w + right->_w); parent->_left = left; parent->_right = right; left->_parent = parent; right->_parent = parent; minHeap.Push(parent); } _root = minHeap.Top(); } ~HuffmanTree() { Destory(_root); _root = NULL; } Node* GetRoot() { return _root; } protected: void Destory(Node* root) { if (root == NULL) return; Destory(root->_left); Destory(root->_right); delete root; } private: Node* _root; };2>压缩和解压缩
#pragma once #pragma warning(disable:4996) #include<stdio.h> #include<cassert> #include<Windows.h> #include<string> #include<iostream> #include"HuffmanTree.hpp" struct weight { long long _count; //出现次数 char _ch; //字符 string _code; //编码 weight(long long count=0) :_count(count) ,_ch(0) ,_code("") {} weight operator+(const weight& w) { long long tmp = _count + w._count; return weight(tmp); } bool operator<(const weight& w) { return _count < w._count; } bool operator!=(const weight& w) { return !(_count == w._count); } }; class HuffmanPress { public: HuffmanPress() { for (int i = 0; i < 256; i++) { _infos[i]._ch = i; } } bool FilePress(const char* filename) { //1.统计各个字符出现的次数 FILE* fout = fopen(filename, "rb"); assert(fout); //ch定义成int 有可能超过127 char ch = fgetc(fout); long long charcount = 0; while (1) { if (feof(fout)) break; _infos[(unsigned char)ch]._count++; ch = fgetc(fout); charcount++; } //2.构建HuffmanTree weight invalid(0); HuffmanTree<weight> hp(_infos, 256, invalid); //GetRoot()返回的是Node* //3.获取Huffman编码 HuffmanTreeNode<weight>* root = hp.GetRoot(); string code; _GetCodeR(root, code); //4.给压缩文件中写哈夫曼编码 string Compreesfile = filename; Compreesfile += ".huffman"; FILE* fin = fopen(Compreesfile.c_str(), "wb"); assert(fin); //统计完次数 最后需要是指针指向文件开头 fseek(fout, 0, SEEK_SET); ch = fgetc(fout); int value = 0; int pos = 0; while (!feof(fout)) { string s = _infos[(unsigned char)ch]._code; for (size_t i = 0; i < s.size(); i++) { value <<= 1; if (s[i]=='1') { value |= 1; } if (++pos==8) { fputc(value, fin); value = 0; pos = 0; } } ch = fgetc(fout); } if (pos)//最后编码不足一字节 { value = value << (8 - pos); fputc(value, fin); } //配置文件 string Configfile = filename; Configfile += ".config"; FILE* fcon = fopen(Configfile.c_str(), "wb"); assert(fcon); char countstr[20];//字符出现的次数 //把这些全部用字符保存 //(source,dstin,进制) 保留了高32位 itoa(charcount >> 32, countstr, 10); fputs(countstr, fcon); fputc('\n', fcon); //与运算 和1相&是本身 保留了低32位 总结一下相关位运算 itoa(charcount & 0xffffffff, countstr,10); fputs(countstr, fcon); fputc('\n', fcon); for (int i = 0; i < 256; i++) { string s; if (_infos[i]!=invalid) { fputc(_infos[i]._ch, fcon); fputc(',', fcon); itoa(_infos[i]._count, countstr, 10); s += countstr; fputs(s.c_str(), fcon); fputc('\n', fcon); } } //这种方法会重复计算 比如有两个a 会记录两次 //while (ch!=EOF) //{ // string s; // if (_infos[ch]!=invalid) // { // fputc(ch, fcon); // fputc(',', fcon); // itoa(_infos[ch]._count, countstr, 10); // s += countstr; // fputs(s.c_str(), fcon); // fputc('\n', fcon);//一行结束 // } // ch = fgetc(fout); //} fclose(fout); fclose(fin); fclose(fcon); return true; } bool FileUncompress(char* filename) //这里给的是压缩文件名 { //文件操作 string Configfile = filename; int tmp = Configfile.rfind('.'); Configfile = Configfile.substr(0, tmp); string Uncompress = Configfile + ".uncompress"; FILE* fucon = fopen(Uncompress.c_str(), "wb");//反压缩文件 assert(fucon); //这个是读 FILE* fin = fopen(filename, "rb");//哈夫曼文件(哈夫曼编码保留的文件) assert(fin); Configfile += ".config"; FILE* fcon = fopen(Configfile.c_str(), "rb");//配置文件 出现的字符及其对应的出现次数(重建哈夫曼树) 和总字符数 assert(fcon); string s; _ReadLine(fcon, s); long long count = atoi(s.c_str());//总字数 s.clear(); _ReadLine(fcon, s); count <<= 32; count += atoi(s.c_str()); s.clear(); while (_ReadLine(fcon,s)) { if (!s.empty()) { char ch = s[0]; _infos[(unsigned char)ch]._count = atoi(s.substr(2).c_str()); s.clear(); } //这里不懂 else { s += '\n'; } } //再次构建Huffman树 weight invalid(0); HuffmanTree<weight> hp(_infos, 256, invalid); HuffmanTreeNode<weight>* root = hp.GetRoot(); HuffmanTreeNode<weight>* cur = root; char ch = fgetc(fin); int pos = 8;//pos是8 while (1) { --pos; if ((ch>>pos)&1) { cur = cur->_right; } else { cur = cur->_left; } if (cur->_left==NULL&&cur->_right==NULL) { fprintf(fucon,"%c", cur->_w._ch); //fputc(cur->_w._ch, fucon); cur = root; count--; } if (pos==0) { ch = fgetc(fin); pos = 8; } if (count==0) { break; } } fclose(fin); fclose(fcon); fclose(fucon); return true; } protected: bool _ReadLine(FILE* filename, string& line) { assert(filename); if (feof(filename)) return false; ///时刻注意这里是unsigned char unsigned char ch = fgetc(filename); while (ch != '\n') { line += ch; ch = fgetc(filename); if (feof(filename)) return false; } return true; } void _GetCodeR(HuffmanTreeNode<weight>* root,string code) { if (root==NULL) return; if (root->_left==NULL&&root->_right==NULL) { _infos[(unsigned char)root->_w._ch]._code = code; } _GetCodeR(root->_left,code + '0'); _GetCodeR(root->_right,code + '1'); } private: weight _infos[256]; }; void Test() { HuffmanPress h; h.FilePress("test.txt"); h.FileUncompress("test.txt.huffman"); } int main() { Test(); system("pause"); return 0; }Heap.h
#include<iostream> using namespace std; #include<vector> template<class T> struct Greater { bool operator()(const T& x, const T& y) { return x > y; } }; template<class T> struct Less { bool operator()(const T&x, const T& y) { return x < y; } }; template<class T, class Compare = Grater<T>> class Heap { public: Heap() {} Heap(const T* a, size_t n) { //vector(动态数组) //vector reserve() 改变Capacity 配套用push_back 建议使用这个 //vector resize() 改变size大小 a[i] = array[i] _v.reserve(n); for (size_t i = 0; i < n; i++) { _v.push_back(a[i]); } //建堆 for (int i = ((int)_v.size() - 2) / 2; i >= 0; i--) { AdjustDown(i); } //向下调正算法给的第一个就是父节点 向上给的第一个是叶子节点 //向下调正算法 假设左右都是大堆 往下换 叶子节点不用算,从最后一个非叶子节点算起(倒着算) } void Push(const T& data) { _v.push_back(data); //向上调正算法 只在push()这里用,把新填的叶子节不断向上换 直到符合大/小堆 AdjustUp(_v.size() - 1); } void Pop() { //1.先将堆顶元素与堆的最后一个元素交换 swap(_v[_v.size() - 1], _v[0]); //2.删除最后一个节点 _v.pop_back(); //3.向下调整 AdjustDown(0); } T& Top() { return _v[0]; } size_t Size() { return _v.size(); } protected: void AdjustDown(int root) { int parent = root; int child = root * 2 + 1; while (child<(int)_v.size()) { if (child + 1 < (int)_v.size() && Compare()(_v[child + 1], _v[child])) { child++; } if (Compare()(_v[child], _v[parent])) { swap(_v[parent], _v[child]); parent = child; child = parent * 2 + 1; } else { break; } } } void AdjustUp(int child) { int parent = (child - 1) / 2; //注意这里是child>0 不是parent>0 while (child>0) { if (Compare()(_v[parent], _v[child])) { break; } else { swap(_v[child], _v[parent]); child = parent; parent = (child - 1) / 2; } } } private: vector<T> _v; };