一、前言
如果你學習資料結構,就一定會學到Huffman樹,而Huffman編碼實際上上就是zip壓縮的核心部分,所以,如果已經學習了Huffman樹,為何不嘗試寫一個壓縮程式出來呢?
如果你沒有學習Huffman樹,那我們們就一起先學習一下Huffman樹吧。
二、Huffman 樹壓縮檔案
定義:Huffman樹,又稱為最優二叉樹,是加權路徑長度最短的二叉樹。
建立:
這樣建立的樹,保證所有資料成員都在葉子節點上,且數越小,離根節點越遠,越大,離根節點越近,那麼這樣的特點應用於壓縮中是很關鍵的,我們可以讓出現次數少的字元編碼長一些,次數多的字元編碼短一些。接下來我們看看壓縮的步驟吧~
1>統計要壓縮的檔案中字元出現的次數。
遍歷一遍檔案,將字元出現的次數統計在一個結構體陣列裡,陣列裡包含字元,字元出現的次數,對該字元的編碼。
2>用得到的陣列構建一個Huffman樹。
因為每次要取最小值,所以這裡考慮建立一個小堆。
3>得到Huffman編碼
怎麼得到呢?向右為1,向左為0,就是這麼簡單,我畫圖示意一下:
原本用一個char表示的字元,現在只佔了幾個位,這就是為什麼能將檔案壓縮。
4>向壓縮檔案裡寫入Huffman編碼。
寫入的時候,滿8個位寫進去,如果最後不足8個位,先補齊,解壓的時候要注意,解壓到原始檔字元數的時候停止即可。原始檔的總字元數可以在第一次遍歷統計出現的字元個數時統計,還有一種方法就是,仔細觀察Huffman樹就知道,它的根節點的大小,其實就是所有葉子節點相加的和。所以,根節點的大小就是原始檔裡所有字元出現的總次數。
至此,壓縮就結束了。
但是,怎麼解壓縮呢?解壓縮至少也得已知這樣的一顆樹才行啊,所以,我們在壓縮完成後要建立一個配置檔案。
5>建立配置檔案
配置檔案裡要儲存原始檔字元及出現的次數。有了這樣的配置檔案,就可以再次構建Huffman樹!
三、解壓縮
1>讀取配置檔案,重新構建Huffman樹
2>讀取壓縮檔案
由壓縮時的原理可知,此時讀到1指標向右移動,0向左移,到葉子節點停下,將字元還原。不停的迴圈,直到檔案結束或者總字元數變為0.這裡就能體現出,Huffman壓縮是一種無損的壓縮,如果程式碼沒有問題,它會原原本本的還原原始檔。
解壓到這裡成功。可以先使用小檔案測試,若沒有問題則找個大點的檔案,還有各類格式的檔案都拿來壓一壓測一下。
四、我遇到的問題
1>編譯時不通過,一大堆的錯誤,我找了半天!最後發現是一個很簡單的問題,我的Huffman樹使用的是C++模板實現的,模板不能分離編譯,而我在壓縮時建立Huffman樹是在另一個檔案中進行的,所以編譯不通過。
解決方法:.h字尾改成.hpp,重新包一下標頭檔案ok。
2>檔案的開啟方式。這裡開啟檔案一定要用二進位制形式,”wb”,”rb”.因為二進位制開啟和文字開啟其實是有區別的。文字方式開啟,會對‘n’進行特殊處理,那如果這個字元本身就是’n’.這就會出現問題,所以使用二進位制開啟,特點:不進行任何處理,是什麼就是什麼。
3>壓縮後解壓縮的圖片打不開,經過我反覆查詢,終於發現是配置檔案裡對‘’的處理問題,我在寫配置檔案起初是用一個string把字元和它出現的次數連線起來放進去。比如:a,3 這樣帶來的問題是 ,200 寫的時候是以c字串的形式寫的,所以遇見”就終止了,那麼在解壓縮的時候就會出問題。
解決方法:先把字元放進去,再把剩下的構建成string物件放進去。
五、原始碼
1>Huffman樹
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
#pragma once #include<iostream> #include"Heap.h" #include"Press.h" using namespace std; template<class T> struct HuffmanTreeNode { typedef HuffmanTreeNode<T> Node; T _weight; Node* _left; Node* _right; Node* _parent; HuffmanTreeNode(const T& w) :_weight(w), _left(NULL), _right(NULL), _parent(NULL) {} }; template<class T> class HuffmanTree { public: typedef HuffmanTreeNode<T> Node; HuffmanTree() :_root(NULL) {} ~HuffmanTree() { _destory(_root); } Node* GetRoot() { return _root; } template<class T> struct Less { bool operator()(const T& left, const T&right) { return left->_weight < right->_weight; } }; HuffmanTree(T* a, int size,T invalid) //構建Huffman樹 { Heap<Node* , Less<Node*>> hp; //建小堆 for (int i = 0; i<size; i++) { if (a[i] != invalid) { Node* tmp = new Node(a[i]); hp.Push(tmp); } } while (hp.Size()>1) { Node* left = hp.Top(); hp.Pop(); Node* right = hp.Top(); hp.Pop(); Node* parent = new Node(left->_weight + right->_weight); hp.Push(parent); parent->_left = left; parent->_right = right; left->_parent = parent; right->_parent = parent; } _root = hp.Top(); } protected: void _destory(Node* root) { if (NULL == root) return; _destory(root->_left); _destory(root->_right); delete root; } private: Node* _root; }; |
2>壓縮和解壓縮
最後,檔案大了之後怎麼對比兩個檔案是否一致呢?我用的是beyond Compare這個軟體,很方便,能對比各種型別的檔案。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 |
#pragma once #pragma warning(disable:4996) #include<cassert> #include<Windows.h> #include<string> #include<iostream> #include"Huffman.hpp" typedef long long type; struct weight //權值裡應該包含字元出現的次數以及對應的字元和Huffman編碼 { unsigned char _ch; type _count; string _code; weight(type count = 0) : _ch(0) ,_count(count) , _code("") {} weight operator+(const weight& w) { type 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 = (unsigned char)i; } } bool FilePress(const char* filename) { //統計出每個字元出現的次數。 FILE* fOut = fopen(filename, "rb"); assert(fOut); int ch = fgetc(fOut); type charcount = 0; //統計出字元出現的總次數 while (ch != EOF) { if (feof(fOut)) break; _infos[ch]._count++; ch = fgetc(fOut); charcount++; } weight invalid(0); HuffmanTree<weight> hf(_infos, 256,invalid); //用得到的權重陣列構建一個Huffman樹 HuffmanTreeNode<weight>* root = hf.GetRoot(); //得到Huffman編碼 string code; _GetCodeR(root, code); //開始壓縮,建立壓縮後的檔案 string CompressFilename = filename; CompressFilename += ".huffman"; FILE* fIn = fopen(CompressFilename.c_str(), "wb"); assert(fIn); //統計完次數使得檔案指標指向了最後,所以需要使指標指向檔案頭 fseek(fOut, 0, SEEK_SET); //向壓縮檔案裡寫入Huffman編碼 int pos = 0; char value = 0; int ch1 = fgetc(fOut); while (ch1 != EOF) { if (feof(fOut)) break; string& code = _infos[ch1]._code; for (size_t i = 0; i < code.size(); i++) { value <<= 1; if (code[i] == '1') //得到二進位制的1 { value |= 1; } if (++pos == 8) //滿8位寫入檔案 { fputc(value, fIn); value = 0; pos = 0; } } ch1 = fgetc(fOut); } if (pos) //最後的編碼不滿足一個位元組 { value =value<<(8 - pos); fputc(value, fIn); } //將字元和字元出現的次數寫進配置檔案,檔案解壓時會用到 string ConfigFilename = filename; ConfigFilename += ".config"; FILE* fConfig = fopen(ConfigFilename.c_str(), "wb"); assert(fConfig); char countStr[20]; //字元出現的次數 //先把所有字元出現的總次數寫進配置檔案,為防止超過int範圍,charcount使用的是long long 所以要分兩步寫入 itoa(charcount >> 32, countStr, 10); //轉換高位 fputs(countStr, fConfig); //寫入 fputc('\n', fConfig); itoa(charcount & 0Xffffffff, countStr, 10); //轉換低位 fputs(countStr, fConfig); //寫入 fputc('\n', fConfig); for (int i = 0; i < 256; i++) { string put; if (_infos[i]!=invalid) { fputc(_infos[i]._ch,fConfig);//必須先把ch放進去,如果把ch作為string的字元最後轉換為C的字元,會導致'\0'沒有處理 put.push_back(','); itoa(_infos[i]._count, countStr, 10); put += countStr; fputs(put.c_str(), fConfig); fputc('\n', fConfig); } } fclose(fOut); fclose(fIn); fclose(fConfig); return true; } bool FileUncompress(char* filename) //這裡給的是壓縮檔名 { //1.讀取配置檔案 string ConfigFilename = filename; int count = ConfigFilename.rfind('.'); ConfigFilename = ConfigFilename.substr(0, count); string UnCompressname = ConfigFilename + ".unpress"; FILE* fUnCompress = fopen(UnCompressname.c_str(), "wb"); //建立解壓縮檔案 ConfigFilename += ".config"; FILE* fconfig = fopen(ConfigFilename.c_str(),"rb"); assert(fconfig); assert(fUnCompress); FILE* fpress = fopen(filename, "rb"); //開啟壓縮好的檔案 assert(fpress); type charcount = 0; //先讀出字元出現的總次數 string line; _ReadLine(fconfig,line); charcount = atoi(line.c_str()); charcount <<= 32; line.clear(); _ReadLine(fconfig, line); charcount += atoi(line.c_str()); line.clear(); while (_ReadLine(fconfig,line)) //檔案結束時feof會返回0 { if (!line.empty()) { char ch = line[0]; _infos[(unsigned char)ch]._count = atoi(line.substr(2).c_str()); line.clear(); } else //若讀到一個空行,對應的字元為換行符 { line += '\n'; } } //2.再次構建Huffman樹 weight invalid(0); HuffmanTree<weight> hf(_infos, 256, invalid); //用得到的權重陣列構建一個Huffman樹 HuffmanTreeNode<weight>* root = hf.GetRoot(); HuffmanTreeNode<weight>* cur = root; char ch = fgetc(fpress); int pos = 8; while (1) { --pos; if ((ch >> pos) & 1) { cur = cur->_right; } else { cur = cur->_left; } if (cur->_left == NULL&&cur->_right == NULL) { fputc(cur->_weight._ch, fUnCompress); cur = root; //再次從根節點遍歷 charcount--; } if (pos == 0) { ch = fgetc(fpress); pos = 8; } if (charcount == 0) //不讀取壓縮時為了湊夠一個位元組而加進去的位元位 break; } fclose(fconfig); fclose(fpress); fclose(fUnCompress); return true; } protected: bool _ReadLine(FILE* filename,string& line) { assert(filename); if (feof(filename)) return false; unsigned char ch = fgetc(filename); while (ch != '\n') { line += ch; ch = fgetc(filename); if (feof(filename)) //break; return false; } return true; } void _GetCodeR(HuffmanTreeNode<weight>* root, string code) { if (NULL == root) return; if (root->_left == NULL&& root->_right == NULL) { _infos[root->_weight._ch]._code = code; } _GetCodeR(root->_left, code + '0'); _GetCodeR(root->_right, code + '1'); } private: weight _infos[256]; }; void TestCompress() { HuffmanPress hft; int begin = GetTickCount(); // hft.FilePress("test1.txt"); //hft.FilePress("git.txt"); // hft.FilePress("1.jpg"); // hft.FilePress("8.pdf"); //hft.FilePress("Input.BIG"); hft.FilePress("listen.mp3"); int end = GetTickCount(); cout << end-begin << endl; } void TestUnCompress() { HuffmanPress hf; int begin = GetTickCount(); // hf.FileUncompress("test1.txt.huffman"); // hf.FileUncompress("1.jpg.huffman"); // hf.FileUncompress("git.txt.huffman"); // hf.FileUncompress("8.pdf.huffman"); //hf.FileUncompress("Input.BIG.huffman"); hf.FileUncompress("listen.mp3.huffman"); int end = GetTickCount(); cout << end - begin << endl; } |