一、解压原理:
了解了压缩原理之后,要解压文件就是压缩文件的逆过程;拿昨天的例子来说,如果我们收到这样一串二进制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<HuffmNode> list = new LinkedList<HuffmNode>();
- //统计次数
- //初始化
- 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 <last; i++) {
- code+="0";
- }
- str1=code.substring(0, 8);
- int c=changeStringToInt(str1);
- fos.write(c);
- fos.flush();
- /**===============将数据写入到文件中================*/
- //读文件,并将对应的哈夫曼编码串接成字符串
- int value=fis.read();
- String str = "";
- while(value!=-1){
- str+=HuffmCodes[value];
- // System.out.println((char)value+":"+str);
- value=fis.read();
- }
- System.out.println(str);
- fis.close();
- String s="";
- //将字符8位分割,对弈一个字节
- while(str.length()>=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 <last1; i++) {
- str+="0";
- }
- s=str.substring(0, 8);
- // System.out.println(s);
- int d=changeStringToInt(s);
- fos.write(d);
- //压缩后不够补0的个数暂
- fos.write(last1);
- fos.flush();
- fos.close();
- }
- //插入元素位置的索引
- public int getIndex(HuffmNode node) {
- for (int i = 0; i < list.size(); i++) {
- if(node.getData()<=list.get(i).getData()){
- return i;
- }
- }
- return list.size();
- }
- //将字符串转换成整数
- public int changeStringToInt(String s){
- int v1=(s.charAt(0)-48)*128;
- int v2=(s.charAt(1)-48)*64;
- int v3=(s.charAt(2)-48)*32;
- int v4=(s.charAt(3)-48)*16;
- int v5=(s.charAt(4)-48)*8;
- int v6=(s.charAt(5)-48)*4;
- int v7=(s.charAt(6)-48)*2;
- int v8=(s.charAt(7)-48)*1;
- return v1+v2+v3+v4+v5+v6+v7+v8;
- }
- }
重新测试了一个文件,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;
- }
- }
五、运行结果:字节大小相同

打开文件后文件内容相同


问题:为什么压缩后的文件反而更大了?

