用C++實現Huffman文件編碼和解碼(2 總結)


這個是代碼是昨天寫完的,一開始的時候還出了點小bug,這個bug在晚上去吃飯的路上想明白的,回來更改之后運行立刻完成最后一步,大獲成功。

簡單說下huffman編碼和文件壓縮主要的技術。

Huffman編碼,解碼:

I 創建Huffman樹

II 根據Huffman樹實現編碼,並將編碼結果和要編碼的數據建立映射關系。

III Huffman解碼,也就是根據獲取的Huffman碼來逆向獲取解碼信息,而且你從解壓文件中一次性獲取的數據是一個很長的字符串,沒有預處理好的成段字符串式Huffman碼。1

I 首先,如何創建Huffman樹?

在這個我在前天的那篇文章中簡單的提了一下,現在好好說一下。如果你不知道什么是Huffman樹,請google之~

對於獲取到的文件,首先要做的就是,建立一個長度為256的int數組,全部置零,然后以字節流的形式讀取文件,並對字節流中的字節出現次數進行統計,方法就是以字節數值為數組偏移地址,對應的數組元素進行+1操作。另外這里需要提一下的就是,用於存儲文件字節流的緩沖區最好是unsigned char類型,因為這樣能直接使用,如果是char的,在轉化為int類型的時候,一旦數值大於127,因為補碼問題,你就直接乘上了通往未知數值的高鐵~

完成統計之后,將這個數組中出現次數不為0的元素添加對應大小的二叉樹節點數組中,然后以出現次數為Key值,進行排序。

在排序完成之后,就能開始構建Huffman樹了。操作如下:

1 如果數組中元素個數不為1,將前兩個元素構造為一個臨時節點的子樹,此時臨時節點的Key值為兩個元素Key值之和,然后刪除數組中的第一個元素(從數組中刪除),再將臨時節點賦值給當前數組的第一個元素。

(其實就是將前兩個元素添加到一個臨時節點的左右根節點,然后在原數組中刪除這兩個元素,接着再將這個臨時節點插入到數組頭部,充當新的節點。上面的那段描述我覺得說的不是很清楚,但是那個是我在代碼中發現的一個可以優化的地方,減少了一個元素的刪除操作)

2 此時數組依據key值的排序很有可能已經不再有序,而又因為僅有一個亂序元素,所以專門設計了一個函數,一次完成排序,效率,應該是最高的了。重復1

這樣當數組中只有1個元素的時候,就是Huffman樹的根節點了。

這樣,Huffman樹的構造就完成了。我上面說的可能不是很清楚,你看了之后可能會有疑問,所以我在這貼下部分代碼,你可以看一下,就是這么簡單,而且很巧妙。

Huffman樹節點,一開始就是一個Struct,但是因為涉及到了STL,所以添加了方法

 1 struct HaffmanStruct
 2 {
 3     //a small structure
 4     HaffmanStruct():val(0),ncounts(0),lNext(NULL),rNext(NULL){}
 5     bool operator < (HaffmanStruct &);
 6     bool operator > (HaffmanStruct &);
 7     void Reset();
 8     unsigned char val;
 9     unsigned int ncounts;
10     char HuffmanCode[254];
11     //used for tree
12     HaffmanStruct * lNext;
13     HaffmanStruct * rNext;
14 };
View Code

給他一個數組,他給你一顆Huffman樹

 1 void HuffManEncode(vector<HaffmanStruct> & vecValidNumberArray)
 2 {
 3     HaffmanStruct ValidStruct;//temporary struct
 4     //Analysis
 5     while(vecValidNumberArray.size() != 1)
 6     {
 7         ValidStruct.Reset();
 8         ValidStruct.ncounts = vecValidNumberArray[0].ncounts + vecValidNumberArray[1].ncounts;
 9         ValidStruct.lNext = new HaffmanStruct;
10         *ValidStruct.lNext = vecValidNumberArray[0];
11         ValidStruct.rNext = new HaffmanStruct;
12         *ValidStruct.rNext = vecValidNumberArray[1];
13         vecValidNumberArray.erase(vecValidNumberArray.begin());
14         vecValidNumberArray[0] = ValidStruct;
15         SingleSort(&vecValidNumberArray[0], vecValidNumberArray.size(), 0);
16     }
17 }
View Code

