1. 程式人生 > >哈夫曼編碼實現檔案的壓縮和解壓

哈夫曼編碼實現檔案的壓縮和解壓

哈夫曼編碼的概念

哈夫曼編碼是基於哈夫曼樹實現的一種檔案壓縮方式。
哈夫曼樹:一種帶權路徑最短的最優二叉樹,每個葉子結點都有它的權值,離根節點越近,權值越小(根節點權值為0,往下隨深度增加依次加一),樹的帶權路徑等於各個葉子結點的數值與其權值的乘積和。哈夫曼樹如圖:
在這裡插入圖片描述
從圖中我們可以看出,資料都存放在葉子結點中,且為了達到樹的帶權路徑最短,我們把數值大的節點放在靠近根的位置,這棵樹的帶權路徑長度為:23+53+72+131=48。接下來我們為每個節點賦予哈夫曼編碼,假設從根節點出發,到左子樹獲得編碼0,到右子樹獲得編碼1,這樣我們可以得到D的編碼是0,B的編碼是10,C的編碼是110,A的編碼是111。離根越近的節點對應的編碼越短,節點的數值越大。那麼,如何把哈夫曼編碼應用在文件的壓縮上呢?我們記檔案中字元出現的次數為節點的數值,出現次數最多的字元會分配到哈夫曼樹的靠近根節點的地方,自然也就會獲得較短的哈夫曼編碼。於是我們通過這種方式,使得文件中的字元獲得不同的哈夫曼編碼,因為出現頻次高的字元對應編碼較短,所以從文件中獲取的位元組被哈夫曼編碼替換之後,會獲使得其佔用的總儲存空間變小,實現壓縮的效果。

實現哈夫曼壓縮和解壓的步驟詳解

建立哈夫曼樹:
1、使用IO流逐位元組讀取TXT文件。用一個數組(0~255,下標表示ASCII碼)來儲存不同字元出現的次數(對應位置加一)。
2、建一個節點類,儲存節點物件的資訊。將陣列每一位表示的字元和出現頻次存入建立的節點,把所有節點存入一個連結串列。
3、根據節點儲存的頻次值,對連結串列進行排序(從小到大)。
4、從連結串列中取出並刪除最小的兩個節點,建立一個他們的父節點,父節點不存字元,值為那兩個節點的和,把那兩個節點分別作為其左子節點和右子節點,最後把這個父節點存入連結串列。再次排序,取出並刪除最小的兩個節點,生成父節點,再存入…以此類推,最終生成一棵哈夫曼樹。
5、對哈夫曼樹進行遍歷,使得葉子結點獲得相應編碼,同時把字元和它對應的哈夫曼編碼存入HashMap。
哈夫曼壓縮的實現:


1、再次讀取原文件(之前第一次讀取只是為了獲取HashMap),根據HashMap中的字元與編碼的鍵值對把整個文件轉化為一串01碼(此處可以用01字串表示)。
2、準備將資料寫入要壓縮的目錄。首先把HashMap寫入(如果壓縮檔案中沒有HashMap的資訊,在解壓的時候將無法還原)。HashMap包括兩個部分,一部分是key值(即字元),佔一個位元組,另一部分是01字串編碼,若轉為位元組表示,可能小於8位有可能大於8位(即長度不確定),我們在寫入時必須明確每個01串佔據的位元組個數,再者,因為我們是以位元組的形式寫資料,寫資料的時候總位數應是8的整數倍,需要對01串末尾補0。我們具體是這樣寫HashMap的:寫鍵值對的數量(佔一個位元組);寫key值(把字元轉為ASCII值寫入,佔一個位元組);寫01碼佔幾個位元組(是補0後的位元組數,此資訊佔一個位元組);寫補0情況(某位補0數,此處也佔一個位元組),寫補零後的01碼對應的若干位元組。繼續下一個鍵值對的寫入…以此類推,直到整個HashMap的鍵值對都寫完。
3、剛才寫的是編碼資訊,接下來準備把整個原文件轉換得到的01串寫入,這也是我們之後需要還原的資訊。剛才的流沒有關閉,我們是繼續寫入的。因為這依然會遇到最後一個位元組不足8位的情況,我們需要補0並記錄補0情況。先寫整個文件的補0情況(一個位元組),再把補0後的01串以每8位為一個位元組寫入壓縮檔案。
4、以上操作便實現了哈夫曼壓縮。另外需要注意的是,IO流的read()和write()方法是對位元組進行讀寫,如果寫的是int型別的資料,那麼它表示的是相應的ASCII碼值,如果寫入的是字元,也是會轉化為對應的位元組的(0~255個字元都有對應的ASCII碼,也都有對應的位元組表示)。
壓縮格式如圖