答:因为我们写了很多与该问价解压的内容进去;平时的压缩一般是针对大文件的;举个简单的例子,你用系统的压缩软件,压缩一个只要几个字节的文本,你会发现,压缩文件也变得比原文件更大了;
public class HuffmanCompress { // 压缩函数 public void compress(File inputFile, File outputFile) { Compare cmp = new Compare(); PriorityQueue<HufTree> queue = new PriorityQueue<HufTree>(12, cmp); // 映射字节及其对应的哈夫曼编码 HashMap<Byte, String> map = new HashMap<Byte, String>(); // 文件中含有的字符的种类数 int i, char_kinds = 0; int char_temp, file_len = 0; FileInputStream fis = null; FileOutputStream fos = null; ObjectOutputStream oos = null; // 哈夫曼树节点个数 int node_num; HufTree[] huf_tree = null; String code_buf = null; // 临时存储字符频度的数组 TmpNode[] tmp_nodes = new TmpNode[256]; for (i = 0; i < 256; ++i) { tmp_nodes[i] = new TmpNode(); tmp_nodes[i].weight = 0; tmp_nodes[i].uch = (byte) i; } try { fis = new FileInputStream(inputFile); fos = new FileOutputStream(outputFile); oos = new ObjectOutputStream(fos); // 统计字符频度,计算文件长度 while ((char_temp = fis.read()) != -1) { ++tmp_nodes[char_temp].weight; ++file_len; } fis.close(); Arrays.sort(tmp_nodes); // 排序后就会将频度为0的字节放在数组最后,从而去除频度为0的字节 // 同时计算出字节的种类 for (i = 0; i < 256; ++i) { if (tmp_nodes[i].weight == 0) break; } char_kinds = i; // 只有一种字节的情况 if (char_kinds == 1) { oos.writeInt(char_kinds); oos.writeByte(tmp_nodes[0].uch); oos.writeInt(tmp_nodes[0].weight); // 字节多于一种的情况 } else { node_num = 2 * char_kinds - 1;// 计算哈夫曼树所有节点个数 huf_tree = new HufTree[node_num]; for (i = 0; i < char_kinds; ++i) { huf_tree[i] = new HufTree(); huf_tree[i].uch = tmp_nodes[i].uch; huf_tree[i].weight = tmp_nodes[i].weight; huf_tree[i].parent = 0; huf_tree[i].index = i; queue.add(huf_tree[i]); } tmp_nodes = null; for (; i < node_num; ++i) { huf_tree[i] = new HufTree(); huf_tree[i].parent = 0; } // 创建哈夫曼树 createTree(huf_tree, char_kinds, node_num, queue); // 生成哈夫曼编码 hufCode(huf_tree, char_kinds); // 写入字节种类 oos.writeInt(char_kinds); for (i = 0; i < char_kinds; ++i) { oos.writeByte(huf_tree[i].uch); oos.writeInt(huf_tree[i].weight); map.put(huf_tree[i].uch, huf_tree[i].code); } oos.writeInt(file_len); fis = new FileInputStream(inputFile); code_buf = ""; // 将读出的字节对应的哈夫曼编码转化为二进制存入文件 while ((char_temp = fis.read()) != -1) { code_buf += map.get((byte) char_temp); while (code_buf.length() >= 8) { char_temp = 0; for (i = 0; i < 8; ++i) { char_temp <<= 1; if (code_buf.charAt(i) == '1') char_temp |= 1; } oos.writeByte((byte) char_temp); code_buf = code_buf.substring(8); } } // 最后编码长度不够8位的时候,用0补齐 if (code_buf.length() > 0) { char_temp = 0; for (i = 0; i < code_buf.length(); ++i) { char_temp <<= 1; if (code_buf.charAt(i) == '1') char_temp |= 1; } char_temp <<= (8 - code_buf.length()); oos.writeByte((byte) char_temp); } } oos.close(); fis.close(); } catch (Exception e) { e.printStackTrace(); } } // 解压函数 public void extract(File inputFile, File outputFile) { Compare cmp = new Compare(); PriorityQueue<HufTree> queue = new PriorityQueue<HufTree>(12, cmp); int i; int file_len = 0; int writen_len = 0; FileInputStream fis = null; FileOutputStream fos = null; ObjectInputStream ois = null; int char_kinds = 0; int node_num; HufTree[] huf_tree = null; byte code_temp; int root; try { fis = new FileInputStream(inputFile); ois = new ObjectInputStream(fis); fos = new FileOutputStream(outputFile); char_kinds = ois.readInt(); // 字节只有一种的情况 if (char_kinds == 1) { code_temp = ois.readByte(); file_len = ois.readInt(); while ((file_len--) != 0) { fos.write(code_temp); } // 字节多于一种的情况 } else { node_num = 2 * char_kinds - 1; // 计算哈夫曼树所有节点个数 huf_tree = new HufTree[node_num]; for (i = 0; i < char_kinds; ++i) { huf_tree[i] = new HufTree(); huf_tree[i].uch = ois.readByte(); huf_tree[i].weight = ois.readInt(); huf_tree[i].parent = 0; huf_tree[i].index = i; queue.add(huf_tree[i]); } for (; i < node_num; ++i) { huf_tree[i] = new HufTree(); huf_tree[i].parent = 0; } createTree(huf_tree, char_kinds, node_num, queue); file_len = ois.readInt(); root = node_num - 1; while (true) { code_temp = ois.readByte(); for (i = 0; i < 8; ++i) { if ((code_temp & 128) == 128) { root = huf_tree[root].rchild; } else { root = huf_tree[root].lchild; } if (root < char_kinds) { fos.write(huf_tree[root].uch); ++writen_len; if (writen_len == file_len) break; root = node_num - 1; // 恢复为根节点的下标,匹配下一个字节 } code_temp <<= 1; } // 在压缩的时候如果最后一个哈夫曼编码位数不足八位则补0 // 在解压的时候,补上的0之前的那些编码肯定是可以正常匹配到和他对应的字节 // 所以一旦匹配完补的0之前的那些编码,写入解压文件的文件长度就和压缩之前的文件长度是相等的 // 所以不需要计算补的0的个数 if (writen_len == file_len) break; } } fis.close(); fos.close(); } catch (Exception e) { e.printStackTrace(); } } // 构建哈夫曼树 public void createTree(HufTree[] huf_tree, int char_kinds, int node_num, PriorityQueue<HufTree> queue) { int i; int[] arr = new int[2]; for (i = char_kinds; i < node_num; ++i) { arr[0] = queue.poll().index; arr[1] = queue.poll().index; huf_tree[arr[0]].parent = huf_tree[arr[1]].parent = i; huf_tree[i].lchild = arr[0]; huf_tree[i].rchild = arr[1]; huf_tree[i].weight = huf_tree[arr[0]].weight + huf_tree[arr[1]].weight; huf_tree[i].index = i; queue.add(huf_tree[i]); } } // 获取哈夫曼编码 public void hufCode(HufTree[] huf_tree, int char_kinds) { int i; int cur, next; for (i = 0; i < char_kinds; ++i) { String code_tmp = ""; for (cur = i, next = huf_tree[i].parent; next != 0; cur = next, next = huf_tree[next].parent) { if (huf_tree[next].lchild == cur) code_tmp += "0"; else code_tmp += "1"; } huf_tree[i].code = (new StringBuilder(code_tmp)).reverse().toString(); } } }