以上就是Huffman樹構造的全部過程。

 

II 根據Huffman樹獲取Huffman編碼

對樹最有效的訪問方式就是遍歷,而遍歷有兩種方式:深度優先遍歷和廣度優先遍歷。不過學過Huffman編碼的人都知道,Huffman的編碼,必須使用深度優先遍歷,你懂得~

我在此默認的模式是,左樹為0,右樹為1.而這個遍歷函數需要使用一個編碼緩沖區和輸出目標,以及深度探測。於是乎,一個參數好多的遞歸函數新鮮出爐了,昨天才被我正式造出來。

 1 template <class T>
 2 void ErgodicTree(T & Root, char * szStr, int nDeep, string pStrArray[])
 3 {
 4     if(Root.lNext == NULL && Root.rNext == NULL)
 5         pStrArray[Root.val] = szStr;
 6     szStr[nDeep] = '0';
 7     if(Root.lNext != NULL)
 8         ErgodicTree(*Root.lNext, szStr, nDeep + 1, pStrArray);
 9     szStr[nDeep] = '1';
10     if(Root.rNext != NULL)
11         ErgodicTree(*Root.rNext, szStr, nDeep + 1, pStrArray);
12     szStr[nDeep] = 0;
13 }
View Code

需要注意的是,編碼和解碼的遞歸函數是不一樣的,在這專門提一下,因為編碼是一次性遍歷完成全部的節點,而解碼是每次只遍歷到葉子節點。

可以看到,每次向下傳遞參數的時候,左樹就置'0',右樹就置'1',返回的時候必須清零。這樣下一級函數會獲取的結果,並且根據Deep的值對應置位,上級函數函數的乞討遞歸也不會受到影響。代碼寫的很簡單,但是其實很細致。

一旦訪問到了葉子節點,就直接輸出,這里寫的也很巧妙,也就是在這里,獲取到了Huffman編碼,輸出到對應的string數組中。

這樣,就完成了Huffman編碼。

III Huffman解碼

用Huffman解碼之前,你獲取到的是一個很長的,內容是'0'和'1'的字符串。在我的代碼中,這個字符串的長度是1024.

其實Huffman的解碼實現起來也很簡單,但是,存在細節性問題。

比如:從遞歸函數中獲取返回值、下次解碼的偏移地址、字符串訪問已經到頭了,但是解碼失敗(你想想這個問題出現的圓心),此時字符串中還剩下幾個未解碼的字符。

這些都是相當細節性的問題,另外文件中一般有n多個1024長度的以上的字節數,如何承上啟下也是問題。

這一切,都在下面這段代碼中解決:

 1 char Buffer[128];
 2     char DecodeBuffer[1056];//增加了八個緩沖字節
 3     DWORD dwReadByte;
 4     DWORD dwFlag = 1;
 5     DWORD dwDeep = 0;
 6     char tmpchar;
 7     int EffectiveBufferSize = 0;
 8     int nLeftNumberInBuffer = 0;
 9     char szSmallBuffer[2] = {0};