在這裡插入圖片描述
解壓的實現:
1、先讀取第一個位元組,即編碼個數,確定了我們需要讀多少組資料。
2、開始正式讀取鍵值對資訊。讀取key值,讀取01碼對應的位元組數,讀取補0情況,再讀取表示01串的位元組資料,去掉之前補的0,還原回0和1表示的字串,即字元對應的哈夫曼編碼,把讀到的字元和哈夫曼編碼儲存在一個新建的HashMap中,需要注意的是此處key值儲存為哈夫曼編碼,value值儲存為字元的資訊。以此類推,直到讀完所有鍵值對資訊。
3、讀整個檔案補0個數,讀取檔案位元組資料,去掉補的0,得到之前存入的哈夫曼編碼01字串。
4、確定希望解壓的檔案目錄。逐位讀取01字串,將讀到的位累加在一個臨時字串中,每讀一位都拿這個臨時字串和HashMap進行對照,如果有對應key值,則獲取對應字元資訊寫入流,把字串置空,繼續迴圈累加新的01串。最終讀完後,解壓目錄中便得到了我們解壓後的檔案。

程式碼實現

1、節點類:

public class Node<T> implements Comparable<Node<T>>{
	private T data;
	private int weight;
	private Node<T> left;
	private Node<T> right;	
	public Node(T data,int weight)
	{
		this.data=data;
		this.weight=weight;
	}		
	/**
	 * 獲取節點資料
	 */
	public String toString()
	{
		return "data:"+data+"   "+"weight:"+weight;
	}
	/**
	 * 節點權值比較方法
	 * @param o
	 * @return
	 */
	public int compareTo(Node<T> o) {       
		if(this.weight>o.weight)
			return 1;
		else if(this.weight<o.weight)
			return -1;
		return 0;
	}	
	public void setData(T data)
	{
		this.data=data;
	}	
	public void setWeight(int weight)
	{
		this.weight=weight;
	}	
	public T getData()
	{
		return data;
	}	
	public int getWeight()
	{
		return weight;
	}	
	public void setLeft(Node<T> node)
	{
		this.left=node;
	}	
	public void setRight(Node<T> node)
	{
		this.right=node;
	}	
	public Node<T> getLeft()
	{
		return this.left;
	}	
	public Node<T> getRight()
	{
		return this.right;
	}		
}

2、mian方法入口及建樹的方法

