说到哈夫曼编码,我们先了解一下什么是哈夫曼树:
哈夫曼树(Huffman Tree)是在叶子结点和权重确定的情况下,带权路径长度最小的二叉树,也被称为最优二叉树。
故依据哈夫曼树的带权路径长度最小的这个特点,我们可以实现对文件进行压缩。
一般情况下我们考虑对文本文件进行压缩,而很少考虑对图片、音频等进行压缩,对于此的原因这里我总结了几点如下:
- 文本文件中多是文字信息,而对于文字来说在目前一般的编码方式里(如utf-8)是以两个字节即16个bit二进制来进行存储的,故在编码时若文字字符对应的编码串长度小于16就能实现压缩
- 每个人写作都有一定的习惯,比如说我们总是在不经意间习惯性的对某些文字的使用频率大大高于其他文字的使用频率,同时经常使用的,这样就更有利于我们建立更短权值路径的哈夫曼树
- 对于图片、音频等文件,都是使用一个字节一个字节的进行存储,故要做到压缩,需要在编码时其对应的编码串长度小于8才能实现压缩的目的
- 图片、音频等文件,其文件中各字节出现的频次都非常接近,而这一点是非常不利于我们期望达成的压缩目的的
下面我们以一段代码来统计一下一个视频文件中各个字节出现的频次
public class Demo {
public static void main(String[] args) throws IOException {
FileInputStream fis=new FileInputStream("C:\\Users\\zyx\\Desktop\\4.mp4");
FileOutputStream fos=new FileOutputStream("C:\\Users\\zyx\\Desktop\\1.mp4");
int len;
byte[] buf=new byte[1024];
HashMap<Byte,Integer> map=new HashMap<>();
while((len=fis.read(buf))!=-1) {
for(int i=0;i<len;i++) {
if(map.get(buf[i])==null) {
map.put(buf[i], 1);
}else {
map.put( buf[i], map.get(buf[i])+1);
}
}
}
System.out.println(map);
}
}
运行结果如下:
可以明显的看到除了0以外的各个字节出现的频次都非常接近,故猜测我们在对这类文件进行编码压缩时,最终结果在很大概率上压缩效率会很低,甚至还可能发生膨胀的情况,这与我们期望的压缩的目的就背道而驰了。
了解了选择.txt文本文件的原因后,我们再来了解一下哈夫曼树的建树过程:
- 初始化各节点的权值信息
- 对节点集合进行排序,每次移除权值最小的两个节点,通过这两个节点生成一个父节点,父节点的权值等于两个子节点的权值之和,并分别指向这两个子节点,最后将父节点加入到节点集合中
- 重复2过程,直到节点数组中只剩一个节点,此节点就作为哈夫曼树的根节点
补充:可以发现在实际的压缩过程中,建树环节是最消耗时间的一块,因此对于上面的2过程,推荐使用快速排序的方式,这样相比于普通的冒泡排序的方式,效率的提升就非常明显了
看一下冒泡和快排部分的代码:
public void sort() {
Node dem;
for(int i=0;i<nodes.size()-1;i++) {
for(int j=0;j<nodes.size()-1-i;j++) {
if(nodes.get(j).weight>nodes.get(j+1).weight) {
dem=nodes.get(j);
nodes.set(j, nodes.get(j+1));
nodes.set(j+1, dem);
}
}
}
}
public void quickSort(int begin,int end) {
if(begin<end) {
Node temp=nodes.get(begin);
int i=begin;
int j=end;
while(i<j) {
while(i<j&&nodes.get(j).weight>temp.weight)
j--;
nodes.set(i, nodes.get(j));
while(i<j&&nodes.get(i).weight<=temp.weight)
i++;
nodes.set(j, nodes.get(i));
}
nodes.set(i, temp);
quickSort(begin, i-1);
quickSort(i+1, end);
}
}
上面的是普通冒泡排序的代码,下面的是快速排序的代码
下面通过程序运行来测试两种排序在压缩一个413KB的文本文件的耗时,运行结果如下:
冒泡的运行结果:
快排的运行结果:
由运行结果可见,由冒泡排序改为快排后效率的提升约8倍有余
在看完前言后,下面进入正式部分
一、实现思路
1、压缩:
- 通过IO流读取统计文本文件中每个字符出现的次数,将出现的次数作为权重初始化LinkedList节点集合(因当前程序中存在频繁的删除操作,故使用LinkedList的效率优于ArrayList)
- 建树过程采用快排
- 使用StringBuilder记录总编码串,通过余运算得出需要补"0"的个数给编码串补上相应的"0"的个数使其能被8整除,将StringBuilder转化为一系列长度为8的String字符串,并通过自定义方法将其转换为相应的字节
- 二叉树遍历以及编码方案
- 读入一个需要压缩的文本文件,输出一个配置文件和一个压缩文件,配置文件用于存储保存了键值对的HashMap对象,压缩文件则存储压缩后的编码信息以及补0的个数的标记信息
2、解压
- 自定义负数字节转换为相应的int值的方法
- 通过对象流读取配置文件拿到对应的HashMap对象,再通过文件流经自定义方法配合Integer类中的静态方法toBinaryString(int i)经一系列处理还原得到之前的编码串
- 将得到的编码串经HashMap中的键值对信息一一比对并经IO流写出即可一个字节不差的还原为原来的文本文件
针对效率问题的一个补充:在使用文件流读取压缩文件时,事先声明了一个长度为1024,元素类型为byte的数组,使用read(byte b[])方法,每次都读取最多1024字节的数据到字节数组,好处是减少了与操作系统交互的次数,提高了读取的效率,同时该数组的长度若过大则内容占用也会较大也会影响效率,因此数组的长度适中即可
3、编码器界面
- 主要使用到java类库中的JFileChooser类分别完成对文本文件路径的选取,以及输出目录路径的选择
- 完成一定的布局,添加相应的监听器功能
- 对输入输出路径是否正确的检测以及调用压缩和解压两个类中的关键方法,成功压缩或解压弹出提示框
二、源码部分
1、Hfm类(压缩):</