10 
11     //創建解壓文件
12     HANDLE hDeCompressionFile = QuickCreateFile("C:\\DCRecord.txt");
13     assert(hDeCompressionFile != INVALID_HANDLE_VALUE);
14 
15     while(1)
16     {
17         int i = 0;
18         dwReadByte = ReadHuffCodeFromFile(hHuffFile, Buffer, 128);
19         if(dwReadByte == 0)
20             break;
21         EffectiveBufferSize = ReadBitToBuffer(Buffer, (int)dwReadByte, DecodeBuffer + nLeftNumberInBuffer, 1024);
22         EffectiveBufferSize += nLeftNumberInBuffer;
23         //TextFileFunction(DecodeBuffer, 1024);
24         for(i = 0;(i + dwDeep) < EffectiveBufferSize;i += dwDeep)
25         {
26             dwDeep = 0;
27             tmpchar = DecodeHuffman(&vecHuffmanArray[0], DecodeBuffer + i, EffectiveBufferSize - i, dwDeep, dwFlag);
28             if(dwFlag == 1)
29             {
30                 szSmallBuffer[0] = tmpchar;
31                 WriteBufferIntoFileNormally(hDeCompressionFile, szSmallBuffer, 1);
32             }
33             else
34             {
35                 dwFlag = 1;
36                 break;
37             }
38         }
39         nLeftNumberInBuffer = EffectiveBufferSize - i;
40         memcpy(DecodeBuffer, DecodeBuffer + i, nLeftNumberInBuffer);
41     }
View Code

這段代碼中對於這種問題完成的很好,我上面說的在晚上去吃飯的路上就是想明白了實現承上啟下那個問題的。

大致步驟如下:

要注意到參數Deep是引用值,是會修改原值的。這個值同時是遞歸時使用的字符串偏移地址,這個地址所在的值,決定了下一級是向左子樹走還是向右走的方向。也就是根據字符串數據來訪問Huffman樹,一旦訪問到葉子節點,就表明此次的解碼完成了,返回這個對應值。雖然是遞歸調用,但是每一級遞歸調用只有一條通路選擇,所以返回值具有可傳遞性。

完成一段字符串的解碼之后,此時的Deep參數就已經是訪問過的字符串個數了,就能用於下一次解碼的地址偏移,能夠用於循環代碼操作。

另外還有個問題就是,我從獲取的huffman解碼整條字符串,都是8的倍數(因為下級函數時將一個字節的8位數據按位解讀,寫入字符串),所以到最后的一段字符串解碼失敗很正常,因為這段字符串碼不完全。此時就需要將這段字符碼移到首部,然后與下一次讀出來的字符碼進行拼接。你可以注意到,我的代碼中,一般都是在for循環中直接聲明int i,但是在這里卻是在while循環外聲明的i,就是為了實現拼接,使得解碼操作能夠傳遞下去。如果一次性創建一個很大很大的緩沖區把整個文件都讀進來,我只能說:圖樣圖森破。

具體的操作就看代碼吧,我寫的時候是有點小糾結的,但是寫完了一看,呵呵,就這么簡單。

這樣,解碼也就完成了。

 

最后說一下文件操作。

首先寫文件有一塊很重要的就是,需要寫一個文件頭。而且是一個變長的文件頭。

文件頭主要內容:(不涉及文件夾)

1 文件原名,以及后綴

2 需要編碼的數據的數據個數

3 存在的字符和該字符出現的次數

上面的2 3其實就是把構造Huffman樹最基礎的數組存入文件中,這樣解壓文件就能根據文件頭來構造Huffman樹,從而實現解碼了。為此我專門寫了一個負責文件頭的函數。而且這里有個注意事項就是,寫文件的時候,要把排好序的數組寫進去,這樣解壓文件就不需要再次進行排序了,能省則省嘛。

查看文件頭數據:

 

這個是我以前水平還很爛很爛的時候寫的一個查看程序,最可笑的就是,我這緩沖區用的是一個CString,現在看看,真是荒唐可笑。

不過現在對於小文件,還是能看一看的。你能看到,前幾個都是1的,就是int數據,只有出現1次的字符。后面出現的次數逐漸增加。

再往下拉的話,就是各種亂碼了,都是按字節解釋起來亂七八糟的東西了。

其實編碼解碼的文件操作這塊,我覺得應該算是計算機中,對最小數據單元的操作了,絕對沒有比這更小的了。因為要做的是根據編碼結果一位一位的將數據寫到char變量中,而在讀文件這塊,也是整塊的讀內存,然后按位解析字節,獲取到以字節為單位的解碼數據。