public class HFMcompression {	
	public static void main(String[] args)
	{
		HFMcompression hc = new HFMcompression();
		File file  = new File("E:\\workspace\\mayifan\\src\\com\\myf\\HFMcompression1223\\data1.txt");//原始檔地址	
		FileOperation fo = new FileOperation();
		int [] a = fo.getArrays(file);		
		System.out.println(Arrays.toString(a)); //列印		
		LinkedList<Node<String>> list = hc.createNodeList(a);//把陣列的元素轉為節點並存入連結串列			
		for(int i=0;i<list.size();i++)
		{
			System.out.println(list.get(i).toString());
		}
		Node<String> root = hc.CreateHFMTree(list); //建樹		
		System.out.println("列印整棵樹、、、、");
		hc.inOrder(root); //列印整棵樹
		System.out.println("獲取葉子結點哈夫曼編碼");
		HashMap<String,String> map = hc.getAllCode(root);//獲取字元編碼HashMap          
		String str = fo.GetStr(map, file);
		System.out.println("轉化得到的01字串:"+str);
	        File fileCompress = new File("E:\\workspace\\mayifan\\src\\com\\myf\\HFMcompression1223\\data2.zip");//壓縮檔案地址
		fo.compressFile(fileCompress,map,str);  //生成壓縮檔案
		File fileUncompress = new File("E:\\workspace\\mayifan\\src\\com\\myf\\HFMcompression1223\\data3.txt");//壓縮檔案地址
		fo.uncompressFile(fileCompress,fileUncompress);//解壓檔案至fileUncompress處
	}	
	/**
	 * 把獲得的陣列轉化為節點並存在連結串列中
	 * @param arrays
	 * @return
	 */
	public LinkedList<Node<String>> createNodeList(int[] arrays)
	{
		LinkedList<Node<String>> list = new LinkedList<>();
		for(int i=0;i<arrays.length;i++)
		{
			if(arrays[i]!=0)
			{
				String ch = (char)i+"";
				Node<String> node = new Node<String>(ch,arrays[i]); //構建節點並傳入字元和權值
				list.add(node); //新增節點
			}
		}
		return list;
	}		
	/**
	 * 對連結串列中的元素排序
	 * @param list
	 * @return
	 */
	public void sortList(LinkedList<Node<String>> list)
	{
		for(int i=list.size();i>1;i--)
		{
			for(int j=0; j<i-1;j++)
			{
				Node<String> node1 = list.get(j);
				Node<String> node2 = list.get(j+1);
				if(node1.getWeight()>node2.getWeight())
				{
					int temp ;					
					temp = node2.getWeight();
					node2.setWeight(node1.getWeight());
					node1.setWeight(temp);
					String tempChar;
					tempChar = node2.getData();
					node2.setData(node1.getData());
				        node1.setData(tempChar);
				        Node<String> tempNode = new Node<String>(null, 0);
				        tempNode.setLeft(node2.getLeft());
				        tempNode.setRight(node2.getRight());
				        node2.setLeft(node1.getLeft());
				        node2.setRight(node1.getRight());
				        node1.setLeft(tempNode.getLeft());
				        node1.setRight(tempNode.getRight());
				}
			}			
		}
	}				
	/**
	 * 建樹的方法
	 * @param list
	 */
	public Node<String> CreateHFMTree(LinkedList<Node<String>> list)
	{
		while(list.size()>1)
		{
			  sortList(list); //排序節點連結串列
			  Node<String> nodeLeft = list.removeFirst();
			  Node<String> nodeRight = list.removeFirst();
			  Node<String> nodeParent = new Node<String>( null ,nodeLeft.getWeight()+nodeRight.getWeight());			  
			  nodeParent.setLeft(nodeLeft);
			  nodeParent.setRight(nodeRight);
			  list.addFirst(nodeParent);
		}
		System.out.println("根節點的權重:"+list.get(0).getWeight());
		return list.get(0);//返回根節點
	}		
	public HashMap<String, String> getAllCode(Node<String> root)
	{
		HashMap<String, String> map = new HashMap<>();
		inOrderGetCode("", map, root);
		return map;
	}	
	/**
	 * 查詢指定字元的哈夫曼編碼(中序遍歷)
	 * @param code
	 * @param st
	 * @param root
	 * @return
	 */
	public void inOrderGetCode(String code ,HashMap<String, String> map,Node<String> root)
	{
		if(root!=null)
		{
			inOrderGetCode(code+"0",map,root.getLeft());						
			if(root.getLeft()==null&&root.getRight()==null)//儲存葉子結點的哈夫曼編碼
			{		
				System.out.println(root.getData());
				System.out.println(code);
				map.put(root.getData(), code);
			}            
			inOrderGetCode(code+"1",map,root.getRight());			
		}				
	}	
	/**
	 * 中序遍歷輸出整棵樹
	 * @param root
	 * @return
	 */
	public void inOrder(Node<String> root)
	{
		if(root!=null)
		{
			inOrder(root.getLeft());			
			if(root.getData()!=null)
                        System.out.println(root.getData());            
			inOrder(root.getRight());			
		}				
	}	
}

3、檔案操作類(包括檔案壓縮對外的介面和檔案解壓對外的介面):

