文件压缩 —— Huffman
我用自己实现的堆和Huffman树,开始着手对文件里面的内容进行一系列操作
压缩原理:由huffman的贪心决定
创建一个文件input.txt,内容为aaaabbbccd
我们已经得到了字符串中abcd的huffman编码了,它们分别是: '0' '11' '101' '100',由于这里的huffman编码都是01组成的,所以我们很容易想到按位存储,这个时候这个字符串的huffman编码形式就是0000111111101101100,我们将它按位写进文件中,这个文件占用19个比特位,所以占用3个字节.
而原文件中有10个字符他们都是char类型,所以占用10个字节. 这就是huffman压缩算法的基本思想.
所以出现的越频繁次数越多的字符它的huffman编码越短,占用的空间越少. 出现较为少的字符他们的huffman的编码虽然长,但是出现的次数少.并且我们的huffman编码是按位存储的,所以这里的压缩效率就显现出来了.
huffman压缩算法压缩率高的情况就是 出现特别多的重复字符,只有极少数的是别的字符(比如 abbbbbbbbbbbbbbbb).
效率低的情况下就是每个字符出现的次数都差不多很平均.(比如abcdabcd)
现在项目的思路渐渐地清晰了起来了:
1.统计:首先读取一个文件,统计出256个字符中各个字符出现的次数.
2.建树:按照字符出现的次数,并以次数作为权值遍历哈夫曼编码树,然后产生各个字符的huffman编码.
3.压缩:再次读取文件,按照该字符对应的编码写到新的编码文件当中.
4.加工:将文件的长度,文件中各个字符以及他们的出现次数,写进编码文件中.
5.解压:利用压缩文件和配置文件恢复出原文件.
6.测试:观察解压出的文件和原文件是否相同,再通过Beyond Compare 4软件进行对比,验证程序的正确性.
首先我们先建堆
template<class T> struct Less { bool operator()(const T& l, const T& r) { return l < r; } }; template<class T> struct Greater { bool operator()(const T& l, const T& r) { return l > r; } }; template<class T, class Compare>//编写与类型无关的代码 class Heap { public: Heap() {} Heap(T* a, size_t n) { _a.reserve(n); //reserve()只开空间 resize()开空间并初始化 for (size_t i = 0; i < n; i++) { _a.push_back(a[i]); } //建堆 for (int i = (_a.size() - 2) / 2; i >= 0; i--) { AdjustDown(i);//从第一个非叶子节点开始 } } void Push(const T& x) { _a.push_back(x); AdjustUp(_a.size() - 1); } void Pop() { swap(_a[0], _a[_a.size() - 1]); _a.pop_back(); AdjustDown(0); } bool Empty() { return _a.empty(); } size_t Size() { return _a.size(); } const T& Top() { return _a[0]; } void Print() { for (size_t i = 0; i < _a.size(); i++) { cout << _a[i] << " "; } } protected: void AdjustDown(size_t root)//向下调整算法 { Compare com; size_t parent = root; size_t child = parent * 2 + 1; while (child < _a.size()) { if (child + 1 < _a.size() && com(_a[child + 1], _a[child])) //if ((child + 1 < _a.size()) && (_a[child + 1] > _a[child])) { child++; //只需得到大的(小的)孩子的下标即可 } if (com(_a[child], _a[parent])) //if (_a[child] > _a[parent]) { swap(_a[child], _a[parent]); parent = child; //子问题 child = parent * 2 + 1; } else { break; } } } void AdjustUp(size_t child)//向上调整算法 { Compare com; size_t parent = (child - 1) / 2; while (child > 0) { if (com(_a[child], _a[parent])) //if (_a[child] > _a[parent]) { swap(_a[child], _a[parent]); child = parent; parent = (child - 1) / 2; } else { break; } } } private: //T* _a; //size_t _size; //size_t _capacity; vector<T> _a; };
构建哈夫曼树
//构造哈夫曼树结点的结构体 template<class W> struct HuffmanTreeNode { HuffmanTreeNode<W>* _left; HuffmanTreeNode<W>* _right; W _weight; //权值 HuffmanTreeNode(const W& weight) :_left(NULL) , _right(NULL) , _weight(weight) {} }; template<class W> class HuffmanTree { typedef HuffmanTreeNode<W> Node; public: HuffmanTree() :_root(NULL) {} //W-> CharInfo HuffmanTree(W* w, size_t n,const W& invalid) { struct PNodeCompare { bool operator()(Node* l, Node* r) { return l->_weight < r->_weight; } }; //构建Huffman树 Heap<Node*,PNodeCompare> 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->_weight + right->_weight); parent->_left = left; parent->_right = right; minheap.Push(parent); } _root = minheap.Top(); } ~HuffmanTree() { Destory(_root); _root = NULL; } void Destory(Node* root) { if (root == NULL) return; Destory(root->_left); Destory(root->_right); delete root; } Node* GetRoot() { return _root; } private: HuffmanTree(const HuffmanTree<W>&); HuffmanTree<W>& operator=(const HuffmanTree<W>&); protected: Node* _root; };
压缩和解压缩
typedef long long LongType; //要用到的自定义类型,存放所有字符,字符出现的次数,还有它的Huffman编码 struct CharInfo { char _ch; //字符 LongType _count; //出现次数 string _code; //Huffman Code bool operator != (const CharInfo& info) { return _count != info._count; } CharInfo operator+(const CharInfo& info) { CharInfo ret; ret._count = _count + info._count; return ret; } bool operator<(const CharInfo& info) { return _count < info._count; } }; class FileComparess { typedef HuffmanTreeNode<CharInfo> Node; struct TmpInfo { char _ch; LongType _count; }; public: FileComparess() { for (size_t i = 0; i < 256; i++) { _infos[i]._ch = i; _infos[i]._count = 0; } } void GenerateHuffmanCode(Node* root, string code) { if (root == NULL) return; if (root->_left == NULL && root->_right == NULL) { _infos[(unsigned char)root->_weight._ch]._code = code; return; } GenerateHuffmanCode(root->_left, code + '0'); GenerateHuffmanCode(root->_right, code + '1'); } void Compress(const char* file) //压缩文件 { //1.统计字符出现的次数 FILE* fout = fopen(file, "rb"); //二进制文件 assert(fout); char ch = fgetc(fout); while (!feof(fout)) { //_infos[]是一个CharInfo类型的数组,一共有256个元素,每一个元素都维护一个字符 _infos[(unsigned char)ch]._count++; ch = fgetc(fout); } //2.构建Huffman Tree CharInfo invalid; invalid._count = 0; HuffmanTree<CharInfo> tree(_infos, 256, invalid); //生成Huffman编码 string code; GenerateHuffmanCode(tree.GetRoot(), code); //开始压缩文件 string compressfile = file; compressfile += ".huffman"; FILE* fin = fopen(compressfile.c_str(), "wb");//c_str()的用法就是返回一个正规的C字符串指针,内容和string相等 assert(fin); //写入字符出现的信息 //fwrite(_infos, sizeof(CharInfo), 256, fin); for (size_t i = 0; i < 256; i++) { if (_infos[i]._count > 0) { TmpInfo info; info._ch = _infos[i]._ch; info._count = _infos[i]._count; fwrite(&info, sizeof(TmpInfo), 1, fin); } } TmpInfo info; info._count = -1; fwrite(&info, sizeof(TmpInfo), 1, fin); //3.压缩 fseek(fout, 0, SEEK_SET); ch = fgetc(fout); char value = 0; size_t pos = 0; while (!feof(fout)) { string& code = _infos[(unsigned char)ch]._code; for (size_t i = 0; i < code.size(); i++) { if (code[i] == '1') value |= (1 << pos); else if (code[i] == '0') value &= ~(1 << pos); else assert(false); ++pos; if (pos == 8)//满8位就往里写 { fputc(value, fin); value = 0; pos = 0; } } ch = fgetc(fout); } if (pos > 0) { fputc(value, fin); } fclose(fout); fclose(fin); } //解压缩 void Uncompress(const char* file) { string uncompressfile = file; size_t pos = uncompressfile.rfind('.'); assert(pos != string::npos); uncompressfile.erase(pos); uncompressfile += ".unhuffman"; FILE* fin = fopen(uncompressfile.c_str(), "wb"); assert(fin); FILE* fout = fopen(file,"rb"); assert(fout); //fread(_infos, sizeof(CharInfo), 256, fout); TmpInfo info; fread(&info, sizeof(TmpInfo), 1, fout); while (info._count != -1) { _infos[(unsigned char)info._ch]._ch = info._ch; _infos[(unsigned char)info._ch]._count = info._count; fread(&info, sizeof(TmpInfo), 1, fout); } //重建Huffman树 CharInfo invalid; invalid._count = 0; HuffmanTree<CharInfo> tree(_infos, 256,invalid); Node* root = tree.GetRoot(); Node* cur = root; LongType n = root->_weight._count; char ch = fgetc(fout); while (!feof(fout)) { for (size_t i = 0; i < 8; i++) { if ((ch & (1 << i)) == 0) cur = cur->_left; else cur = cur->_right; if (cur->_left == NULL && cur->_right == NULL) { fputc(cur->_weight._ch, fin); cur = root; if (--n == 0) { break; } } } ch = fgetc(fout); } fclose(fin); fclose(fout); } protected: CharInfo _infos[256]; };
(1)刚开始写的时候测试发现如果待压缩文件中出现了中文,程序就会崩溃,将错误定位到构建哈夫曼编码的函数处,最后发现是数组越界的错误,
因为如果只是字符,它的范围是-128~127,程序中使用char类型为数组下标(0~127),所以字符没有问题. 但是汉字的编码是两个字节,所以可能会
出现越界,解决方法就是将char类型强转unsigned char,可表示范围为0~255.
(2)解压缩文件生成后大部分的内容丢失了,这个是因为
程序读到了EOF(文件结束标志),
EOF通常用来判断文本文件的结尾,因为EOF的值为-1,ASCII都是字符型,不可能出现-1的情况。而在二进制文件中,信息以数值存放,使用EOF就可能会异常。
解决方案: 使用feof()函数,feof函数既可用以判断二进制文件又可用以判断文本文件。
使用方法是 feof(fp),fp为指向需要判断的文件的指针。如果不到文件结尾,返回0值;如果是文件结尾,返回非0.