哈夫曼编码原理以及实现
哈夫曼编码的主要用途:
哈夫曼编码主要用于数据压缩,通常可以节省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);
}
}版权声明:本文为u014276600原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。