哈夫曼编码原理以及实现

哈夫曼编码原理以及实现

哈夫曼编码的主要用途:

哈夫曼编码主要用于数据压缩,通常可以节省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);
	}
}




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