1. 程式人生 > >一次一小步之用C++實現Huffman檔案壓縮

一次一小步之用C++實現Huffman檔案壓縮

首先介紹下Huffman演算法:

哈夫曼編碼(Huffman Coding)是一種編碼方式,哈夫曼編碼是可變字長編碼(VLC)的一種。Huffman於1952年提出一種編碼方法,該方法完全依據字元出現概率來構造異字頭的平均長 度最短的碼字,有時稱之為最佳編碼,一般就叫作Huffman編碼。

明確幾個概念:(主要來自高教出版的資料結構與演算法,不想看的這裡可以直接跳過,點這裡

結點路徑長:如果n1,n2,n3,...,nk是樹中的節點序列,並且ni是ni+1(1=<i<=k-1)的父結點,這節點序列n1,n2,n3,...,nk稱為n1~nk的一天路徑,路徑長度就是k-1個長度之和。

樹的路徑長度:從樹的根到樹中每一個結點的路徑長度之和。

二叉樹

如上圖所示,假如每個相鄰結點之間長度為1,從A到C之間的路徑長度即為2

擴充二叉樹:每當在原來的二叉樹出現空子樹時,就加上一個特殊的結點,如下圖

外結點:上圖方形的結點;

內結點:圓形的結點;

外路長:根結點到每個外節點的路長之和;

內路長:根結點到每個內結點的路長之和;

加權路長:設有m個實數w1,w2,w3,...,wm,構造一棵有m個外結點的擴充二叉樹,並按一定的方法將數w1,w2,w3,...,wm與這m個外結點聯絡起來,稱SUM(wjlj)為二叉樹額加權路長,其中lj為某個外結點外路長,wj是與此結點相對應的實數,如上圖的實數;

顯然,上圖的加權路長為2 * 3 + 1 * 3 + 3 * 2 + 6 * 3 + 7 * 4 + 4 * 4 + 5 * 3 + 8 * 3,但是,你發現了嗎?將1、2和7、4的未知替換之後,加權路長會減少,對,我麼離Huffman不遠了。

現在,我們需要構造這樣一棵二叉樹,它的出現使我們有最小的加權路長,因而我們能用這個原理來壓縮資訊。

至於怎麼用,且聽我慢慢說來:

計算機裡檔案的最小單位是位元組Byte,8位,採用等長編碼,共256個碼,於是我們將他們構成一顆二叉樹,每個碼都對應一個二叉樹的葉結點,這棵樹有511個結點,共9層。由於是等長編碼,這棵二叉樹的擴充二叉樹的每個權值的大小相同,不妨定為1/256,於是,這顆擴充二叉樹的加權路長為1/256*8*256=8,剛好是每個位元組有的位數。但是,大家知道,檔案中每個字元出現的頻率是不一樣的,比如26個字母的出現的頻率不一致造成了你現在的鍵盤佈局。

於是,我們不妨統計一下每個字元的頻率,將頻率作為上面提到的256個碼擴充二叉樹的權值,這樣,只要通過一定的演算法構造一課最優的擴充二叉樹,我們就可以達到資訊壓縮的目的。

可能看得有點迷糊,不要緊,看下面的例子,順便提下字首性編碼

假如一種資訊由a,b,c,d組成,各字元出現的頻率是0.12、0.4、0.15、0.33,用一個二進位制數字串,對每個字元進行編碼,使任意一個字元的編碼不會是任何其他字元編碼的字首。通常把編碼的這種特性佳作“字首性”,“字首性”使任意兩個字元之間不需要加分隔符。

如上圖所示,等長編碼的編碼長度為2(上面提到過,加權路長即為編碼位數),而哈夫曼編碼長度為:0.12 * 3 + 0.15 * 3 + 0.33 * 2 + 0.4 * 1 = 1.87,顯然,我們達到了資訊壓縮的目的。

好了,說到這裡,我們以256個數碼(對應一個位元組)為例,正式開始講解Huffman演算法:

1、給出256個數碼的統計頻率,即權值,構造具有256棵擴充二叉樹的森林,其中,每棵擴充二叉樹只有一個帶權值的根結點,其左右子樹均為空。

2、在森林中選取兩課根結點權值最小的二叉樹,作為左右子樹構造一棵新的二叉樹,且置新的二叉樹的權值為其左右子樹上根結點的權值之和。

3、刪去這倆二叉樹,加入新得到的二叉樹

4、重複2和3,直到森林中只剩下一顆二叉樹

Huffman編碼構成過程

下面幾個圖可以看到Huffman編碼的構造過程是一個反覆比較的過程,它總是選擇兩個使用頻率較小的結點進行合併,生成出一個樹,這個樹經過編碼後就會得到Huffman編碼。

Haffman0.gif (1837 bytes)

在上圖中各點中的數字代表各點的使用次數,您可以把這幾個方塊想成A,B,C,D,它們在某一文章中的使用頻率為7次,5次,1次等等。

Haffman1.gif (2065 bytes)

選擇使用率小的兩個點1,3構成新點4。

Huffman2.gif (2226 bytes)

在狀態1圖中選擇5,4(也是兩個最小的,注意不是1,3,因為1,3現在已經歸在4裡面了)進行合併。

Huffman3.gif (2557 bytes)

在狀態2表中的最小兩個點已經變為7,6了,這時合併它們兩個生成新點13。

Huffman4.gif (2809 bytes)

只剩兩個點了,不管多少它們也是最小的了,合併了算了。

Huffman5.gif (2835 bytes)

請注意這個編碼,每個點下面有兩個分枝,分別編碼為0,1,當然這是為了方便計算機及其它數字裝置使用,您也可以編碼為“張三”和“李四”。至此編碼結束,所得到編碼即從最上面的點延線下行,至所要編碼的點,將沿路經過的0和1記錄下來就是了。現在您應該明白為什麼總是先把小的合併了吧,因為先合併的會在最下面,編碼長度最長,而先合併的也是最不常使用的,因此編碼長度最長也是應該的。

7 11
6 10
5 00
3 011
1 010

好了,說到這裡還是迷糊的話,不要緊,看看程式碼吧~來自我寫的一個huffman_tree類中,只放上來編碼這部分的,檔案操作這部分的就不獻醜了,其中用到的ProgressBar類在這裡 。

另外由於哈夫曼編碼和解碼都需要哈夫曼樹,我們在檔案壓縮的時候必須把哈夫曼樹存到壓縮後的檔案中去。

 

然後是每個函式:

1、構造和析構,不解釋了~

 

2、挑選頻率最小的兩個根結點的標號

 

3、統計數碼頻率

 

4、正式建裡哈夫曼樹,可能你說為什麼要分兩步,因為我為了縮小檔案大小,之後存入壓縮後的檔案只存256個float頻率,當然,對於大檔案沒什麼意義~

 

5、編碼

 

6、解碼:

解碼是編碼的相反過程,只要一個一個地讀入壓縮後的字元流的中的每一個位,然後是1的話就向向右子樹查詢,0的話就左,直到找到葉結點,將找到的葉結點中存放的數碼存入字元流即可

 

上面有用到C的位操作,只是不知道我這樣用效率高不高,應該還有別的更高校的位操作方法,下次找個試試

 

因為我編碼和解碼都是一個函式直接完成的,速度還是不錯的~

好了,先寫這些吧,歡迎各位批評指正~