哈夫曼編碼原理以及實現

哈夫曼編碼原理以及實現

哈夫曼編碼的主要用途:

哈夫曼編碼主要用於數據壓縮,通常可以節省20%-90%的空間,具體的壓縮率依賴於數據的特性。下面舉個簡單的例子說明對於字符不同編碼方式所使用的空間大小。
從圖中可以看出:
1、定長編碼6個字符使用的二進制位數爲 (45*3+13*3+12*3+16*3+9*3+5*3)= 300 個二進制位來編碼文件。
2、變長編碼中6個字符使用的二進制位數爲 (45*1+13*3+12*3+16*3+9*4+5*4) = 224 個二進制位來編碼文件。
3、變長編碼與定長編碼相比節約了(300-224)/300 = 25%的空間。

哈夫曼編碼原理概述

哈夫曼編碼是有貪心算法來構造的最優前綴碼。哈夫曼編碼是通過二叉樹的形式構造表示的,其中構造出的二叉樹一定是一顆滿二叉樹。
下面簡述哈夫曼編碼的構造過程:
1、由給定的m個權值{w(1),w(2),w(3),...,w(m)},構造m課由空二叉樹擴充得到的擴充二叉樹{T(1),T(2),....T(m)}。每個T(i)(1<= i <= m)只有一個外部節點(也是根節點),它的權值置爲m(i)。概括一下就是把原先的節點封裝成二叉樹結點的形式。
2、在已經構造的所有擴充二叉樹中,選取根結點的權值最小和次最小的兩棵,將他們作爲左右子樹,構造成一棵新的擴充二叉樹,它的根結點(新建立的內部結點)的權值置爲其左、右子樹根結點權值之和。
3、重複執行步驟(2),每次都使擴充二叉樹的個數減少一,當只剩下一棵擴充二叉樹時,它便是所要構造的哈夫曼樹。
下面是哈夫曼編碼的具體操作步驟:
從圖中可以看出,每次選擇兩個最小的結點,生成新的二叉樹之後,新二叉樹的根結點重新加入到原結點序列中。


哈夫曼編碼代碼實現過程

在算法導論中,哈夫曼編碼使用優先隊列管理結點序列,如果優先隊列使用最小堆來維護,這樣哈夫曼編碼算法時間複雜度爲O(n*lgn);

在這裏我使用鏈表的形式存儲結點序列,採用時間複雜度爲O(n^2)的方式來實現,考慮到哈夫曼編碼生成的二叉樹爲滿二叉樹,而滿二叉樹中總的結點個數等於葉子結點加上內部結點的和,葉子結點又等於內部結點加上一。
所以我們可以根據已知的結點序列數,計算出總的需要使用的鏈表的空間大小 = 結點數*2 + 1 。
首先是定義的二叉樹結點的結構,具體get,set方法我就不羅列出了
public class Haff {

	private int node; // 結點的值
	private int parent; // 父結點的值
	private int llink; // 左孩子結點的值
	private int rlink; // 右孩子結點的值
	private int mark; // 標記結點下標,解碼時候方便
}
下面是用Java寫的哈夫曼編碼二叉樹的生成過程:
	public void generatorTree(List<Haff> list){		
		int length = (list.size()+1)/2;
		for (int i = 0; i < length-1; i++) {
			
			// x,y爲最小兩個數組的下標,min1,min2爲Integer類型最大值,方便比較的大小
			int min1 = MAXINT, min2 = MAXINT, x = ELEINDEX, y = ELEINDEX;
			// 找出指定鏈表長度內最小的兩個數
			for (int j = 0; j < length + i; j++) {
				if (list.get(j).getParent() == -1 && min1 > list.get(j).getNode()) {
					y = x;
					min2 = min1;
					min1 = list.get(j).getNode();
					x = j;
				} else if (list.get(j).getParent() == -1 && min2 > list.get(j).getNode()) {
					min2 = list.get(j).getNode();
					y = j;
				}
			}
			list.get(x).setParent(length + i);
			list.get(y).setParent(length + i);
			list.get(length + i).setNode(min1 + min2);
			list.get(length + i).setLlink(x);
			list.get(length + i).setRlink(y);
		}
	}



從代碼中我們可以看出,每次循環需要找到已知鏈表長度中兩個最小的結點,然後生成新的結點加入到鏈表中。

下面是根據已經生成的哈夫曼樹生成哈夫曼編碼的過程(採用遞歸的方式實現):

	利用遞歸的方法,生成HaffMan編碼
	public void generatorCode(List<Haff> list,int index,StringBuilder strb,Map<Integer,String> map){
		
		if(list.get(index).getRlink() == -1 || list.get(index).getLlink() == -1){
			map.put(list.get(index).getMark(), strb.toString());
			return;
		}
		strb.append("0");
		generatorCode(list,list.get(index).getLlink(),strb,map);
		strb.deleteCharAt(strb.length()-1);
		strb.append("1");
		generatorCode(list,list.get(index).getRlink(),strb,map);
		strb.deleteCharAt(strb.length()-1);
	}
也可以使用棧從而採用非遞歸的方式實現。

模擬哈夫曼編碼實現文件壓縮過程

首先讀取文件中的字符(這裏主要是ASCII碼中所表示的字符,Unicode中的字符集沒有考慮到),統計數據中單個字符的出現次數,並生成哈夫曼樹,遍歷哈弗曼樹生成
哈夫曼編碼,將哈夫曼編碼存放在map中,再次掃描文件,轉換原字符爲對應的哈夫曼編碼,並且模擬哈夫曼編碼的還原過程,讀取哈夫曼編碼文件,還原爲原來的文件。下面是代碼,關鍵部分都做了註釋:

package algo;

public class Haff {

	private int node;
	private int parent;
	private int llink;
	private int rlink;
	private int mark;

	public Haff(){
		this.node = 0;
		this.parent = -1;
		this.llink = -1;
		this.rlink = -1;
		this.mark = -1;
	}
	
	public int getMark() {
		return mark;
	}

	public void setMark(int mark) {
		this.mark = mark;
	}