這塊我就貼這幾個函數,用於從字節到位,和從位到字節的操作。這活,還真的是挺細致的。記得我昨天調代碼的時候,還專門調試這幾個函數,因為當時解碼出來的是亂碼,然后我對照寫文件前的Huffman碼,還真的找到了問題所在。

Byte to Bit

 1 void SetByteBit(char * ByteAddr, int Val, int BitAddr)
 2 {
 3     int TmpVal = -1;
 4     int tmpval = 1;
 5     tmpval <<= BitAddr;
 6     if(Val == 0)
 7     {
 8         TmpVal ^= tmpval;
 9         *ByteAddr &= TmpVal;
10     }
11     else
12     {
13         *ByteAddr |= tmpval;
14     }
15 }
16 /*
17 將huffman碼按位寫入文件
18 */
19 void WriteByteToFile(HANDLE hFile, const char * lpszHuffCode, int Mode)
20 {
21     if(hFile == INVALID_HANDLE_VALUE)
22         return;
23 
24     static int snBytePointer = 0;
25     static int snBitPointer = 0;
26     static char WriteBuffer[512];
27     DWORD wfCounter = 0;//寫緩沖區指針
28     int nLength = 0;
29     if(lpszHuffCode != NULL)
30         nLength = strlen(lpszHuffCode);
31 
32     if(Mode == 1)
33     {
34         WriteFile(hFile, WriteBuffer, snBytePointer + !!snBitPointer, &wfCounter, NULL);
35         snBytePointer = 0;
36         snBitPointer = 0;
37         wfCounter = 0;
38         return;
39     }
40 
41     for(int i = 0;i < nLength;\
42      ++snBitPointer >= 8?(snBytePointer ++,snBitPointer = 0):snBitPointer,i ++)
43     {
44         if(snBytePointer > 511)
45         {
46             WriteFile(hFile, WriteBuffer, 512, &wfCounter, NULL);
47             snBytePointer = 0;
48         }
49         SetByteBit(&WriteBuffer[snBytePointer], lpszHuffCode[i] - '0',snBitPointer);
50     }
51 }
View Code

Bit to Byte

 1 /*
 2 ReadByteBit Function
 3 2013 10 05
 4 */
 5 
 6 void ReadBitFromByte(const char Byte, char * buf = NULL)
 7 {
 8     if(buf == NULL)
 9         return;
10     int nTmpVal = 1;
11     int nAndResult;
12     for(int i = 0;i < 8;++ i)
13     {
14         nTmpVal <<= i;
15         nAndResult = nTmpVal & Byte;
16         buf[i] = '0' + !!nAndResult;
17         nTmpVal = 1;//this is prrety important
18     }
19 }
20 /*
21 還是寫中文注釋吧
22 這個函數是用於將從文件讀出來的二進制信息取出來,並存放到字符串中。
23 返回值就是讀出來的位數的長度
24 */
25 
26 int ReadBitToBuffer(const char * ReadBuf, int nByteNumber,char * OutputBuf, int nOBufLength)
27 {
28     if(nOBufLength < nByteNumber * 8)
29         return 0;
30     for(int i = 0;i < nByteNumber;i ++)
31     {
32         ReadBitFromByte(ReadBuf[i], OutputBuf + 8 * i);
33     }
34     return nByteNumber * 8;
35 }
View Code

剛才看了看自己寫的一部分代碼,感覺,還真的感覺到了代碼中有不少自己的努力和智慧。

代碼就不全貼了,內容就這么多,都是最基礎的操作。不過此番之后,我覺得,我已經有能力編寫壓縮文件程序了,至少數據壓縮存儲這一塊,我有了最基礎的技術。

看下解碼效果:

簡單提下:

我的解壓文件字符沒問題,但是為什么有些符號卻不一樣?你可以看到,最后的輸出的結果是有些許不同的,這令我很費解~

源文件:

 

壓縮后的文件,這感覺,用三個字母表示:WTF……

解壓文件:

至此完成

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM