Java小程序之哈夫曼树与文件压缩和解压缩(三)文件解压篇
一、解压原理:
了解了压缩原理之后,要解压文件就是压缩文件的逆过程;拿昨天的例子来说,如果我们收到这样一串二进制1 1 01 1 1 01 00(昨天漏掉了一个问题,这里是9个0 1,每8个一个字节,那么剩下的那个0需要补7个0,构成一个完整的字节,这样才能写出文件)怎么解压出aabbac呢?很自然的想到,我们需要拿到对应的哈夫曼编码;a的编码是1,b的编码是01,c的编码是00;拿到这个编码后,我们开始对这个0 1串分割,先取出一个0或1,看是否有对应的编码,如上,我们先取第一个1,编码中有1的编码,对应a,那么把第一个1还原为a,接下来,再去一个0或1,得到1,编码中有1,对应a,那么还原为a,接下来去0,没有编码,取01对应b,把01还原为b......以此类推
有人会怀疑这样的正确性,不会解压错误吗?比如如果编码中有1 和11,那么11改怎么还原呢?是解析成两个1进行还原,还是解析成一个11进行还原呢?其实不用担心,这就是哈夫曼编码的优势,哈夫曼编码中不会出现这样的问题;不相信的话,你可以自己去检验下;
将这么多,其实很简单,就类似于情报的破解,我只要有密码本就可以了;而哈夫曼编码就是我们的密码本;
二、哈夫曼树文件解压实现:
文件的压缩和解压缩是两个相对独立的程序;所以,我们在把压缩数据写入文件之前,需要把该文件对应的哈夫曼编码一起写入文件,相当于解压时的密码本;
所以,昨天的压缩程序还少了一步,就是把编码写入压缩文件中;我们只需把每个字母对应的哈夫曼编码的长度以及所有字母对应的哈夫曼编码写入文件即可;
读取文件的时候,先读取每个哈夫曼编码的长度,在根据长度去分割写入的哈夫曼编码,同时把哈夫曼编码写入对应的位置即可;如上图所示,前面的96长度都是0,不需要分割哈夫曼编码;97的长度是1,则分割1,并把1存入对应的字符数组中;同时分割01,把01存储在字符数组的第98个位置;以此类推;
难点:处理不够8位01的写入,记得把补0的个数一起写入文件;
三、整个思路整理如下:
四、压缩工程源代码和解压缩工程源代码:
两个独立的工程,代码不共享;
用压缩工程压缩的文件,可以用解压缩的工程文件解压缩;
压缩工程源代码:
HuffmNode类和昨天的一样,就不上传了;
更改后的compress类:
package com.huaxin.compress;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.util.LinkedList;
public class Compress {
public int [] times = new int[256];
public String [] HuffmCodes=new String[256];
public LinkedList list = new LinkedList();
//统计次数
//初始化
public Compress(){
for (int i = 0; i < HuffmCodes.length; i++) {
HuffmCodes[i]="";
}
}
public void countTimes(String path) throws Exception{
//构造文件输入流
FileInputStream fis = new FileInputStream(path);
//读取文件
int value=fis.read();
while(value!=-1){
times[value]++;
value=fis.read();
}
//关闭流
fis.close();
}
//构造哈夫曼树
public HuffmNode createTree(){
//将次数作为权值构造森林
for (int i = 0; i < times.length; i++) {
if(times[i]!=0){
HuffmNode node = new HuffmNode(times[i],i);
//将构造好的节点加入到容器中的正确位置
list.add(getIndex(node), node);
}
}
//将森林(容器中的各个节点)构造成哈夫曼树
while(list.size()>1) {
//获取容器中第一个元素(权值最小的节点)
HuffmNode firstNode =list.removeFirst();
//获取中新的第一个元素,原来的第一个元素已经被移除了(权值次小的节点)
HuffmNode secondNode =list.removeFirst();
//将权值最小的两个节点构造成父节点
HuffmNode fatherNode =
new HuffmNode(firstNode.getData()+secondNode.getData(),-1);
fatherNode.setLeft(firstNode);
fatherNode.setRight(secondNode);
//父节点加入到容器中的正确位置
list.add(getIndex(fatherNode),fatherNode);
}
//返回整颗树的根节点
return list.getFirst();
}
//利用前序遍历获取编码表
public void getHuffmCode(HuffmNode root,String code){
//往左走,哈夫曼编码加0
if(root.getLeft()!=null){
getHuffmCode(root.getLeft(),code+"0");
}
//往右走,哈夫曼编码加1
if(root.getRight()!=null){
getHuffmCode(root.getRight(),code+"1");
}
//如果是叶子节点,返回该叶子节点的哈夫曼编码
if(root.getLeft()==null && root.getRight()==null){
// System.out.println(root.getIndex()+"的编码为:"+code);
HuffmCodes[root.getIndex()]=code;
}
}
//压缩文件
public void compress(String path,String destpath) throws Exception{
//构建文件输出流
FileOutputStream fos = new FileOutputStream(destpath);
FileInputStream fis = new FileInputStream(path);
/**===============把码表写入文件================*/
//将整个哈夫曼编码以及每个编码的长度写入文件
String code ="";
for (int i = 0; i < 256; i++) {
fos.write(HuffmCodes[i].length());
code+=HuffmCodes[i];
fos.flush();
}
//把哈夫曼编码写入文件
// System.out.println("code="+code);
String str1="";
while(code.length()>=8){
str1=code.substring(0, 8);
int c=changeStringToInt(str1);
// System.out.println(c);
fos.write(c);
fos.flush();
code=code.substring(8);
}
//处理最后一个不为8的数
int last=8-code.length();
for (int i = 0; i =8){
s=str.substring(0, 8);
int b=changeStringToInt(s);
// System.out.println(c);
fos.write(b);
fos.flush();
str=str.substring(8);
}
//最后不够8位添0
int last1=8-str.length();
for (int i = 0; i
重新测试了一个文件,Test也做了一下更改:
package com.huaxin.compress;
public class Test {
public static void main(String[] args) throws Exception {
//创建压缩对象
Compress compress = new Compress();
//统计文件中0-255出现的次数
compress.countTimes("C:\\Users\\Administrator\\Desktop\\my.docx");
//构造哈夫曼树,并得到根节点
HuffmNode root=compress.createTree();
//得到哈夫曼编码
compress.getHuffmCode(root, "");
//压缩文件
compress.compress("C:\\Users\\Administrator\\Desktop\\my.docx",
"C:\\Users\\Administrator\\Desktop\\my.docx.zip");
}
}
解压工程源代码:
package com.huaxin.decompress;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
public class Decompress {
//每个编码的长度
public int [] codelengths = new int [256];
//对应的哈夫曼编码值
public String [] codeMap=new String[256];
public static void main(String[] args) {
Decompress d = new Decompress();
d.decompress("C:\\Users\\Administrator\\Desktop\\my.docx.zip",
"C:\\Users\\Administrator\\Desktop\\mydecompress.docx");
}
/*
* 解压思路:
* 1、读取文件里面的码表
* 2、得到码表
* 3、读取数据
* 4、还原数据
*/
public void decompress(String srcpath,String destpath) {
try {
FileInputStream fis = new FileInputStream(srcpath);
FileOutputStream fos = new FileOutputStream(destpath);
int value;
int codeLength=0;
String code="";
//还原码表
for (int i = 0; i < codelengths.length; i++) {
value=fis.read();
codelengths[i]=value;
// System.out.println(times[i]);
codeLength+=codelengths[i];
}
//得到总长度
//将总长度除以8的到字节个数
int len=codeLength/8;
//如果不是8的倍数,则字节个数加1(对应压缩补0的情况)
if((codeLength)%8!=0){
len++;
}
//读取哈夫曼编码
// System.out.println("codeLength:"+len);
for (int i = 0; i < len; i++) {
//把读到的整数转换成二进制
code+=changeIntToString(fis.read());
}
// System.out.println("哈夫曼编码:"+code);
for (int i = 0; i < codeMap.length; i++) {
//如果第i个位置不为0 ,则说明第i个位置存储有哈夫曼编码
if(codelengths[i]!=0){
//将得到的一串哈夫曼编码按照长度分割分割
String ss=code.substring(0, codelengths[i]);
codeMap[i]=ss;
code=code.substring(codelengths[i]);
}else{
//为0则没有对应的哈夫曼编码
codeMap[i]="";
}
}
//读取压缩的文件内容
String codeContent="";
while(fis.available()>1){
codeContent+=changeIntToString(fis.read());
}
//读取最后一个
value=fis.read();
//把最后补的0给去掉
codeContent=codeContent.substring(0, codeContent.length()-value);
for (int i = 0; i < codeContent.length(); i++) {
String codecontent=codeContent.substring(0, i+1);
for (int j = 0; j < codeMap.length; j++) {
if(codeMap[j].equals(codecontent)){
// System.out.println("截取的字符串:"+codecontent);
fos.write(j);
fos.flush();
codeContent=codeContent.substring(i+1);
// System.out.println("截取后剩余编码长度:"+codeContent.length());
// count=1;
i=-1;
break;
}
}
}
// }
fos.close();
fis.close();
} catch (Exception e) {
e.printStackTrace();
}
}
//十进制转二进制字符串
public String changeIntToString(int value) {
String s="";
for (int i = 0; i < 8; i++) {
s=value%2+s;
value=value/2;
}
return s;
}
}
五、运行结果:字节大小相同
打开文件后文件内容相同
问题:为什么压缩后的文件反而更大了?
答:因为我们写了很多与该问价解压的内容进去;平时的压缩一般是针对大文件的;举个简单的例子,你用系统的压缩软件,压缩一个只要几个字节的文本,你会发现,压缩文件也变得比原文件更大了;
六、总结
通过这个项目,学到了一些和数据结构相关的知识,其实也算是回顾吧。毕竟自己也学过数据结构这门课程;但重要的是,通过文件压缩与解压缩这个项目,把所学的数据结构用到了项目中,这是重要的;这个项目需要自己多思考,绝不是一下就能搞明白的,尤其是自学的人;还是根据思路,慢慢琢磨,仔细明白其中的原理吧;
题外话,今天我们做了这段时间的Java项目总结;感觉自己还是需要多锻炼;上台发言还是有点紧张,语速过快;希望下次有更好的表现;在华信,在路上,让我们一起走上人生的巅峰;
共勉!