哈夫曼壓縮原理及其簡單實現
1、原理
首先要了解一下哈夫曼樹即最優二叉樹的概念,就是給每個葉子節點一個權值,構建一顆二叉樹,使得權值乘以葉子節點到跟節點的和值最小,那麼這棵樹就 是最優二叉樹。下面就是建立一棵二叉樹的過程,葉子節點裡的值就是其權值
現在來看一看這個最優二叉樹的特點,那就是權值越大,離跟節點越近。
好,現在來看一看檔案的特點,一個檔案是由不同的位元組組成,但位元組最多就只有255個,那麼其中就一定有重複的位元組,我們可以讀出其中的位元組,並統計這個位元組的頻率,把頻率作為葉子節點的權值,來構建一個二叉樹。
就以上圖為例,如果一個檔案裡有abcd四個字元,a的頻率為7,b的頻率為5,c為3,d為1,那麼這個檔案就有7+5+3+1=16個位元組,如果把父節點到左孩子的路徑標為0,到右孩子的路徑標為1。那麼每個葉子節點就可以用一個“01”串來表示,即a-7-“0”,b-5-“10”,c-3-“110”,d-1-“111”,再把各個字元對應的01串存入檔案,現在檔案的大小為7*1+5*2+3*3+1*3=29位,補成8的倍數32位,就變成了4個位元組。
當然當檔案大了得出的01串是不止8位的,但是頻率越大,裡跟節點越近,一般都是小於八位的,這就達到了壓縮的目的,因為把位數變少的次數多於變多的次數。
2、具體實現
我是分為以下幾個步驟:
1.讀取檔案內容並統計各個位元組的頻率
2、根據統計的頻率構建哈夫曼樹,並得出哈夫曼編碼表,即各個位元組對應的01串
3、把哈夫曼編碼表寫入壓縮檔案(用於解壓),把原始檔內容對應的哈夫曼編碼寫入壓縮檔案
4、解壓,先讀出哈夫曼編碼表,對應編碼表還原檔案。
當然,我在每一步中都遇到了問題
1、第一步中,最大的問題就是如何統計並儲存每個位元組的值及頻率,我是用一個長度為256的陣列來實現的,陣列下標表示位元組的值,陣列的值儲存頻率。然後再把統計的值及頻率存入一個隊列當中。以下是核心程式碼
2、第二步中就是如何構架哈夫曼樹的問題,以及遍歷得到哈夫曼編碼,直接上程式碼吧public ArrayList<TreeNode> getNodedata(String src) { ArrayList<TreeNode> list = new ArrayList<TreeNode>(); try { fis = new FileInputStream(src); int[] buf = new int[256];// 存放資料頻率的方法 int b; while ((b = fis.read()) != -1) { buf[b]++; } for (int i = 0; i < 256; i++) { if (buf[i] != 0) { Nodedata data = new Nodedata(); data.data = i + ""; data.count = buf[i]; TreeNode treenode = new TreeNode(data);// 把資料存到樹節點上 list.add(treenode); } } fis.close(); } catch (IOException e) { e.printStackTrace(); } return list; }
public class Modetree {
Map<String, String> map = new HashMap<String, String>();
//構建哈夫曼樹的方法
public TreeNode getTreeroot(ArrayList<TreeNode> list)
{
while(list.size()>1){
Collections.sort(list);//對節點列表的按頻率的低高排序
TreeNode node1 = list.remove(0);
TreeNode node2 = list.remove(0);//取處頻率最小的兩個節點
Nodedata data = new Nodedata();//建立一個新結點,其權值為nide1和node2的和
data.data="新的樹枝節點";
data.count= node1.data.count+node2.data.count;//附權值
TreeNode root = new TreeNode(data);
root.left= node1;
root.right = node2;
node1.parent = root;
node2.parent = root; //構建關係
list.add(root);//迴圈取出構建
}
return list.get(0);
}
//遍歷樹的方法,得到哈夫曼編碼表的方法
public Map<String,String> showTree(TreeNode root,String str){
//如果有左孩子就加個0,有右孩子就加個1,直到遍歷完成為止
if(root!=null)
{
if(root.left!=null)
{
showTree(root.left,str+"0");//如果左孩子不為空,就給編碼加個0
}
if(root.right!=null)
{
showTree(root.right,str+"1");//如果右孩子不為空,就給編碼加個1
}
if(root.left==null&&root.right==null)
{
root.data.bm = str;
map.put(root.data.data, str);
System.out.println(root.data);
return map;
}
}
return map;
}
3、得到了哈夫曼編碼,建的樹就沒有作用了,現在是如何把檔案中的每個位元組換成哈夫曼編碼,而且在這之前要把哈夫曼編碼寫入壓縮檔案,
解壓的時候會用到,寫入的方法就有很多了,我用的是最笨的方法,一個位元組一個位元組的寫。這裡就不貼程式碼了,因為每個人想得都不一樣,只要達到目的就行了。主要貼出一段把長度為八的“01”字串轉化為一個整型的方法
// 把八個字元的01字元轉化為01字串對應的整型就是進行了位運算
public byte charToint(char[] cs) {
return (byte)((cs[0] - 48)* 128 // cs【0】-48把字元變為字元內容的整型若cs【0】為‘1’那麼它的acill碼值為49,減去48的話,就變成了整型的1
+ (cs[1] - 48) * 64
+ (cs[2] - 48) * 32
+ (cs[3] - 48) * 16
+ (cs[4] - 48) * 8
+ (cs[5] - 48) * 4
+ (cs[6] - 48) * 2
+ (cs[7] - 48) * 1);// 8位有效值的int型轉化為byte型時有的是負數,因為byte相當於是隻有7位的有效數值最高位是表示正負的
}
4、第四步是解壓的過程,還原檔案。怎麼寫的就怎麼讀出來,先得把哈夫曼編碼表讀出來,然後讀出先前寫入的檔案內容,還是轉換為01字串的形式,然後對照編碼表還原檔案的內容。
當然這個壓縮器做的還很粗糙,需要進行優化,比如說讀寫檔案的速度問題,一個位元組一個位元組的讀,感覺很慢。還有就是我有時候壓縮後的檔案比原始檔還大,這是由於我寫入了哈夫曼編碼表,有時候就比原始檔大了。後續優化好了再放程式碼出來