	public int getNode() {
		return node;
	}
	public void setNode(int node) {
		this.node = node;
	}
	public int getParent() {
		return parent;
	}
	public void setParent(int parent) {
		this.parent = parent;
	}
	public int getLlink() {
		return llink;
	}
	public void setLlink(int llink) {
		this.llink = llink;
	}
	public int getRlink() {
		return rlink;
	}
	public void setRlink(int rlink) {
		this.rlink = rlink;
	}
}
package algo;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class HaffTree {
	
	private final static int[] array = new int[256];
	private final static int MAXINT = Integer.MAX_VALUE;
	private final static int ELEINDEX = -1;
	
	static {
		Arrays.fill(array, 0);
	}
	
	// 初始化列表
	public List<Haff> initTree(File file){
		
		int length = 0;
		int size = array.length;
		List<Haff> list = new ArrayList<Haff>(size);
		
		// 打開文件讀取流,統計文件中的各個字母出現的次數
		try(FileInputStream in = new FileInputStream(file)){
			int c;
			while((c = in.read()) != -1){
				array[c]++;
			}
		}catch(IOException ex){
			System.err.println(ex);
		}
		// 因爲HaffMan樹是滿二叉樹, 初始化list,鏈表的長度爲葉子節點加內部節點。內部節點等於葉子節點減一
		for (int i = 0; i < size; i++) {
			Haff haff = new Haff();
			if (array[i] != 0) {
				haff.setNode(array[i]);
				haff.setMark(i);
				list.add(haff);
				length++;
			}
		}
		for(int i = 0; i < length-1; i++){
			list.add(new Haff());
		}
		return list;
	}
	// HaffMan樹
	public Map<Integer,String> generatorTree(List<Haff> list){
		
		int length = (list.size()+1)/2;
		for (int i = 0; i < length-1; i++) {
			
			// x,y爲最小兩個數組的下標,min1,min2爲Integer類型最大值,方便比較的大小
			int min1 = MAXINT, min2 = MAXINT, x = ELEINDEX, y = ELEINDEX;
			// 找出指定鏈表長度內最小的兩個數
			for (int j = 0; j < length + i; j++) {
				if (list.get(j).getParent() == -1 && min1 > list.get(j).getNode()) {
					y = x;
					min2 = min1;
					min1 = list.get(j).getNode();
					x = j;
				} else if (list.get(j).getParent() == -1 && min2 > list.get(j).getNode()) {
					min2 = list.get(j).getNode();
					y = j;
				}
			}
			list.get(x).setParent(length + i);
			list.get(y).setParent(length + i);
			list.get(length + i).setNode(min1 + min2);
			list.get(length + i).setLlink(x);
			list.get(length + i).setRlink(y);
		}
		StringBuilder strb = new StringBuilder();
		Map<Integer,String> map = new HashMap<Integer,String>();
		// 根據HaffMan樹,生成HaffMan編碼
		generatorCode(list,list.size()-1,strb,map);
		
		return map;
	}
	
	// 利用遞歸的方法,生成HaffMan編碼
	public void generatorCode(List<Haff> list,int index,StringBuilder strb,Map<Integer,String> map){
		
		if(list.get(index).getRlink() == -1 || list.get(index).getLlink() == -1){
			map.put(list.get(index).getMark(), strb.toString());
			return;
		}
		strb.append("0");
		generatorCode(list,list.get(index).getLlink(),strb,map);
		strb.deleteCharAt(strb.length()-1);
		strb.append("1");
		generatorCode(list,list.get(index).getRlink(),strb,map);
		strb.deleteCharAt(strb.length()-1);
	}
	// 根據HaffMan編碼生成新的文件
	public void getNewFile(File inFile,File outFile,Map<Integer,String> map){
			
		try(FileInputStream in = new FileInputStream(inFile);
			FileOutputStream out = new FileOutputStream(outFile)){
			int c;
			while((c = in.read()) != -1){
				if(map.containsKey(c)){
					out.write(map.get(c).getBytes());
				}
			}
		}catch(IOException ex){
			System.err.println(ex);
		}
	}
	// 還原HaffMan編碼
	public void enGeneratorCode(File file,File inFile,List<Haff> list){

		// 打開要讀取和寫入的文件
		try(FileInputStream in = new FileInputStream(inFile);
				FileOutputStream out = new FileOutputStream(file)){
			int temp = 0,index = list.size()-1,node = 0;
			// 從根節點根據讀入的字符遍歷
			while((node = in.read()) != -1){

				temp = getNextNode(list, index, node);
				if(list.get(temp).getLlink() == -1){

					out.write((char)list.get(temp).getMark());
					index = list.size()-1;
				}else{
					index = temp;
				}
			}
		}catch(IOException ex){
			System.err.println(ex);
		}
	}
	
	public int getNextNode(List<Haff> list,int index,int node){
		if(node == 48){
			return list.get(index).getLlink();
		}else{
			return list.get(index).getRlink();
		}
	}
}
package algo;

import java.io.File;
import java.util.List;
import java.util.Map;

public class readFile {

	public static void main(String[] args){
		
		HaffTree haffTree = new HaffTree();
		File inFile = new File("E:/user.txt");
		File outFile = new File("E:/userCode.txt");
		File file = new File("E:/default.txt");
		
		// 獲得HaffMan編碼
		List<Haff> list = haffTree.initTree(inFile);
		Map<Integer,String> map = haffTree.generatorTree(list);
		
		// 根據HaffMan編碼輸出新的文件
		haffTree.getNewFile(inFile, outFile,map);
		
		// 還原HaffMan編碼
		haffTree.enGeneratorCode(file, outFile, list);
	}
}




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章