哈夫曼编码的概念
哈夫曼编码是基于哈夫曼树实现的一种文件压缩方式。
哈夫曼树:一种带权路径最短的最优二叉树,每个叶子结点都有它的权值,离根节点越近,权值越小(根节点权值为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 = (<