1. 程式人生 > >哈夫曼壓縮原理及其簡單實現

哈夫曼壓縮原理及其簡單實現

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的陣列來實現的,陣列下標表示位元組的值,陣列的值儲存頻率。然後再把統計的值及頻率存入一個隊列當中。以下是核心程式碼

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;
	}
   2、第二步中就是如何構架哈夫曼樹的問題,以及遍歷得到哈夫曼編碼,直接上程式碼吧
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字串的形式,然後對照編碼表還原檔案的內容。

 當然這個壓縮器做的還很粗糙,需要進行優化,比如說讀寫檔案的速度問題,一個位元組一個位元組的讀,感覺很慢。還有就是我有時候壓縮後的檔案比原始檔還大,這是由於我寫入了哈夫曼編碼表,有時候就比原始檔大了。後續優化好了再放程式碼出來