距離上次寫完哈夫曼編碼已經過去一周了,這一周都在寫huffman壓縮解壓,哎,在很多小錯誤上浪費了很多時間調bug。其實這個程序的最關鍵部分不是我自己想的,而是借鑒了某位園友的代碼,但是,無論如何,自己也是思考,學習,調試了很久,慢慢地清除了一個一個bug。一周的課后時間都花在這上面了,學習了一點東西,對文件的操作也了解了不少,也算有了一點心得吧。下面一一說來吧--
這個程序有三個最關鍵的點,一個是上次寫的哈夫曼編碼,那個是基礎,另外一個就是位操作,這個是最關鍵的,具體來講位操作就是如何把一個01字符串變成內存中實際存放的0和1, 下面還會對這里做出更多解釋,還有一個是對文件的操作。
整個程序以二進制的形式讀寫文件,不是以文本形式讀寫,因為所有文件在計算機中都是以二進制的形式存放的,以這種最根本的形式能夠達到對所有文件操作的目的,當然,文件夾是不可以的,目前對任何形式的單個文件都可以壓縮和解壓,但是壓縮率和文件本身密切相關,不同文件壓縮率可能相差很大,甚至某些文件壓縮完比原文件還大,這並不是bug,而是在我的這種方法下理論上就可能出現這種情況。另外,該程序只是為了以最簡單的方式來實現哈夫曼壓縮,所以沒仔細考慮時間復雜度,可能壓縮某些比較大的視頻文件或其他文件需要很長的時間,原因是以二進制的形式讀寫文件本身就要花費很多時間。想想一個100M的文件都有100 * 1024 *1024 字節,每次讀取一個字節,都要上億次,還要考慮構建哈夫曼樹,得到哈夫曼編碼,寫入文件等等,時間復雜度自然會很高了。這也是我第一次通過自己的實踐這么直觀地認識到時間復雜度的重要性,也就是算法的重要性。想想我們平時在電腦上用的那些壓縮軟件,壓縮效率是那么高,還特別快,人家肯定用到了很好的算法。
之前我在看C++Primer的時候對流這一章沒怎么去看,所以一開始就不知道怎么以二進制的形式操作文件,這里還是要對文件流fstream比較了解才能寫這個程序,特別是ifstream的 read()函數以及ofstream的write()函數,怎么去理解fout.write((char *)(&obj), sizeof(obj))中的char*這種強制類型轉換呢?這玩意我想了很久,網上看了挺多文章的,沒看到有人說過這個,按我的理解,應該就是把任何數據轉換成它們在內存中的表示(0和1),然后以字節為單位,把每8位轉換成256個ASCII碼中的一個字符,然后再寫入文件,以這種方式來達到用二進制的形式把任何一種數據寫入文件或者從文件讀取( fin.read((char *)(&obj), sizeof(obj)) ),然后注意這里的第一個參數必須是引用。還有一個在操作文件流容易錯的地方就是操作完記得關閉這個流,斷開和相應文件的連接,防止下次另外一個流和同一個文件關聯,你不可能對一個文件同時進行讀和寫的操作。另外對文件流指針,以及二進制文件結束標志都要清楚,很多錯誤都源於對這些細節不清楚。比如以二進制形式讀取時,總發現最后多讀了一點東西,仔細調試一下,發現多讀的這個東西輸出是兩個連在一起的空格,查看ASCII發現是十進制255對應的那個字符,八進制為'\377',叫做nbsp,全稱是Non-breaking space or no-break space(http://www.theasciicode.com.ar/),在查詢相關知識得知它就是任何二進制文件的結束標志,文件流指針讀到這個字符時就知道這個文件結束了,這樣一來似乎就豁然開朗了,還有得明白文件指針的移動原理,文件指針時刻這個下一個將被操作的字符,每次以字節為單位移動,所以在用到。fin.get()函數時,每次調用ifstream的這個成員函數時,文件指針就向后移動一個字節,所以我們以二進制形式讀取一個文件的每一個字符應該這樣讀
1 while (1) 2 { 3 c = fin.get();//每次以二進制的形式讀取八位,並把對應的字符存入c中去 4 if (fin.eof()) break;//讀到二進制文件末尾,不再讀取 5 freqs[c]++; 6 file_len++; //記錄文件的長度,即文件包含多少字節 7 }
而不是
while (!fin.eof()) { c = fin.get();//每次以二進制的形式讀取八位,並把對應的字符存入c中去 freqs[c]++; file_len++; //記錄文件的長度,即文件包含多少字節 }
或者這樣也不行
while (1) { if (fin.eof()) break;//讀到二進制文件末尾,不再讀取 freqs[fin.get()]++; file_len++; //記錄文件的長度,即文件包含多少字節 }
想想文件流指針是在執行完fin.get()就移動一次,就能明白了,我們不能把文件的結束標識符也讀取進來,另外我們寫入的時候也不用自己寫入那個結束標識符,因為當ofstream流對象在和它之前的關聯的文件斷開時就會自動寫入一個文件結束標志符。
另外容易犯的一個錯誤是,由於我每次是以字節為單位按字符的形式讀取文件的,這其實就是以二進制操作文件的本質,每次處理8位正好對應256個ASCII碼中的一個字符,但是注意這些字符都是unsigned char,也就是無符號的,因為我把字符當成數組下標處理,所以當我最開始讀取文件時,每次讀取一個字符,添加到一個string,然后直接利用這個得到的string直接調用我上次寫的那個
int Create_freq_array(unsigned int (&freqs)[NUM_CHARS],string s, int &char_size)
函數,結果無形中就埋下了一個錯誤,注意,string中的字符都是char型,是有符號的!當你把一個unsigned char寫入string中,也就是如下
while (1) { c = fin.get();//每次以二進制的形式讀取八位,並把對應的字符存入c中去 if (fin.eof()) break;//讀到二進制文件末尾,不再讀取 s += c;//string s; file_len++; //記錄文件的長度,即文件包含多少字節 }
這樣雖然能利用string記錄下原文件讀取的字符序列,但是當你調用
int Create_freq_array(unsigned int (&freqs)[NUM_CHARS],string s, int &char_size)//傳入數組的引用, { int i, maxfreq = 0; for(int i=0;i<NUM_CHARS;i++) freqs[i] = 0;//注意傳入的數組的各元素先賦值為0 for(auto iter =s.begin(); iter!=s.end(); iter++) { freqs[*iter]++; //*iter為char型,這里轉換成了int型,即以某個字符的ASCII碼作為 if(freqs[*iter] > maxfreq)//它在freq數組中的下標,注意這種方式不能表示非ASCII碼字符! maxfreq = freqs[*iter];//每次記得更新maxfreq的值 } for(i=0; i<NUM_CHARS; i++)//計算char_size值 { if(freqs[i]) { char_size++; } } return 0; }
假如從文件中讀取的某個字符是ASCII碼表中的后128個,比如是ASCII為130的那個(10000010,對應字符為 é )那么問題來了,當你寫入string中沒問題,string能裝下這個字符,畢竟string里可以存漢字,也就是說string里是signed char ,那么當你執行紅色上面紅色標注的那個語句時,你把上面這個字符當成signed char轉換成int型的數組下標,那些顯然下標是負的,越界了!這個錯誤我找了很久才找出來,真的一開始沒考慮這么多,解決辦法為用unsigned char數組來保存從文件中讀取的字符,所以改寫了代碼,沒有利用上面寫的那個函數。
還有一個錯誤花了我兩天的時間才發現,那就是上一篇最開始定義了一個常量
#define MAX_FREQ 10000 //最大頻率必須小於這個數
這次壓根沒管這個數,結果在壓縮解壓函數上白浪費了很多時間尋找錯誤,尾后無意中發現竟然是這個數太小了
發現原因是在某次對某個文件試驗調試時注意到這個
這個是
int Build_Huffman_tree(unsigned int (&freqs)[NUM_CHARS],HuffNode (&Huffman_tree_array)[MAX_SIZE],unsigned int n)
中構建完的Huffman_tree_array數組中的最后一個元素,它的左右孩子竟然都是零,我曹,這他么不對呀!依據這個我找到了我尋找兩天未果的錯誤
修改代碼
#define MAX_FREQ 100000000 //最大頻率必須小於這個數
這下總沒事了
其實還有很多小錯誤,比如數組沒有初始化,某個變量沒有初始化。。。還是年輕
說完遇到的這么多錯誤,接下來放代碼了,來仔細談談如何實現
------------------------------------------------------------ 代碼區 -----------------------------------------------------------------------
1 // huffman.cpp : 定義控制台應用程序的入口點。 2 // 3 4 #include "stdafx.h" 5 6 #include <malloc.h> 7 #include <windows.h> 8 #include <string> 9 10 #include "huffman_code.cpp" 11 12 13 14 typedef struct { 15 unsigned char uch; // 以8bits為單元的無符號字符 16 unsigned long freq; // 每類(以二進制編碼區分)字符出現頻度 17 }TmpNode; 18 19 int Huffman_compress() 20 { 21 string str_read, str_write;//讀取和寫入的文件名 22 string str_file_name,file_name;//讀取的文件的拓展名 23 string buf;// 待存編碼緩沖區 24 int flag = 0; 25 unsigned char x; 26 unsigned char char_temp; 27 unsigned char c; 28 unsigned int char_size = 0;//用以保存string中所包含的字符種類 29 int k = 0; 30 float compress_rate; 31 unsigned long file_len = 0; 32 unsigned long file_len2 = 0; 33 unsigned int freqs[NUM_CHARS]; 34 HuffNode Huffman_tree_array[MAX_SIZE]; 35 HuffCode Huffman_code_array[NUM_CHARS]; 36 37 cout << "請輸入你所需要壓縮的文件名:" << endl; 38 cin >> str_read; 39 ifstream fin(str_read.c_str(), ios::binary);//從str_read中讀取輸入數據 40 41 //尋找讀取的文件的拓展名 42 file_name = str_read; 43 reverse(file_name.begin(), file_name.end()); 44 for (auto iter = file_name.begin(); iter != file_name.end(); iter++) 45 { 46 if (*iter == '.') 47 { 48 flag = 1; 49 break; 50 } 51 str_file_name += *iter; 52 } 53 if (flag)//flag為1表示找到了windows系統下的文件拓展名標識符".",也就是說,讀取的文件存在拓展名,反轉str_file_name 54 reverse(str_file_name.begin(), str_file_name.end()); 55 else 56 str_file_name.clear();//讀取的文件不存在拓展名,清空str_file_name 57 if (!fin) 58 { 59 cout << "讀取文件" << str_read << "失敗!" << endl << endl; 60 return 0; 61 } 62 else cout << "請輸入壓縮后的文件名:" << endl; 63 cin >> str_write; 64 cout << endl; 65 ofstream fout(str_write.c_str(), ios::binary);//向str_write中寫入數據 66 67 cout << "壓縮中..." << endl; 68 for (int i = 0; i<NUM_CHARS; i++) 69 freqs[i] = 0;//注意傳入的數組的各元素先賦值為0 70 while (1) 71 { 72 c = fin.get();//每次以二進制的形式讀取八位,並把對應的字符存入c中去 73 if (fin.eof()) break;//讀到二進制文件末尾,不再讀取 74 freqs[c]++; 75 file_len++; //記錄文件的長度,即文件包含多少字節 76 } 77 fin.close(); 78 for (int i = 0; i<NUM_CHARS; i++)//計算char_size值 79 { 80 if (freqs[i]) 81 { 82 char_size++; 83 } 84 } 85 TmpNode *tmp_nodes = (TmpNode *)malloc(char_size * sizeof(TmpNode));//分配一個實際出現字符種類大小的數組,保存實際出現的字符 86 for (int i = 0; i < 256; ++i) 87 { 88 if (freqs[i]) 89 { 90 tmp_nodes[k].uch = i; 91 tmp_nodes[k].freq = freqs[i]; 92 k++; 93 } 94 } 95 96 //把拓展名信息寫入輸出文件 97 x = str_file_name.size();//用一個字節存儲文件拓展名的長度,所以要先轉換成unsigned char型 98 fout.write((char*)&x, sizeof(unsigned char)); 99 if (str_file_name.size()) 100 { 101 for (auto iter = str_file_name.begin(); iter != str_file_name.end(); iter++) 102 { 103 x = *iter; 104 fout.write((char*)&x, sizeof(unsigned char));//把出現的唯一字符寫入壓縮文件 105 } 106 } 107 108 if (char_size == 1)//只有一種字符 109 { 110 fout.write((char*)&char_size, sizeof(unsigned int));//把出現的字符種類寫入壓縮文件 111 fout.write((char*)&tmp_nodes[0].uch, sizeof(unsigned char));//把出現的唯一字符寫入壓縮文件 112 fout.write((char*)&tmp_nodes[0].freq, sizeof(unsigned long));//把出現的唯一字符的頻率寫入壓縮文件 113 free(tmp_nodes); 114 } 115 else 116 { 117 Build_Huffman_tree(freqs, Huffman_tree_array, char_size); 118 Huffman_code(Huffman_tree_array, Huffman_code_array, char_size); 119 fout.write((char*)&char_size, sizeof(unsigned int));//把出現的字符種類寫入壓縮文件 120 for (int i = 0; i<char_size; i++) 121 { 122 fout.write((char*)&tmp_nodes[i].uch, sizeof(unsigned char));//把出現的字符寫入壓縮文件 123 fout.write((char*)&tmp_nodes[i].freq, sizeof(unsigned long));//把出現的字符的頻率寫入壓縮文件 124 } 125 fout.write((char *)&file_len, sizeof(unsigned long)); // 寫入文件長度 126 free(tmp_nodes); 127 ifstream fin2(str_read.c_str(), ios::binary);//從input.txt中再次讀取輸入數據 128 while (1) 129 { 130 c = fin2.get(); 131 if (fin2.eof()) break; 132 char_temp = c;// 每次讀取8bits,作為一個字符 133 for (int i = 0; i<char_size; i++) 134 { 135 if (char_temp == Huffman_code_array[i].data) 136 { 137 buf += Huffman_code_array[i].s; 138 break;//匹配上了就跳出循環 139 } 140 } 141 while (buf.size() >= 8) 142 { 143 char_temp = '\0'; // 清空字符暫存空間,改為暫存字符對應編碼 144 for (auto iter = buf.begin(); iter<buf.begin() + 8; iter++) 145 { 146 char_temp <<= 1; // 左移一位,為下一個bit騰出位置 147 if (*iter == '1') 148 char_temp |= 1; // 當編碼為"1",通過 位或 操作符將其添加到字節的最低位 149 } 150 fout.write((char *)&char_temp, sizeof(unsigned char)); // 將字節對應編碼存入文件 151 buf = buf.substr(8);// 編碼緩存去除已處理的前八位 152 } 153 } 154 // 處理最后不足8bits編碼 155 if (buf.size() > 0) 156 { 157 char_temp = '\0'; 158 for (auto iter = buf.begin(); iter != buf.end(); iter++) 159 { 160 char_temp <<= 1; 161 if (*iter == '1') 162 char_temp |= 1; 163 } 164 char_temp <<= 8 - buf.size(); // 將編碼字段從尾部移到字節的高位 165 fout.write((char *)&char_temp, sizeof(unsigned char)); // 存入最后一個字節 166 } 167 fin2.close(); 168 } 169 fout.close(); 170 ifstream fin3(str_write.c_str(), ios::binary); 171 while (1) 172 { 173 c = fin3.get();//每次以二進制的形式讀取八位,並把對應的字符存入c中去 174 if (fin3.eof()) break;//讀到二進制文件末尾,不再讀取 175 file_len2++; //記錄文件的長度,即文件包含多少字節 176 } 177 fin3.close(); 178 compress_rate = float(file_len2) / float(file_len); 179 cout << "壓縮完成!" << endl << endl; 180 cout << "壓縮之前的文件大小為:" << file_len << "字節" << endl; 181 cout << "壓縮之后的文件大小為:" << file_len2 << "字節" << endl; 182 cout << "壓縮率為:" << compress_rate << endl << endl; 183 return 0; 184 } 185 186 int Huffman_uncompress() 187 { 188 string str_read, str_write;//讀取和寫入的文件名 189 string str_file_name; //待解壓的文件的拓展名 190 unsigned int char_size,root; 191 unsigned char code_temp; 192 unsigned char x; 193 int k; 194 unsigned long file_len; 195 unsigned long writen_len = 0; 196 unsigned int freqs[NUM_CHARS]; 197 HuffNode Huffman_tree_array[MAX_SIZE]; 198 cout << "請輸入你所需要解壓的文件名:" << endl; 199 cin >> str_read; 200 ifstream fin(str_read.c_str(), ios::binary);//從str_read中讀取輸入數據 201 if (!fin) 202 { 203 cout << "讀取文件" << str_read << "失敗!" << endl << endl; 204 return 0; 205 } 206 else cout << "請輸入你解壓后的文件名:" << endl; 207 cin >> str_write; 208 cout << endl; 209 cout << "解壓中..." << endl; 210 211 //讀取文件拓展名信息 212 fin.read((char*)&x, sizeof(unsigned char)); 213 k = x; 214 if (k) 215 { 216 for (int i = 0; i < k; i++) 217 { 218 fin.read((char*)&x, sizeof(unsigned char)); 219 str_file_name += x; 220 } 221 } 222 223 str_write += '.';//把拓展名信息寫入輸出文件名 224 str_write += str_file_name; 225 ofstream fout(str_write.c_str(), ios::binary);//向str_write中寫入數據 226 fin.read((char*)&char_size, sizeof(unsigned int));//讀取字符種類 227 if (char_size == 1) 228 { 229 fin.read((char *)&code_temp, sizeof(unsigned char));// 讀取唯一的字符 230 fin.read((char *)&file_len, sizeof(unsigned long)); // 讀取文件長度 231 while (file_len--) 232 fout.write((char *)&code_temp, sizeof(unsigned char)); 233 } 234 else 235 { 236 for (int i = 0; i<NUM_CHARS; i++) 237 freqs[i] = 0;//注意傳入的數組的各元素先賦值為0 238 for (int i = 0; i<char_size; i++) 239 { 240 fin.read((char*)&code_temp, sizeof(unsigned char));//把當前字符從壓縮文件中讀取出來 241 fin.read((char*)&file_len, sizeof(unsigned long));//把當前字符的頻率從壓縮文件中讀取出來 242 freqs[code_temp] = file_len; 243 } 244 Build_Huffman_tree(freqs, Huffman_tree_array, char_size);//根據壓縮文件頭一段信息重建哈夫曼樹 245 root = 2 * char_size - 2;//根結點在樹數組中的下標 246 fin.read((char *)&file_len, sizeof(unsigned long)); // 讀入文件長度 247 248 while (1) 249 { 250 fin.read((char *)&code_temp, sizeof(unsigned char));// 讀取一個字符長度的編碼 251 252 // 處理讀取的一個字符長度的編碼(通常為8位) 253 for (int i = 0; i < 8; ++i) 254 { 255 // 由根向下直至葉節點正向匹配編碼對應字符 256 if (code_temp & 128)//按位與,是壓縮時按位或的逆過程,code_temp字符對應的二進制的最高位是1,往右孩子方向走 257 root = Huffman_tree_array[root].rchild; 258 else 259 root = Huffman_tree_array[root].lchild; 260 261 if (root < char_size)//到達葉子結點 262 { 263 fout.write((char *)&Huffman_tree_array[root].data, sizeof(unsigned char)); 264 ++writen_len;//記錄讀取的文件長度 265 if (writen_len == file_len) break; // 控制文件長度,跳出內層循環 266 root = 2 * char_size - 2; // 復位為根索引,匹配下一個字符 267 } 268 code_temp <<= 1; // 將編碼緩存的下一位移到最高位,供匹配 269 } 270 if (writen_len == file_len) break; // 控制文件長度,跳出外層循環 271 } 272 } 273 fin.close(); 274 fout.close(); 275 cout << "解壓完成!" << endl << endl; 276 return 0; 277 } 278 279 int main() 280 { 281 int a; 282 while (1) 283 { 284 cout << "請選擇你的操作:" << endl; 285 cout << "1 壓縮文件" << endl; 286 cout << "2 解壓文件" << endl; 287 cout << "3 退出程序" << endl; 288 cout << "4 清理屏幕" << endl; 289 cin >> a; 290 cout << endl; 291 switch (a) 292 { 293 case 1: Huffman_compress(); break; 294 case 2: Huffman_uncompress(); break; 295 case 3: exit(0); 296 case 4: system("cls"); 297 } 298 } 299 return 0; 300 }
調試工具為VS2017,所以會有 #include "stdafx.h"
另外,#include "huffman_code.cpp" 就是上次寫的那個程序,改了一點東西,全在這里面(http://files.cnblogs.com/files/journal-of-xjx/huffman.rar)
這是壓縮文件的存儲結構
拓展名長度 | 拓展名 。。。 | 字符種類 | 字符 字符頻率 。。。 | 文件長度 | 編碼 。。。 |
unsigned char | unsigned char 。。。 | unsigned int | unsigned char unsigned long 。。。 | unsigned long | unsigned char 。。。 |
省略號表示該部分有一系列值,另外,如果拓展名長度為0,那么不存儲拓展名,如果文件種類為1,那么不存儲文件長度,解壓文件時按照壓縮時的存儲順序依次讀取各部分的值
整個程序最關鍵的兩部分,一個是壓縮函數中的:
1 while (buf.size() >= 8) 2 { 3 char_temp = '\0'; // 清空字符暫存空間,改為暫存字符對應編碼 4 for (auto iter = buf.begin(); iter<buf.begin() + 8; iter++) 5 { 6 char_temp <<= 1; // 左移一位,為下一個bit騰出位置 7 if (*iter == '1') 8 char_temp |= 1; // 當編碼為"1",通過 位或 操作符將其添加到字節的最低位 9 } 10 fout.write((char *)&char_temp, sizeof(unsigned char)); // 將字節對應編碼存入文件 11 buf = buf.substr(8);// 編碼緩存去除已處理的前八位 12 } 13 14 15 if (buf.size() > 0) 16 { 17 char_temp = '\0'; 18 for (auto iter = buf.begin(); iter != buf.end(); iter++) 19 { 20 char_temp <<= 1; 21 if (*iter == '1') 22 char_temp |= 1; 23 } 24 char_temp <<= 8 - buf.size(); // 將編碼字段從尾部移到字節的高位 25 fout.write((char *)&char_temp, sizeof(unsigned char)); // 存入最后一個字節 26 }
這里用到了按位或操作和移位操作,具體如下:
假如buf現在的內容是 "0101000111010",那么我們先處理前八個字符"01010001",依次從左到右讀取這個字符串,如果讀到1,那么char_temp = char_temp | 1;
char_temp每次初始化為"00000000",1對應的ASCII碼為"00000001",第一次讀到0,char_temp先左移一位,char_temp不執行按位或,還是"00000000",第二次讀到1,此時按位或,char_temp更新為"00000001",然后左移一位,保持char_temp的最低為為0,然后右讀取一個字符,判斷是否執行char_temp = char_temp | 1,八次循環下來,char_temp的值對應的ASCII碼變為了01010001,這正好是我們最開始處理的buf中的前八個字符,換句話說,我們巧妙地利用char_temp與數字1的按位或以及移位操作成功將含有8個01字符串轉換成一個unsigned char 的ASCII碼的8個0和1。最后處理最后剩下的不足8個字符的01串,注意這次移位不是移一位而是 8 - buf.size() 位,把有效信息放前面,無效的0放后面。
二是解壓函數中的:
1 for (int i = 0; i < 8; ++i) 2 { 3 // 由根向下直至葉節點正向匹配編碼對應字符 4 if (code_temp & 128)//按位與,是壓縮時按位或的逆過程,code_temp字符對應的二進制的最高位是1,往右孩子方向走 5 root = Huffman_tree_array[root].rchild; 6 else 7 root = Huffman_tree_array[root].lchild; 8 9 if (root < char_size)//到達葉子結點 10 { 11 fout.write((char *)&Huffman_tree_array[root].data, sizeof(unsigned char)); 12 ++writen_len;//記錄讀取的文件長度 13 if (writen_len == file_len) break; // 控制文件長度,跳出內層循環 14 root = 2 * char_size - 2; // 復位為根索引,匹配下一個字符 15 } 16 code_temp <<= 1; // 將編碼緩存的下一位移到最高位,供匹配 17 }
這里用到了按位與操作和移位操作,目的和上面正好相反,是把ASCII中的0和1轉換成字符串0和1來匹配建立好的huffman編碼信息,還原相應字符。
這一部分代碼借鑒了@http://www.cnblogs.com/keke2014/p/3857335.html的代碼
還有一些地方也參考了他的思路,萬分感謝!
編程需細心,很多你不注意的細節往往隱藏着你需要幾天才能找出來的bug
---XJX
---17.4.11 江大 桃園