編程獨白
給你40分鍾的時間,你可以思考十分鍾,然后用三十分鍾的時間來寫代碼,最后浪費在無謂的調試上;你也可以思考半個小時,徹底弄清問題的本質與程序的脈絡,然后用十分鍾的時間來編寫代碼,體會代碼如行雲流水而出的感覺。在編程過程當中,相信大家都深有體會,在調試上浪費時間,問題出現在下筆之前沒有一個系統結構。
關於哈夫曼
哈夫曼在通信領域有很多的用途,將需要傳輸的數據轉換01串,相比直接傳輸,極大提高了傳輸的速率,同時還在數據壓縮的重要方法。而本篇主要介紹的是哈夫曼壓縮算法。
哈夫曼樹創建
哈夫曼樹創建用了兩種的方法,一種是基於順序表,另一種是基於最小堆。關於這兩種方法可以參考:
建立Huffman樹的基本思路:
給定有權重的一系列數據(帶權重),從中挑選最小權重的兩個數據,組成一棵樹,得到的父節點再插入到數據系列當中。
哈夫曼壓縮思路
假設一字符串是,“abcabcabcabcabcabcddddddddd”,統計各字符出現的次數如下表。
字符 | 出現次數 |
a | 6 |
b | 6 |
c | 6 |
d | 9 |
按照一搬的存儲方法,一個字符占用一個字節,那么共花費(6+6+6+9)*sizeof(char) = 27字節,27字節確實不算什么,但是如果是海量數據的時候,就可能要考慮存儲空間的問題了。
來看看哈夫曼壓縮算法是怎么做的,同樣是上面的例子,我們試着建立哈夫曼樹,出現的次數就當做是權重,出現的次數越多的話,越靠近根節點,那么編碼越短,如下圖:
於是上面的“abcabcabcabcabcabcddddddddd”,就可以轉化為“0001,1000,0110,0001,1000,0110,0001,1000,0110,1111,1111,1111,1111,11”,注意這里采用的按位存儲的,也就是說0和1都是位,而非char型。那么之前的27字節就被我們轉換成了7個字節(7字節不足,不足的話就補零),而這就達到了壓縮的效果。
所以總結一下:利用哈夫曼樹編碼的特點,權重越大越靠近根節點,得到的編碼就越短的原理,而如果把字符出現次數作為權重的話,文本當中出現次數最多的字符就被壓縮成了很短的編碼。
哈夫曼壓縮詳解
·壓縮過程主要步驟如下:
- 統計:讀入源文件,統計字符出現的次數(即統計權重),順便根據權重進行從大到小的排序(主要的話之后的操作會簡單一些);
- 建樹:以字符的權重(權重為0的字符除外)為依據建立哈夫曼樹;
- 編碼:依據2中的哈夫曼樹,得到每一個字符的編碼;
- 寫入:新建壓縮文件,寫入壓縮數據。
其中最為復雜的是步驟4,因為它涉及到了位的操作,待我細細道來。
假設一字符串是,“acbcbacddaddaddccd”,統計各字符出現的次數如下表。
字符 | 出現次數 |
a | 4 |
b | 2 |
c | 5 |
d | 7 |
步驟123統計,建樹,編碼都已經完成了,剩下寫入壓縮文件。將字符串一步一步翻譯成01串,
- a:111
- ac:1111 0
- acb:1111 0110
- acbc:1111 0110 10
- acbcb:1111 0110 1011 0…
似乎都很順利,但是位操作有點麻煩。首先申請足夠大的內存,比如已知文本字符個數是1000個字符(字節),可以申請1000*4,即一個字節平均4字節(32位)的壓縮編碼空間(已經足夠大了),別把這些看成是char型了,當作位來看。
聲明nCodeIndex作為已編碼的位數,相當於counter。剛申請的足夠大的內存以8位划分,那么可以發現:
- nCodeIndex/8可以標明第一個未存滿的字節,相當於nCodeIndex>>3;
-
nCodeIndex%8可以標明第一個未存滿的字節當中有幾位已經完成了存儲,相當於nCodeIndex&7。
可能說的不夠清楚,所以畫了圖:
*(long *)(pDest+(nCodeIndex>>3)) |= (p->code) << (nCodeIndex&7);(其中p為哈夫曼節點)
如此一來,我們就可以很理直氣壯的將*(long *)(pDest+(nCodeIndex>>3))賦值為*(long *)(pDest+(nCodeIndex>>3)) | (p->code) << (nCodeIndex&7)(0|X=X),而不用擔心到底*(long *)(pDest+(nCodeIndex>>3))里面到底有多少位已經是存儲了的。
給出壓縮主要代碼:
void CompressHuffman(char * filename) { //·統計權重 int count = Statistics(filename); Huffman hf; hf.CreateHuffmanTree(acsii,count); //·測試 //hf.Print(); //根據生成的哈夫曼樹,為每個字符生成編碼 EnCode(&hf,count); HuffmanNode * root = hf.GetRoot(); fstream fshuff; fshuff.open("data.huff",ios::binary|ios::out); //寫入壓縮文件的工作流程如下: //·數據個數(字符出現的個數) //·每個數據(data)以及其權重(rate) //·源文件中的字符個數,如源文件為abcd123,那么字符個數為7 //·編碼的長度 //·編碼 //·寫入字符個數,比如abdarr,字符個數是4 fshuff.write((char *)&count,sizeof(int)); //·寫入acsii值和權重,以便在解壓縮的時候重建哈夫曼樹 unsigned int i; for(i=0; i<count; i++) { //·這里的讀寫可能會有問題的 fshuff.write((char *)&(acsii[i].data),sizeof(char)); fshuff.write((char *)&(acsii[i].rate),sizeof(int)); } //·注意區別fs和fshuff,前者是源文件,后者是壓縮后的文件 fstream fs(filename,ios::in|ios::binary); //·獲取源文件文件字節數大小,seekg用在讀中,seekp用在寫中 fs.seekg(0, ios_base::end); long nFileLen = fs.tellg(); fs.seekg(0,ios::beg); //·將源文件逐個寫入到壓縮文件當中 char * pDest = new char [nFileLen]; for(i=0; i<nFileLen; i++) //·清零,不清零會出現致命性的錯誤 *(pDest+i) = 0; unsigned int nCodeIndex = 0; char * temp = new char[nFileLen+1]; fs.read(temp,nFileLen); HuffmanNode * p; for(i=0; i<nFileLen; i++) { //·在acsii表當中 p = GetCodeFromACSIITable(temp[i],count); assert(p != NULL); *(long *)(pDest+(nCodeIndex>>3)) |= (p->lCode) << (nCodeIndex&7); nCodeIndex += p->nCodeLen; } //·寫入源文件的字符個數 fshuff.write((char *)&nFileLen,sizeof(long)); //·寫入編碼長度 fshuff.write((char *)&nCodeIndex,sizeof(int)); unsigned int nDestLen = nCodeIndex/8; if(nDestLen*8 < nCodeIndex) nDestLen ++; //·寫入編碼 fshuff.write(pDest,nDestLen); fshuff.close(); fs.close(); }
簡介哈夫曼壓縮文件DL結構
前一段時間在接觸位圖的時候被位圖結構觸動了,感覺它存儲得有條理,於是萌生了為哈夫曼壓縮文件定義一個存儲結構,稱之為哈夫曼壓縮文件DL結構。關鍵是要統一,這篇博文用的是一種結構,另一篇用的又是另一種,紛雜的樣式會讓初學者發暈,所以統一結構對於學習哈夫曼壓縮文件會有很大的幫助。
DL結構組成部分:
- 節點個數-用以規定創建哈夫曼樹節點個數,以便讀入節點域;
- 節點域-用以創建哈夫曼樹和產生字符編碼;
- 源文件字符數-在解壓的時候有用;
- 編碼長度(以位為單位)-源文件編碼的總長度,以位為單位;
- 編碼域-存儲所有字符編碼的存儲域。
為了便於讓你不看文字就能明白,看下圖,按着這種結構就相當於有了大概的思路。
哈夫曼解壓詳解
解壓的過程就簡單很多了,因為一些代碼已經在解壓過程當中完成,比如哈夫曼樹的建立,我們只要設計壓縮和解壓通用的接口,就可以很簡單的按照編碼域的內容,將編碼翻譯成原文。
- 讀入節點個數;
- 根據1,讀入節點域;
- 創建哈夫曼樹;
- 讀入編碼長度;
- 根據4,讀入編碼域;
- 翻譯;
- 寫入解壓后的文件。
根據編碼長度(以bit為單位),可以計算出編碼域的大小(以byte為單位),讀入編碼域就很方便了。其中翻譯部分我給出一部分代碼,根據哈夫曼樹,將編碼域的01串按位處理,轉換為字符。
- Code&1判斷最低位是0還是1,從而決定指向left還是right;
- Code>>=1,將Code右移,方便處理下一位;
- 每當翻譯出一個字符,就要將當前的哈夫曼指針重新指向root,p = hf.GetRoot()。
for(i=0; i<nFileLen-1;) //·特地少處理一個字節 { Code = *(temp+(nSrcIndex)); for(int j=7; j>=0; j--) { p = (Code&1) ? p->right : p->left; if(!p->left && !p->right) { *(pDest+i) = p->data; p = hf.GetRoot(); i++; } Code>>=1; //·為了處理下一位,右移一位 } nSrcIndex ++; } Code = *(temp+(nSrcIndex)); for(int j=0; j<nOffset; j++) { p = (Code&1) ? p->right : p->left; if(!p->left && !p->right) { *(pDest+i) = p->data; cout << p->data << endl; p = hf.GetRoot(); } Code>>=1; //·為了處理下一位,右移一位 }
附哈夫曼壓縮算法工程:哈夫曼壓縮算法.rar
本文完。Monday, December 26, 2011