public class FileOperation {
	FileOutputStream fos;//申明檔案輸出流物件
	FileInputStream fis; //申明檔案寫入流物件		
	/**
	 * 通過檔案獲取陣列的方法
	 * @param str
	 */
	public int[] getArrays(File file)
	{
		int[] arrays = new int[256];
		try{
			FileInputStream fis = new FileInputStream(file);
			int ascii=0;
			while((ascii=fis.read())!=-1)
			{
				arrays[ascii]++;
			}
		    fis.close();
		}catch(IOException e){
			e.printStackTrace();
		}
		return arrays;
	}		
	/**
	 * 讀取檔案獲取01碼
	 */
	public String GetStr(HashMap map,File file)
	{
		String str="";   //定義字串儲存01碼
		try{						
			FileInputStream fis = new FileInputStream(file);
			int value=0;			
			while((value=fis.read())!=-1)
			{
				str+=map.get((char)value+"");  //取單字元對應的01碼,累加到字串中
			}
			fis.close();
		}catch(IOException e)
		{
			e.printStackTrace();
		}
		return str;
	}				
	/**
	 * 寫HashMap到檔案(寫入編碼個數+第一個key+第一個value所佔位元組數+value最後一個位元組的補0情況+第一個value的若干位元組+下一個key+。。。。)
	 */
    public void writeHashMap(HashMap<String, String> map ,File file)
    {
    	int size = map.size(); //獲取編碼的個數,即HashMap中的鍵值對個數
    	String temp=""; //存放臨時8位01字串
    	int value=0; //存放01字串轉化得到的ASCII值
    	try{
	    	fos = new FileOutputStream(file);
	    	fos.write(size);  //寫HashMap長度
            Set<String> keySet = map.keySet(); //獲取HashMap存放key的容器
            java.util.Iterator<String> it = keySet.iterator();//通過容器獲取迭代器
	    	while(it.hasNext()) //迭代判斷,有下一個key
	    	{
	    		String key = it.next(); //取出下一個key
	    		String code = map.get(key); //取出code
	    		fos.write(key.charAt(0)); //寫key值
	    		int a = code.length()/8;//能存滿的位元組數
	    		int b = code.length()%8;//剩餘的位數
	    		int c =1; //值對應的儲存的位元組數
	    		if(b==0) //無剩餘位
	    		{
	    			c=a;
	    			fos.write(c);  //寫code的位元組數
	    			fos.write(0);  //寫補0數,為0個
	    		    for(int i=0;i<a;i++) //寫code值
	    		    {
	    		    	temp="";
	    		    	for(int j=0;j<8;j++) 
	    		    	{
	    		    		temp+=code.charAt(i*8+j);
	    		    	}
	    		    	value=StringToInt(temp);
	    		    	fos.write(value); //逐一把code的每一位寫出去
	    		    }
	    		}
	    		else 
	    		{
	    		    c=a+1;
	    		    fos.write(c); //寫code的位元組數
	    		    fos.write(8-b); //寫補0數
	    		    for(int i=0;i<8-b;i++) //補0
	    		    {
	    		    	code+="0";
	    		    }
	    		    for(int i=0;i<c;i++)
	    		    {
	    		    	temp="";
	    		    	for(int j=0;j<8;j++)
	    		    	{
	    		    		temp+=code.charAt(8*i+j);
	    		    	}
	    		    	value=StringToInt(temp);
	    		    	fos.write(value); //逐一寫code,包括補的0
	    		    }	    	
	    		}	    			    		
	    	}
    	}catch(IOException e){
    		e.printStackTrace();
    	}
    }	
    /**
     * 把文件轉化為的HFM編碼寫入檔案
     */
	public void writeHFMcode(String HFMcode)
	{
		int len = HFMcode.length();  //獲取HFMcode長度
		int a = len/8;   //求出完整的位元組的數目
		int b = len%8;   //求出剩餘的位數
		String temp = ""; //臨時存放8位資料
		int value = 0; //存放8位01轉化得到的值
		try
		{
			if(b==0)  //無不足八位的部分,不需要補0
			{
				fos.write(0); //寫補0數
				for(int i=0;i<a;i++)
				{
					temp="";
					for(int j=0;j<8;j++)
					{								
						temp+=HFMcode.charAt(i*8+j);						
					}
					value=StringToInt(temp);
					fos.write(value); //寫HFMcode
				}
			}
			else   //需要補0
			{
				int c = 8-b; //計算補0數
				fos.write(c); //寫補0數
				for(int i=0;i<c;i++) //補0
				{
					HFMcode+="0";
				}
				for(int i=0;i<a+1;i++) 
				{
					temp="";
					for(int j=0;j<8;j++)
					{
						temp+=HFMcode.charAt(i*8+j);
					}
					value=StringToInt(temp);
					fos.write(value); //寫HFMcode
				}
			}
			fos.close(); //寫完關閉資源
		}
		catch(IOException e)
		{
			e.printStackTrace();
		}		
	}	
	/**
	 * 把01字串轉化為ASCII碼
	 * @param temp
	 * @return
	 */
	public int StringToInt(String temp)
	{
		int value=0;
		for(int i=0;i<8;i++)
		{
			int x = temp.charAt(i)-48;
			if(x==1)    //為1則累加入value
			{
				value+=Math.pow(2,7-i);  //表示2的(7-i)次方
			}
		}
		return value;
	}	
	/**
	 * 把數值轉化為01字串
	 * @param value
	 */
	public String IntToString(int value)
	{
		String temp1=""; //存放反的字串
		String temp="";  //存放正的字串
		while(value>0) //逐漸取出各個二進位制位數,字串為反向的
		{
			temp1+=value%2;
			value=value/2;
		}		
		for(int i=temp1.length()-1;i>=0;i--)
		{
			temp+=temp1.charAt(i);
		}
		return temp;
	}	
	/**
	 * 把數值轉化為01字串,數值範圍在0~255,01串不超過8位
	 * @param value
	 */
	public String IntToStringEight(int value)
	{
		String temp1=""; //存放反的字串
		String temp="";  //存放正的字串
		int add=0;
		while(value>0) //逐漸取出各個二進位制位數,字串為反向的
		{
			add++;
			temp1+=value%2;
			value=value/2;
		}	
		add=8-add;
		for(int i=0;i<add;i++)//添0至8位
		{
			temp1+="0";
		}
		for(int i=temp1.length()-1;i>=0;i--) //反向的字串獲取正向的字串
		{
			temp+=temp1.charAt(i);
		}
		return temp;
	}						
	/**
	 * 對外部的介面,實現把壓縮後的資料和資訊寫入壓縮檔案
	 * @param fileCompress
	 */
	public void compressFile(File fileCompress,HashMap<String, String> map,String HFMcode)
	{
		writeHashMap(map, fileCompress);  //寫HashMap的資料
		writeHFMcode(HFMcode); //繼續寫HFMcode 01字串
	}	
	/**
	 * 解壓獲取HashMap
	 * @param fileCompress
	 */
	public HashMap<String, String> readHashMap(File fileCompress)
	{
		HashMap<String, String> mapGet = new HashMap<>();
		try
		{
			fis=new FileInputStream(fileCompress); 
			int keyNumber = fis.read(); //讀取key的數量
			String key = ""; //HashMap的鍵值對
			String code= ""; //未去0的字串
			String codeRZ="";//去0的字串
			int length=0; //表示還原後的字串的理論長度,解決字串前面的0的問題
			int byteNum=1; //當前code佔了幾個位元組
			int addZero=0; //補0數
			int value=0; //臨時儲值
			int zeroLength=0;//code沒有1的時候的字串長度
		    for(int i=0;i<keyNumber;i++)
		    {
		    	key = (char)fis.read()+""; //獲取key值
		    	byteNum=fis.read(); //獲取code的位元組數
		    	addZero=fis.read(); //讀取補0數量
		    	if(addZero==0) //沒有補0,是整位元組數
		    	{
		    		for(int k=byteNum-1;k>=0;k--)
		    		{
		    			value+=fis.read()*(Math.pow(2, k*8));
		    		}
		    		code=IntToString(value);//把數值轉為01code
		    		value=0;//清零
		    		length=8*byteNum-code.length();//計算在前面要補多少0
		    		if(code.length()==0)  //若code內數字都為0,只要去掉尾部即可
		    		{
		    			zeroLength=length-addZero;  //計算有多少個0
		    			for(int k=0;k<zeroLength;k++)
		    			{
		    				codeRZ+="0";
		    			}
		    		}
		    		else    //code值不為0,補充前面的0,去掉後面的0
		    		{
			    		for(int k=0;k<length;k++)
			    		{
			    			codeRZ+="0";
			    		}
			    		for(int k=0;k<code.length()-addZero;k++)
			    		{
			    			codeRZ+=code.charAt(k);
			    		}	
		    		}
		    	}
		    	else  //有補0
		    	{
		    		for(int k=byteNum-1;k>=0;k--)
		    		{
		    			value+=fis.read()*(Math.pow(2, k*8));
		    		}
		    		code=IntToString(value);//把數值轉為01code
		    		value=0;//清0
		    		length=8*byteNum-code.length();//計算在前面要補多少0	    		
		    		if(code.length()==0)  //若code內數字都為0,只要去掉尾部即可
		    		{
		    			zeroLength=length-addZero;  //計算有多少個0
		    			for(int k=0;k<zeroLength;k++)
		    			{
		    				codeRZ+="0";
		    			}
		    		}
		    		else   //code值不為0,補充前面的0,去掉後面的0
		    		{
			    		for(int k=0;k<length;k++)
			    		{
			    			codeRZ+="0";
			    		}
			    		for(int k=0;k<code.length()-addZero;k++) //不要後面的0
			    		{
			    			codeRZ+=code.charAt(k);
			    		}
		    		}		    		
		    	}		
		    	mapGet.put(codeRZ , key ); //把讀取到的鍵值對存入建立的HashMap
		    	codeRZ=""; //清空
		    }
		}
		catch(IOException e)
		{
			e.printStackTrace();
		}
		return mapGet;
	}	
	/**
	 * 獲取壓縮檔案中的資料,還原哈夫曼編碼01串
	 */
	public String readHFMStr()
	{
		String str1=""; //存放獲取到的直接的01字串
		String str=""; //存放去掉補0的字串
		int value=0;
		String temp="";
		try{
			int addZero = fis.read(); //讀取整個檔案的補0個數			
			while((value=fis.read())!=-1)
			{
				temp=IntToStringEight(value); //把每個位元組的資料轉化為八位的01
				str1+=temp;       
			}
			if(addZero!=0) //有補0,獲取補0前的字串
			{
				for(int i=0;i<str1.length()-addZero;i++) //補0的部分不賦值
					str+=str1.charAt(i)+""; 
				return str;  
			}
			fis.close();
		}
		catch(IOException e)
		{
			e.printStackTrace();
		}	
		return str1;
	}	
	/**
	 * 寫入檔案的儲存路徑(寫檔案)
	 * @param str
	 * @param mapGet
	 * @param fileCompress
	 */
	public void writeFile(String str , HashMap<String, String> mapGet,File fileCompress)
	{
		try
		{
			fos = new FileOutputStream(fileCompress); //獲取檔案輸出流
			int len = str.length();//獲取01串的長度
			String temp=""; //臨時存放段的01字串
			for(int i=0;i<len;i++)
			{
				temp+=str.charAt(i);
				if(mapGet.containsKey(temp))
				{
					fos.write(mapGet.get(temp).charAt(0)); //一個字元的字串轉字元然後寫出
					temp="";					
				}
			}
			fos.close();
		}
		catch(IOException e)
		{
			e.printStackTrace();
		}
	}	
	/**
	 * 對外部的介面,實現解壓檔案,獲取HashMap和檔案內容
	 * @param fileCompress,壓縮檔案目錄
	 * @param fileUncompress,解壓到的目錄
	 */
	public void uncompressFile(File fileCompress,File fileUncompress)
	{
		HashMap<String, String> mapGet = readHashMap(fileCompress); //獲取雜湊表
		String str = readHFMStr();  //獲取01字串
		writeFile(str,mapGet,fileUncompress);  //寫檔案到儲存路徑
	}				
}

壓縮、解壓效果

1、壓縮檔案所佔記憶體小於原檔案,解壓後的檔案和原檔案大小相同。如圖data1是原檔案,data2是壓縮檔案,data3的解壓後的檔案。我們可以發現壓縮後的壓縮包所佔記憶體3KB<5KB。
在這裡插入圖片描述
2、原檔案和解壓後的檔案的內容展示:
data1.txt:
在這裡插入圖片描述
data3.txt:
在這裡插入圖片描述
解壓後txt的資訊和原檔案完全一致。