乍一聽,這個文件壓縮的名字貌似是很高大上的,其實,在數據結構中學完Huffman樹之後,就可以理解這個東西其實不是那麼的高不可攀。
文件壓縮
所謂文件壓縮,其實就是將對應的字符編碼轉換爲另一種佔據字節數較少的編碼來進行存儲。
舉個栗子:有一串文本:aaaabbbccd,其中單獨將這串字符存放在文件中,它所佔據的將會是至少10個字節(爲什麼說是至少,因爲還有一些必要的文件信息要保存的說)。由此就有人嘗試着要以重新編碼,然後再存儲的方式來節約我們寶貴的磁盤空間以及傳輸時間。還是這個字符串,其中a出現了4次,b出現3次,c出現2次,d只出現了一次。由此我們可以重新編碼:
什麼意思呢?我們按表這個來編碼,a對應0,b對應10,依此類推,可以將原字符串轉換爲00001010 10111111 110,用二進制位來代替原有的字符,這樣將出現次數較多的字符替換爲較短的編碼,便實現了對字符串的壓縮。字符串中字符的出現次數可以遍歷一遍統計出來,那麼現在的問題就是如何得到這樣的編碼了!
Huffman樹
Huffman的定義:假設給定一個有n個權值的集合{w1,w2,w3,…,wn},其中wi>0(1<=i<=n)。若T是一棵有n個 葉結點的二叉樹,而且將權值w1,w2,w3…wn分別賦值給T的n個葉結點,則稱T是權值爲 w1,w2,w3…wn的擴充二叉樹。帶有權值的葉節點叫着擴充二叉樹的外結點,其餘不帶權值 的分支結點叫做內結點。外結點的帶權路徑長度爲T的根節點到該結點的路徑長度與該結點上的權值的乘積。
說的有些偏理論,看個圖:
如上,所有的葉子節點處有所謂的權值,從根結點到某一葉子節點的分支個數爲對應的路徑長度,如(a)中的權重爲1的結點,它的路徑長度爲2,路徑和權值的乘積爲2,這顆樹的帶權路徑長度爲 1*2 + 3*2 + 5*2 + 7*2 = 32;(b)(c)就不計算了,(d)爲7*1 + 1*3 + 3*3 + 5*2 = 29.像(d)這樣的帶權路徑長度最短的樹就叫做Huffman樹,也叫做最優二叉樹。
Huffman樹的創建
- 、由給定的n個權值{w1,w2,w3,…,wn}構造n棵只有根節點的擴充二叉樹森林F= {T1,T2,T3,…,Tn},其中每棵擴充二叉樹Ti只有一個帶權值wi的根節點,左右孩子均爲 空。
- 、重複以下步驟,直到F中只剩下一棵樹爲止:
a、在F中選取兩棵根節點的權值最小的擴充二叉樹,作爲左右子樹構造一棵新的二叉樹。將新二叉樹的根節點的權值爲其左 右子樹上根節點的權值之和。
b、在F中刪除這兩棵二叉樹;
c、把新的二叉樹加入到F中;
最後得到的就是Huffman樹。
由於每次要從森林中選取權重最小的兩棵樹,當然用堆來實現會比較方便,我這裏用的是標準模板庫中的優先級隊列實現的,底層也是堆。具體的代碼太長,附上鍊接:Huffman.hpp:https://github.com/Fireplusplus/Data-Structure/blob/master/HuffmanTree.cpp
Huffman編碼
再創建好的Huffman樹的分支上標記,左分支標記0,右分支標記1,這樣,有根結點到某個葉子節點路徑上的01序列即爲要求得Huffman編碼。如圖:
7的編碼爲0, 1的編碼爲100,3的編碼爲101, 5的編碼爲11. (Huffman樹不是唯一的,編碼也不是唯一的)
由這種方法得到的編碼是前綴編碼:任何一個字符的編碼不是另一個字符編碼的前綴。 這樣才能保證譯碼的唯一性。
理清了所有思路,實現起來就不難了!
實現
大概的說一下是怎麼實現的:
壓縮:
- 遍歷一次文件,統計對應字符出現次數
- 創建Huffman樹
- 得到Huffman編碼
- 將解壓縮必要信息保存到目標文件首部(我的實現:原文件擴展名,後續字符行數,字符與對應次數,數據部分)(以‘\n’分隔)
- 按編碼將對應字符轉換(位運算)
- 保存
解壓:
- 取出文件頭部信息
- 設置字符與對應出現次數
- 以同種算法創建Huffman樹
- 解碼(編碼對應的原字符可由從根結點開始,0走左,1走右,直到遇到葉子節點,則爲對應字符)
- 保存
效果
嘗試着將其中的某個源文件進行壓縮,原大小8字節,壓縮後變爲6字節,節省了2字節的存儲空間。(ps:gl是我瞎編的擴展名)
具體能縮減多少空間就還要看具體字符出現的頻率了。解壓後所有字符均與原文件相同。當然也可以壓圖片:
壓縮前(左)與壓縮後(再解壓後)(右)對比:
a: 看起來沒什麼不同嘛!=_=||
b: 沒什麼不同才叫解壓哈! =_=||
遇到的問題
1.問題:個別字符處理解析錯誤。原因分析:GetLine函數沒有考慮到‘\n’的特殊性,混淆可分隔用的‘\n’與行結束標誌‘\n’。解決方法:重新考慮遇到‘\n’的特殊情況,修正GetLine函數。
2.問題:解壓後文件不全,體現爲無後半部分。原因分析:從文件中讀取字符時,遇到了假的EOF標誌(某字符爲有意義的字符,卻和EOF有着相同的位)。解決方法:文件操作時均採用二進制方式讀寫。
3.問題:解壓後的文件每個1024個字符就出現亂碼。原因分析:文件解壓縮時每次讀取1024個字節到ReadBuf裏,然後對讀進來的每個字節(放在char變量裏)進行位操作,用pos標誌來標記當前處理到的位,解碼後的字符放在WriteBuf裏。但是ReadBuf中一輪數據處理完成後讀取後1024個字節,這時重新清零了pos標誌,導致當前char變量中剩餘的位沒有處理。解決方法:將pos的位置移到讀操作循環的外部。(ps:就這個小bug着實耗費了我很長時間=_=||)
其它的都是一些小問題,就不一一羅列了。
GitHub鏈接
最後,本着資源共享,共同學習的精神,放上項目的開源鏈接(源代碼有詳細註釋):
https://github.com/Fireplusplus/Project/tree/master/FileCompress
用C++實現文件壓縮
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.