編程獨白
給你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






