哈弗曼压缩与解压的原理及对象化实现

来源:互联网 发布:python documentation 编辑:程序博客网 时间:2024/06/05 18:48
Java代码  收藏代码
  1. 上一篇博客当中提到了哈弗曼树的构建与编码,详情请参见:<a href="/blog/1870454">http://cq520.iteye.com/blog/1870454</a>  

       这一次主要是跟大家探讨一下哈弗曼压缩的原理及实现,由于过程化的实现更加容易理解也更加直观,所以这里首先会分步骤跟大家讲解一下哈弗曼压缩的具体实现方法,然后再与大家分享一下对象化的实现。

       首先,我们要知道文件为什么能压缩

       文件是由字节所组成的,一个字节的长度为8位,所以最多只存在256种字节,而一个文件中一般存在许多相同的字节,我们把相同的字节以一种更加精简的方式表示,就完成了我们所说的压缩。

       哈弗曼压缩的原理是什么?

       上次博客中提到了哈弗曼编码,但是只是粗略的带过了,这一次举一个具体的例子来更加直观的说明哈弗曼压缩的原理。

       假设一个文件中是这样的一串字节ABBCCCDDDD,那么这个文件的大小就是10个byte。那么接下来就是我们的哈弗曼压缩的第一步:

一.读取文件,统计每一种字节出现的次数,将出现的字节种类与对应的次数保存起来(可采用数组或者是HashMap,或者是其他的数据结构)

保存完了之后干什么呢??当然是构建哈弗曼树呀。第二步:

二. 利用得到的字节与对应的频次构建哈弗曼树,需要注意的是,构建树的时候是以字节出现的频次作为我们的评判标准,出现次数越多的放在越上层。

比如我们上面所说的这个文件,它所构成的树应该是这样的:



 

 

我们现在得到的树还处于未编码的状态,那么第三步毫无疑问就是我们所说的编码了:

三. 将得到的哈弗曼树进行编码

编码之后的树就变成这个样子了(采取向左编1的方式):



 

 

到了这一步其实我们的压缩就已经完成一半了,听到这里,你可能纳闷了,不对呀,不是还没开始么??

       下面我们就来看一下哈弗曼压缩的精华所在:

       编码之后,A所对应的的编码就是111,B就是110,C是10,D是0,那么我们的文件就变成了11111011,01010100000是不是有一种亲切感?下面只要把这些10串每8个作为一组编码成一个新的字节(2进制转10进制),我们的压缩工作就大功告成了,所以这里每8位我也特别用逗号表示出来了,怎么样一个10个byte的文件瞬间变成了3个byte,是不是很神奇呢?

       等一下,做到这一步其实是远远不够的,有几个问题:

1.       怎么样把这一串的01变成我们所说的byte数?

需要注意的是,我们把文件中的字节变成这个样子只是把它们变成了一个01的字符串,那么这个问题就要用编程方法来解决了,具体方法有很多种,下面会给大家介绍一两种。

2.如果最后几位不满8个怎么办?

       我们可以定义这样一个规则,在最后的位置补0,在文件的末尾再加一位表示最后一个数补0的个数,这样的话这个问题就变得很简单了。

3. 压缩之后我们怎么知道压缩前每种字节对应的编码是什么样子的

       如果你理解了压缩的前三步,你一定会想到这个问题,确实,我们如果按照这种方式压缩,我们所得到的文件将会无法复原。那么要完成压缩,最关键的一步,还要将压缩时所得到的每个字节对应的码表写入文件,这样我们才能保证,我们所做的工作是可逆的。

       好的,说完了这些,压缩剩下来的步骤相信你也已经明了了,压缩第四步:

四.根据每种字节对应的哈弗曼编码,将文件转换成01字符串

五.将得到的01串重新编码成新的byte数组写入文件

       当然,第四步与第五步可以同时完成,而且,每生成8个以上的字符串就将其前8位转换成byte数组的效率要比一次性转换的效率要高,这是因为,当文件转换成编码之后,长度增大,JVM中需要开辟一个很大的内存空间去存放这个字符串,这显然是很耗时的。

不过在过程化实现的代码中,楼主并没有将这一层优化,具体的优化工作需要读者们自己去做。

下面就是过程化实现的代码了:

 

Java代码  收藏代码
  1. package 哈弗曼压缩;  
  2.   
  3. /** 
  4.  * 哈弗曼压缩 
  5.  * @author 陈强 
  6.  * 
  7.  */  
  8. import java.io.File;  
  9. import java.io.FileInputStream;  
  10. import java.io.FileOutputStream;  
  11. import java.io.InputStream;  
  12. import java.io.OutputStream;  
  13. import java.util.ArrayList;  
  14. import java.util.Comparator;  
  15. import java.util.HashMap;  
  16. import java.util.Iterator;  
  17. import java.util.PriorityQueue;  
  18. import java.util.Set;  
  19.   
  20. public class HFMCondense {  
  21.     public static void main(String args[]){  
  22.         String file="D://b.txt";  
  23.         HFMCondense condense=new HFMCondense();  
  24.         //System.out.println("元素与对应的频次表:");  
  25.         //System.out.println(condense.readFiletoMap(file));  
  26.         //System.out.println("生成的哈弗曼树");  
  27.         HFMNode hfmTree=condense.HashMapToHFMTree(condense.readFiletoMap(file));  
  28.         //condense.PreOrderTraverse(hfmTree);  
  29.         //System.out.println();  
  30.         //System.out.println("产生的哈弗曼编码表:");  
  31.         //condense.HuffmanCode(hfmTree,"");  
  32.         condense.HuffmanCoding(hfmTree, "");  
  33.         //转译后的字符串  
  34.         //String codeString=condense.FileToString(file);  
  35.         //System.out.println(codeString);  
  36.         //压缩后的字符串  
  37.         //String newString=new String(condense.createByteArray(codeString));  
  38.         //System.out.println(newString);  
  39.         System.out.println("开始压缩...");  
  40.         long start=System.currentTimeMillis();  
  41.         //System.out.println(condense.FileToString(file));  
  42.         //System.out.println(condense.CodeToString(file));  
  43.         //condense.CompressFile(condense.createByteArray(condense.CodeToString(file)),"D://c");  
  44.         condense.CompressFile(condense.createByteArray(condense.FileToString(file)),"D://c");  
  45.         System.out.println("压缩结束...用时:"+(System.currentTimeMillis()-start));  
  46.         //打印数组最后一个补0的个数  
  47.         //byte content[]=condense.createByteArray(condense.FileToString(file));  
  48.         //System.out.println(content[content.length-2]);  
  49.     }  
  50.     /** 
  51.      * 读取将要被压缩的文件,统计每一个字符出现的频率,并将得到的内容存入HashMap中 
  52.      * @param fileName 将要被压缩的文件 
  53.      * @return 每一个字节数出现的频率所对应的HashMap 
  54.      */  
  55.     public HashMap<Byte,Integer> readFiletoMap(String fileName){  
  56.         HashMap<Byte,Integer> hashMap=new HashMap<Byte,Integer>();  
  57.         File file=new File(fileName);  
  58.         if(file.exists()){  
  59.             try{  
  60.                 InputStream in=new FileInputStream(file);  
  61.                 //创建与文件大小相同的字节数组  
  62.                 byte[] content=new byte[in.available()];  
  63.                 //读取文件  
  64.                 in.read(content);  
  65.                 //存入HashMap中  
  66.                 for(int i=0;i<content.length;i++){  
  67.                     //如果表中存在某一个键  
  68.                     if(hashMap.containsKey(content[i])){  
  69.                         //获取该字节当前的键值  
  70.                         int rate=hashMap.get(content[i]);  
  71.                         //键值增大  
  72.                         rate++;  
  73.                         hashMap.put(content[i], rate);  
  74.                     }  
  75.                     //如果不存在某一个字节对象,则将它存入HashMap当中  
  76.                     else{  
  77.                         hashMap.put(content[i],1);  
  78.                     }  
  79.                 }  
  80.                 in.close();  
  81.             }catch(Exception e){  
  82.                 e.printStackTrace();  
  83.             }     
  84.         }  
  85.         else{  
  86.             System.out.println("文件不存在");  
  87.         }  
  88.         return hashMap;  
  89.     }  
  90.     /** 
  91.      * 将HashMap中的元素取出来封装到哈弗曼树中,树中叶子结点保存的是HashMap中的每一个键值与频率 
  92.      * @param map 读取的Map 
  93.      * @return  哈夫曼树的根结点 
  94.      */  
  95.     public HFMNode HashMapToHFMTree(HashMap<Byte,Integer> map){  
  96.         //得到存储键值的系  
  97.         Set<Byte> keys=map.keySet();  
  98.         //得到迭代器对象  
  99.         Iterator<Byte> iter=keys.iterator();  
  100.         //如果还有值  
  101.         while(iter.hasNext()){  
  102.             byte key=iter.next();//获取系中的键  
  103.             int value=map.get(key);//得到该键出现的频率  
  104.             //创建结点并将结点对象加入到队列当中  
  105.             HFMNode node=new HFMNode(key,value);  
  106.             nodeQueue.add(node);  
  107.             nodeList.add(node);  
  108.         }  
  109.         //当所剩的结点数还大于两个  
  110.         while(nodeQueue.size()>=2){  
  111.             //得到键值频率最小的两个结点  
  112.             HFMNode left=nodeQueue.poll();  
  113.             HFMNode right=nodeQueue.poll();  
  114.             //将这两个结点组合起来生成新的结点  
  115.             HFMNode node=new HFMNode(left.data,left.value+right.value,left,right);  
  116.             nodeQueue.add(node);  
  117.         }  
  118.         //获取队列中的最后一个结点作为根结点  
  119.         HFMNode hfm=nodeQueue.poll();  
  120.         return hfm;  
  121.     }  
  122.     /** 
  123.      * 为生成的哈弗曼树进行编码,产生对应的哈弗曼编码表 
  124.      * @param hfm  对应的哈弗曼树 
  125.      * @param code 对应生成的哈弗曼编码 
  126.      * @return 哈弗曼编码表 
  127.      */  
  128.     //创建一个新的哈弗曼编码表  
  129.     HashMap<Byte,String> codeMap=new HashMap<Byte,String>();  
  130.     public HashMap<Byte,String> HuffmanCoding(HFMNode hfm,String code){  
  131.         //如果左子树不为空,则左子树编码加1  
  132.         if(hfm.lchild!=null){  
  133.             HuffmanCoding(hfm.lchild,code+"1");  
  134.         }  
  135.         //如果右子树不为空,则右子树编码加0  
  136.         if(hfm.rchild!=null){  
  137.             HuffmanCoding(hfm.rchild,code+"0");  
  138.         }  
  139.         //如果到达叶子结点,则将元素放入HashMap中生成哈弗曼编码表  
  140.         if(hfm.lchild==null&&hfm.rchild==null){  
  141.             codeMap.put(hfm.data,code);  
  142.             hfm.code=code;  
  143.         }  
  144.         return codeMap;  
  145.     }  
  146.     /** 
  147.      * 将哈弗曼编码转换成字符串 
  148.      * @param fileName 读取的文件名 
  149.      * @return 编码之后的哈弗曼字符串 
  150.      */  
  151.     public String CodeToString(String fileName){  
  152.         File file=new File(fileName);  
  153.         String codeString="";  
  154.         //如果文件存在  
  155.         if(file.exists()){  
  156.             try{  
  157.                 InputStream in=new FileInputStream(file);  
  158.                 byte content[]=new byte[in.available()];  
  159.                 in.read(content);  
  160.                 int i=0;  
  161.                 int len=content.length;//得到文件的字节长度  
  162.                 int size=nodeList.size();//得到队列的长度  
  163.                 while(i<len){  
  164.                     for(int j=0;j<size;j++){  
  165.                         if(content[i]==nodeList.get(j).data){  
  166.                             codeString+=nodeList.get(j).code;  
  167.                             break;  
  168.                         }  
  169.                     }  
  170.                     i++;  
  171.                 }  
  172.                 in.close();  
  173.             }catch(Exception e){  
  174.                 e.printStackTrace();  
  175.             }  
  176.         }else {  
  177.             System.out.println("文件不存在");  
  178.         }  
  179.         return codeString;  
  180.     }  
  181.     /** 
  182.      * 将文件按照对应的哈弗曼编码表转成01字符串 
  183.      * @param fileName 读入的文件名 
  184.      * @return 转译后的字符串 
  185.      */  
  186.     public String FileToString(String fileName){  
  187.         File file=new File(fileName);  
  188.         String StringContent="";  
  189.         //如果文件存在  
  190.         if(file.exists()){  
  191.             try{  
  192.                 InputStream in=new FileInputStream(file);  
  193.                 byte content[]=new byte[in.available()];  
  194.                 in.read(content);  
  195.                 //循环转译  
  196.                 int len=content.length;  
  197.                 for(int i=0;i<len;i++){  
  198.                     StringContent+=codeMap.get(content[i]);  
  199.                 }  
  200.                 in.close();  
  201.             }catch(Exception e){  
  202.                 e.printStackTrace();  
  203.             }  
  204.         }else{  
  205.             System.out.println("文件不存在");  
  206.         }  
  207.         return StringContent;  
  208.     }  
  209.     /** 
  210.      * 将转译后的01字符串重新转换后放入新的字节数组当中 
  211.      * @param code 转译后的01字符串 
  212.      * @return 新的字节数组,里面包含了压缩后的字节内容 
  213.      */  
  214.     public byte[] createByteArray(String code) {  
  215.         //将每8位字符分隔开来得到字节数组的长度  
  216.         int size=code.length()/8;  
  217.         //截取得到字符串8整数后的最后几个字符串  
  218.         String destString=code.substring(size*8);     
  219.         byte dest[]=destString.getBytes();    
  220.         //s用来记录字节数组的单字节内容  
  221.         int s = 0;  
  222.         int i=0;        
  223.         int temp = 0;     
  224.         // 将字符数组转换成字节数组,得到字符的字节内容,方便将二进制转为十进制  
  225.         byte content[] = code.getBytes();     
  226.         for (int k = 0; k < content.length; k++) {     
  227.             content[k] = (byte) (content[k] - 48);     
  228.         }  
  229.         //转译后的字节内容数组  
  230.         byte byteContent[];     
  231.         if (content.length % 8 == 0) {// 如果该字符串正好是8的倍数     
  232.             byteContent = new byte[content.length / 8 + 1];     
  233.             byteContent[byteContent.length - 1] = 0;// 那么返回的字节内容数组的最后一个数就补0     
  234.         } else {     
  235.             //否则该数组的最后一个数就是补0的个数  
  236.             byteContent = new byte[content.length / 8 + 2];     
  237.             byteContent[byteContent.length - 1] = (byte) (8 - content.length % 8);     
  238.         }     
  239.         int bytelen=byteContent.length;  
  240.         int contentlen=content.length;  
  241.         // byteContent数组中最后一个是补0的个数,实际操作到次后个元素  
  242.         //Math.pow返回第一个参数的第二个参数次幂的值  
  243.         while (i < bytelen - 2) {     
  244.             for (int j = 0; j < contentlen; j++) {     
  245.                 if (content[j] == 1) {// 如果数组content的值为1     
  246.                     s =(int)(s + Math.pow(2, (7 - (j - 8 * i))));// 累积求和     
  247.                 }// if     
  248.                 if ((j+1)%8==0) {// 当有8个元素时     
  249.                     byteContent[i] = (byte) s;// 就将求出的和放入到byteContent数组中去     
  250.                     i++;     
  251.                     s = 0;// 并重新使s的值赋为0     
  252.                 }// if     
  253.             }// for     
  254.         }// while       
  255.         int destlen=dest.length;  
  256.         for(int n=0;n<destlen;n++){     
  257.             temp+=(dest[n]-48)*Math.pow(27-n);//求倒数第2个字节的大小     
  258.         }    
  259.         byteContent[byteContent.length - 2] = (byte) temp;   
  260.         return byteContent;     
  261.     }    
  262.     /** 
  263.      * 压缩并输出新文件 
  264.      * @param content 压缩后产生的新的字节数组 
  265.      * @param fileName 输出文件名 
  266.      */  
  267.     public void CompressFile(byte[] content,String fileName){  
  268.         File file=null;  
  269.         //统一后缀名  
  270.         if(!fileName.endsWith("hfm")){  
  271.             file=new File(fileName+".hfm");  
  272.         }else if(fileName.endsWith("hfm")){  
  273.             file=new File(fileName);      
  274.         }  
  275.         int len=content.length;  
  276.         if(len>0){  
  277.             try{  
  278.                 OutputStream out=new FileOutputStream(file);  
  279.                 //将字节内容写入文件  
  280.                 out.write(content);  
  281.                 out.close();  
  282.             }catch(Exception e){  
  283.                 e.printStackTrace();  
  284.             }  
  285.         }else{  
  286.             System.out.println("压缩出错");  
  287.         }  
  288.     }  
  289.     /** 
  290.      * 测试一下哈弗曼树建立是否正确 
  291.      * @param hfm 
  292.      */  
  293.     public void PreOrderTraverse(HFMNode hfm){  
  294.         if(hfm!=null){  
  295.             System.out.print(hfm.value+" ");  
  296.             PreOrderTraverse(hfm.lchild);  
  297.             PreOrderTraverse(hfm.rchild);  
  298.         }  
  299.     }  
  300.     /** 
  301.      * 存储哈弗曼树结点的优先队列 
  302.      */  
  303.     ArrayList<HFMNode> nodeList=new ArrayList<HFMNode>();  
  304.     PriorityQueue<HFMNode> nodeQueue=new PriorityQueue<HFMNode>(11,new MyComparator());  
  305.     /** 
  306.      * 实例化的一个比较器类 
  307.      */  
  308.     class MyComparator implements Comparator<HFMNode>{  
  309.         public int compare(HFMNode o1, HFMNode o2) {  
  310.             return o1.value-o2.value;  
  311.         }     
  312.     }  
  313. }  

 结点类:

 

Java代码  收藏代码
  1. package 哈弗曼压缩;  
  2.   
  3. public class HFMNode {  
  4.     byte data;  //存储字节的数据域  
  5.     int value;  //字节出现的频率  
  6.     String code;//叶子结点的哈弗曼编码  
  7.     HFMNode lchild,rchild;//左右孩子的引用  
  8.     //只指定数据的构造体  
  9.     public HFMNode(byte data,int rate){  
  10.         this(data,rate,null,null);  
  11.     }  
  12.     //同时指定左右孩子的构造体  
  13.     public HFMNode(byte data,int value,HFMNode lchild,HFMNode rchild){  
  14.         this.data=data;  
  15.         this.value=value;  
  16.         this.lchild=lchild;  
  17.         this.rchild=rchild;  
  18.     }  
  19. }  

 

大家也看到了里面有两个方法做了同样一件事情,而经过测试之后发现两个方法的效率差不多,我们注意到的是,上面所做的每一步都是下一步的前提,所以在做过程化的实现时,一定要先理清楚前后的逻辑关系,此外,上文也提到了,这样做完之后还有几个问题没有解决。

1.       码表还未写入文件。(写入码表的方法有很多种,可以使用DataOutputStream类或者ObjectOutputStream类里面所提供的方法,大家自己查看一下API吧)

2.       压缩的效率很低(上面这一种实现的方法效率几乎是最低了,优化有几个方向,例如引入缓冲,生成编码的同时进行转译,采用更加快速的存储方式(采用数组存储的效率要高于HashMap.

压缩完成之后就要进行解压了,相信掌握了压缩技术的你,解压已经不成难题了,需要注意的是,

1.       码表最好放在压缩文件的前半部分,(这是因为,你可以在码表的最后用两个0表示码表的结束,而在文件的尾部,你可能也可以碰到两个0,这样你就无法判断哪里是码表的开始了

2.       读出来码表之后记得保存起来,解压时需要一个一个字节的去对照码表

 

说到这里,我们的过程化实现就已经基本结束了,但是大家可能想起来了,不是要讲对象化的实现么??怎么讲了这么久还没开始呀?别急别急,上面只是为了让大家更好的理解哈弗曼压缩的原理。下面就是对象化实现的具体代码,由于对象化的实现难于理解,所以很多地方都给予了注释,甚至一些不必要的地方都写上了,(当然注释中可能有些不到位的地方)不过我想写好注释或许才是我们真正所需要的,湖大的陆亮学长就曾说过,我可能写不出一行有技术含量的代码,但我能写出一行规范的代码。

对象化的实现方法中,提供了按位输入与输出的类,这些类都是自定义的,因为在编程中我们所能操作的计算机的最小单元是byte那么在这个类中是怎么做的呢?将一个字节进行8次移位按位与运算,我们就得到了这个字节的8bit的表示方式

里面还有很多比较实用的方法,希望大家能够耐心的看一下,好的,话不多说了,方法如下:

基本数据接口:

Java代码  收藏代码
  1. package 哈弗曼完全压缩;  
  2.   
  3. public interface BitUtils {  
  4.      public static final int BITS_PER_BYTES=8;//位与byte之间的转换单位  
  5.      public static final int DIFF_BYTES=256;//0x100  
  6.      public static final int EOF=256;//EndOfFile 资料源无更多的资料可读取  
  7. }  

 位输入类:

Java代码  收藏代码
  1. package 哈弗曼完全压缩;  
  2. /** 
  3.  * InputStream的包装类,提供按位输入 
  4.  */  
  5. import java.io.IOException;  
  6. import java.io.InputStream;  
  7.   
  8. public class BitInputStream {  
  9.     private InputStream in;//基本输入流  
  10.     private int buffer;//byte缓冲区  
  11.     private int bufferPos;//表示缓冲区中有多少未被使用的空间  
  12.     /** 
  13.      * 封装InputStream的构造方法,初始化byte缓冲区的大小 
  14.      * @param is InputStream对象 
  15.      */  
  16.     public BitInputStream(InputStream is){  
  17.         in=is;  
  18.         bufferPos=BitUtils.BITS_PER_BYTES;//初始化缓冲区的剩余空间  
  19.     }  
  20.     /** 
  21.      * 读取一位的方法,每8次对其进行调用就会从基本输入流中读出一个byte 
  22.      * @return 1位数据,1或者0 
  23.      * @throws IOException  
  24.      */  
  25.     public int readBit() throws IOException{  
  26.         //如果缓冲区还未被使用  
  27.         if(bufferPos==BitUtils.BITS_PER_BYTES){  
  28.             //输入流读取一位  
  29.             buffer=in.read();  
  30.             //读到文件的末尾了  
  31.             if(buffer==-1)  
  32.                 return -1;  
  33.             //清空缓冲区  
  34.             bufferPos=0;  
  35.         }  
  36.         //扩张缓冲区  
  37.         return getBit(buffer,bufferPos++);  
  38.     }  
  39.     /** 
  40.      * 关闭输入流 
  41.      * @throws IOException  
  42.      */  
  43.     public void close() throws IOException{  
  44.         in.close();  
  45.     }  
  46.     /** 
  47.      * 获取一个byte中每一位的方法 
  48.      * @param pack  
  49.      * @param pos  
  50.      * @return  
  51.      */  
  52.     private static int getBit(int pack,int pos){  
  53.         //将一个字节进行8次按位与运算,得到这个字节的8位  
  54.         return (pack&(1<<pos))!=0?1:0;  
  55.     }  
  56. }  

 位输出类:

 

Java代码  收藏代码
  1. package 哈弗曼完全压缩;  
  2. /** 
  3.  * OutputStream的包装类,提供按位输出的方法 
  4.  */  
  5. import java.io.IOException;  
  6. import java.io.OutputStream;  
  7.   
  8. public class BitOutputStream {  
  9.     private OutputStream out; //基本输出流  
  10.     private int buffer;//输出的缓冲区  
  11.     private int bufferPos;//缓冲区中剩余的位数  
  12.     /** 
  13.      * 封装OutputStream的构造方法,初始化缓冲区大小 
  14.      * @param os 
  15.      */  
  16.     public BitOutputStream(OutputStream os){  
  17.         bufferPos=0;  
  18.         buffer=0;  
  19.         out=os;  
  20.     }  
  21.     /** 
  22.      * 写入一串的位 
  23.      * @param val 包含有位数据的数组 
  24.      * @throws IOException 
  25.      */  
  26.     public void writeBits(int []val) throws IOException{  
  27.         int len=val.length;  
  28.         for(int i=0;i<len;i++){  
  29.             writeBit(val[i]);  
  30.         }  
  31.     }  
  32.     /** 
  33.      * 写入位的方法(0或1),每8次对其进行调用就从基本流中写入一个byte 
  34.      * @param val 当前写入的位数据 
  35.      * @throws IOException 
  36.      */  
  37.     public void writeBit(int val) throws IOException{  
  38.         buffer=setBit(buffer,bufferPos++,val);//将缓冲数据转换成位数据  
  39.         //每读到一个byte就刷新一次  
  40.         if(bufferPos==BitUtils.BITS_PER_BYTES)//缓冲区已满则刷新缓冲区  
  41.             flush();  
  42.     }  
  43.     /** 
  44.      * 刷新此缓冲的输出流 
  45.      * @throws IOException 
  46.      */  
  47.     public void flush() throws IOException{  
  48.         if(bufferPos==0)//如果缓冲中没有数据则不执行  
  49.             return;  
  50.         //将缓冲区中的数据写入  
  51.         out.write(buffer);  
  52.         //重置缓冲区  
  53.         bufferPos=0;  
  54.         buffer=0;  
  55.     }  
  56.     /** 
  57.      * 关闭流的方法 
  58.      * @throws IOException 
  59.      */  
  60.     public void close() throws IOException{  
  61.         flush();  
  62.         out.close();  
  63.     }  
  64.     /** 
  65.      * 进行位数据转换的方法 
  66.      * @param pack 
  67.      * @param pos 
  68.      * @param val 当前位 
  69.      * @return 
  70.      */  
  71.     private int setBit(int pack,int pos,int val){  
  72.         if(val==1)  
  73.             //按位或运算  
  74.             pack|=(val<<pos);  
  75.         return pack;  
  76.     }  
  77. }  

 结点类:

Java代码  收藏代码
  1. package 哈弗曼完全压缩;  
  2. /** 
  3.  * 哈弗曼结点类 
  4.  */  
  5. public class HuffNode implements Comparable<HuffNode>{  
  6.     public int value;//结点数据  
  7.     public int weight;//权重  
  8.     HuffNode left,right;//左右孩子结点  
  9.     HuffNode parent;//父亲结点  
  10.     /** 
  11.      * 初始化结点的数据,权重,左右孩子结点与父亲结点 
  12.      * @param v 数据 
  13.      * @param w 权重 
  14.      * @param lchild 左孩子结点 
  15.      * @param rchild 右孩子结点 
  16.      * @param pt    父亲结点 
  17.      */  
  18.     HuffNode(int v,int w,HuffNode lchild,HuffNode rchild,HuffNode pt){  
  19.         value=v;  
  20.         weight=w;  
  21.         left=lchild;  
  22.         right=rchild;  
  23.         parent=pt;  
  24.     }  
  25.     /** 
  26.      * 比较两个结点的权重 
  27.      */  
  28.     public int compareTo(HuffNode rhs) {  
  29.         return weight-rhs.weight;  
  30.     }  
  31. }  

 字符统计类:

Java代码  收藏代码
  1. package 哈弗曼完全压缩;  
  2. /** 
  3.  * 字符统计类,获取输入流(通常是文件)中所含的字符数 
  4.  * 8位字节认为是ASCII字符 
  5.  */  
  6. import java.io.IOException;  
  7. import java.io.InputStream;  
  8.   
  9. public class CharCounter {  
  10.     //字节的下标表示字节的种类,对应的值表示出现的次数  
  11.     private int theCounts[]=new int[BitUtils.DIFF_BYTES];//字节的种类总共有256种  
  12.     /** 
  13.      * 默认的无参的构造方法 
  14.      */  
  15.     public CharCounter(){  
  16.           
  17.     }  
  18.     /** 
  19.      * 封装了基本的InputStream,读取数据并进行字符的频次统计 
  20.      * @param input InputStream对象 
  21.      * @throws IOException 
  22.      */  
  23.     public CharCounter(InputStream input) throws IOException{  
  24.         int ch;//读到的字节  
  25.         //一直读到文件的末尾,每一种byte出现了多少次  
  26.         while((ch=input.read())!=-1){  
  27.             theCounts[ch]++;  
  28.         }  
  29.     }  
  30.     /** 
  31.      * 获取该字符统计数组的某一个字符出现的次数 
  32.      * @param ch 数组下标 
  33.      * @return 该下标位置字符出现的次数 
  34.      */  
  35.     public int getCount(int ch){  
  36.         return theCounts[ch&0xff];  
  37.     }  
  38.     /** 
  39.      * 设置某一个字符出现的次数 
  40.      * @param ch 数组下标 
  41.      * @param count 字符出现次数 
  42.      */  
  43.     public void setCount(int ch,int count){  
  44.         theCounts[ch&0xff]=count;  
  45.     }  
  46. }  

 哈弗曼树类:

Java代码  收藏代码
  1. package 哈弗曼完全压缩;  
  2.   
  3. import java.io.DataInputStream;  
  4. import java.io.DataOutputStream;  
  5. import java.io.IOException;  
  6. import java.util.PriorityQueue;  
  7.   
  8. public class HuffmanTree {  
  9.     private CharCounter theCounts;//字符统计类对象  
  10.     private HuffNode root;//根结点  
  11.     private HuffNode[] theNodes=new HuffNode[BitUtils.DIFF_BYTES+1];//存储HuffNode的数组  
  12.     public static final int ERROR=-3;//错误  
  13.     public static final int INCOMPLETE_CODE=-2;//不完全的结点编码  
  14.     public static final int END=BitUtils.DIFF_BYTES;//字节的溢出位  
  15.     /** 
  16.      * 实例化一个字符统计类对象 
  17.      */  
  18.     public HuffmanTree(){  
  19.         theCounts=new CharCounter();  
  20.         root=null;  
  21.     }  
  22.     /** 
  23.      * 可以通过CharCounter对象来创建一个huffmanTree对象 
  24.      * @param cc CharCounter对象 
  25.      */  
  26.     public HuffmanTree(CharCounter cc){  
  27.         theCounts=cc;  
  28.         root=null;  
  29.         createTree();//创建HuffmanTree  
  30.     }  
  31.     /** 
  32.      * 得到要寻找的字符编码所在的树结点,如果该字符不在树上,则返回null表示出错,否则,通过父亲链逆向查找,直到到达根结点 
  33.      * @param ch 当前结点的下标 
  34.      * @return 结点相对的字符编码数组 
  35.      */  
  36.     public int[] getCode(int ch){  
  37.         HuffNode current=theNodes[ch];  
  38.           
  39.         if(current==null)  
  40.             return null;  
  41.         String v="";//结点的编码  
  42.         HuffNode parent=current.parent;  
  43.           
  44.         while(parent!=null){  
  45.             if(parent.left==current)  
  46.                 v="0"+v;//左结点编码  
  47.             else   
  48.                 v="1"+v;//右结点编码  
  49.             //向下遍历  
  50.             current=current.parent;  
  51.             parent=current.parent;  
  52.         }  
  53.         int len=v.length();  
  54.         int [] result=new int[len];//创建一个与编码相同大小数组  
  55.         for(int i=0;i<len;i++)  
  56.             result[i]=v.charAt(i)=='0'?0:1;  
  57.         return result;  
  58.     }  
  59.     /** 
  60.      * 获取编码对应的字符 
  61.      * @param code 哈弗曼编码 
  62.      * @return 存储在结点中的值(如果结点不是叶子结点,则返回符号INCOMPLETE) 
  63.      */  
  64.     public int getChar(String code){  
  65.         HuffNode leaf=root;//获取根结点  
  66.         int len=code.length();  
  67.         //按照编码向左或向右遍历到叶子结点  
  68.         for(int i=0;leaf!=null&&i<len;i++)  
  69.             if(code.charAt(i)=='0')  
  70.                 leaf=leaf.left;  
  71.             else  
  72.                 leaf=leaf.right;  
  73.         //根结点为空  
  74.         if(leaf==null)  
  75.             return ERROR;  
  76.         return leaf.value;  
  77.     }  
  78.     /** 
  79.      * 写入编码表的方法 
  80.      * @param out 写入的数据流 
  81.      * @throws IOException 
  82.      */  
  83.     public void writeEncodingTable(DataOutputStream out) throws IOException{  
  84.         for(int i=0;i<BitUtils.DIFF_BYTES;i++){  
  85.             if(theCounts.getCount(i)>0){  
  86.                 out.writeByte(i);//将字节写入(通常是文件)  
  87.                 out.writeInt(theCounts.getCount(i));//将字节出现的次数写入(通常是文件)  
  88.             }  
  89.         }  
  90.         //最后写入0表示编码的结束  
  91.         out.writeByte(0);  
  92.         out.writeInt(0);  
  93.     }  
  94.     /** 
  95.      * 读取编码表的方法 
  96.      * @param in 数据输入流对象 
  97.      * @throws IOException 
  98.      */  
  99.     public void readEncodingTable(DataInputStream in) throws IOException{  
  100.         for(int i=0;i<BitUtils.DIFF_BYTES;i++)  
  101.             theCounts.setCount(i, 0);  
  102.         byte ch;  
  103.         int num;  
  104.         for(;;){  
  105.             ch=in.readByte();//读到的字节  
  106.             num=in.readInt();//字符出现的次数  
  107.             if(num==0)//如果读到0表示编码表的结束  
  108.                 break;  
  109.             theCounts.setCount(ch, num);  
  110.         }  
  111.         createTree();//创建HuffmanTree  
  112.     }  
  113.     /** 
  114.      * 构造哈弗曼编码树的方法 
  115.      */  
  116.     public void createTree(){  
  117.         //创建一个优先队列来保存HuffNode  
  118.         PriorityQueue<HuffNode> pq=new PriorityQueue<HuffNode>();  
  119.           
  120.         for(int i=0;i<BitUtils.DIFF_BYTES;i++){  
  121.             if(theCounts.getCount(i)>0){//如果某一个字节出现过  
  122.                 HuffNode newNode=new HuffNode(i,theCounts.getCount(i),null,null,null);  
  123.                 theNodes[i]=newNode;  
  124.                 pq.add(newNode);//将新结点添加到队列中  
  125.             }  
  126.         }  
  127.           
  128.         theNodes[END] =new HuffNode(END,1,null,null,null);  
  129.         pq.add(theNodes[END]);  
  130.         //当剩余的结点多于一个时  
  131.         while(pq.size()>1){  
  132.             //每次取出当前最小的两个结点  
  133.             HuffNode n1=pq.remove();//remove方法与poll方法的唯一不同之处在于:此队列为空时将抛出一个异常  
  134.             HuffNode n2=pq.remove();  
  135.             //将最小的两个结点链接形成新结点  
  136.             HuffNode result=new HuffNode(INCOMPLETE_CODE,n1.weight+n2.weight,n1,n2,null);  
  137.             n1.parent=n2.parent=result;  
  138.             //将新结点添加到队列当中  
  139.             pq.add(result);  
  140.         }  
  141.         root=pq.element();//根结点就是队列中的最后一个结点  
  142.     }  
  143. }  

 解压缩类:

Java代码  收藏代码
  1. package 哈弗曼完全压缩;  
  2. /** 
  3.  * 包含解压缩的包装类 
  4.  */  
  5. import java.io.DataInputStream;  
  6. import java.io.IOException;  
  7. import java.io.InputStream;  
  8.   
  9. public class HZIPInputStream extends InputStream{  
  10.     private BitInputStream bin;//位输入流  
  11.     private HuffmanTree codeTree;//编码的HuffmanTree对象  
  12.     /** 
  13.      * 封装InputStream对象,实例化HuffmanTree对象与BitInputStream对象,并读入哈弗曼编码 
  14.      * @param in 
  15.      * @throws IOException 
  16.      */  
  17.     public HZIPInputStream(InputStream in) throws IOException{  
  18.         //数据输入流  
  19.         DataInputStream din=new DataInputStream(in);  
  20.           
  21.         codeTree=new HuffmanTree();  
  22.         codeTree.readEncodingTable(din);  
  23.           
  24.         bin=new BitInputStream(in);  
  25.     }  
  26.     /** 
  27.      * 读取文件的方法 
  28.      */  
  29.     public int read() throws IOException{  
  30.         String bits="";//哈弗曼编码的字符串  
  31.         int bit;//位  
  32.         int decode;//解码后的字符  
  33.         while(true){  
  34.             bit=bin.readBit();  
  35.             if(bit == -1)  
  36.                 throw new IOException("Unexpected EOF");//意外的资源结束  
  37.               
  38.             bits+=bit;  
  39.             decode=codeTree.getChar(bits);//获取编码对应的字符  
  40.             if(decode==HuffmanTree.INCOMPLETE_CODE)//向下搜索到叶子结点  
  41.                 continue;  
  42.             else if(decode==HuffmanTree.ERROR)//编码出错  
  43.                 throw new IOException("Unexpected error");  
  44.             else if(decode==HuffmanTree.END)//编码溢出  
  45.                 return -1;  
  46.             else   
  47.                 return decode;  
  48.         }  
  49.     }  
  50.     /** 
  51.      * 关闭输入流 
  52.      */  
  53.     public void close() throws IOException{  
  54.         bin.close();  
  55.     }  
  56. }  

 压缩类:

Java代码  收藏代码
  1. package 哈弗曼完全压缩;  
  2. /** 
  3.  * 包含压缩的包装类 
  4.  */  
  5. import java.io.ByteArrayInputStream;  
  6. import java.io.ByteArrayOutputStream;  
  7. import java.io.DataOutputStream;  
  8. import java.io.IOException;  
  9. import java.io.OutputStream;  
  10.   
  11. public class HZIPOutputStream extends OutputStream{  
  12.     private ByteArrayOutputStream byteOut=new ByteArrayOutputStream();//实例化的一个字节数组输出流对象  
  13.     private DataOutputStream dout;//数据输出流对象  
  14.     /** 
  15.      * 实例化一个DataOutputStream对象的构造方法 
  16.      * @param out 输出流对象 
  17.      * @throws IOException 
  18.      */  
  19.     public HZIPOutputStream(OutputStream out) throws IOException{  
  20.         dout=new DataOutputStream(out);  
  21.     }  
  22.     /** 
  23.      * 写入编码频率的方法 
  24.      */  
  25.     public void write(int ch) throws IOException{  
  26.         byteOut.write(ch);  
  27.     }  
  28.     /** 
  29.      * 关闭流的方法 
  30.      */  
  31.     public void close() throws IOException{  
  32.         byte[] theInput=byteOut.toByteArray();//将字节数组输出流转换数据转换成字节数组进行输入  
  33.         ByteArrayInputStream byteIn=new ByteArrayInputStream(theInput);//将字节数组封装到字节输入流中  
  34.           
  35.         CharCounter countObj=new CharCounter(byteIn);//实例化字符统计对象并统计字节数组的字符出现的次数  
  36.         byteIn.close();//关闭字节输入流  
  37.           
  38.         HuffmanTree codeTree=new HuffmanTree(countObj);//通过CharCounter对象实例化一个HuffmanTree对象  
  39.         codeTree.writeEncodingTable(dout);//将编码写入数据输出流中  
  40.           
  41.         BitOutputStream bout=new BitOutputStream(dout);//创建位输出流  
  42.           
  43.         //将按编码转换后的位写入  
  44.         int len=theInput.length;  
  45.         for(int i=0;i<len;i++)  
  46.             bout.writeBits(codeTree.getCode(theInput[i]&0xff));  
  47.         bout.writeBits(codeTree.getCode(BitUtils.EOF));//文件结束的标示符  
  48.           
  49.         //关闭流  
  50.         bout.close();  
  51.         byteOut.close();  
  52.     }  
  53. }  

 压缩与解压方法及测试:

Java代码  收藏代码
  1. package 哈弗曼完全压缩;  
  2.   
  3. import java.io.BufferedInputStream;  
  4. import java.io.BufferedOutputStream;  
  5. import java.io.DataInputStream;  
  6. import java.io.File;  
  7. import java.io.FileInputStream;  
  8. import java.io.FileOutputStream;  
  9. import java.io.IOException;  
  10. import java.io.InputStream;  
  11. import java.io.OutputStream;  
  12.   
  13.   
  14. public class HZIP {  
  15.     /** 
  16.      * 压缩文件的方法,此方法需要传入正确的绝对路径名 
  17.      * @param inFile 需要被压缩的文件 
  18.      * @param outFile 压缩之后的输出文件 
  19.      * @throws IOException IO异常 
  20.      */  
  21.     public static void compress(String inFile,String outFile) throws IOException{  
  22.         String compressFile=null;//创建压缩文件  
  23.         String extension=inFile.substring(inFile.length()-4);//获取源文件的后缀名  
  24.         File file=new File(outFile);  
  25.         //如果文件已经存在  
  26.         if(file.exists()){  
  27.             System.out.println("文件已经存在");  
  28.         }else{  
  29.             //自动补齐后缀名  
  30.             if(!outFile.endsWith(".hfm")){  
  31.                 compressFile=outFile+extension+".hfm";    
  32.             }  
  33.             else{  
  34.                 compressFile=outFile+extension;  
  35.             }  
  36.             //创建文件输入的缓冲流  
  37.             InputStream in=new BufferedInputStream(new FileInputStream(inFile));  
  38.             //创建文件输出的缓冲流  
  39.             OutputStream out=new BufferedOutputStream(new FileOutputStream(compressFile));  
  40.             int ch;  
  41.             //创建哈弗曼压缩的输入流  
  42.             HZIPOutputStream hzout=new HZIPOutputStream(out);  
  43.             while((ch=in.read())!=-1){  
  44.                 hzout.write(ch);  
  45.             }  
  46.             //关闭流  
  47.             in.close();  
  48.             hzout.close();  
  49.         }  
  50.     }  
  51.     /** 
  52.      * 解压文件的方法,此方法需要填入正确的绝对路径名 
  53.      * @param compressedFile  需要被解压的文件 
  54.      * @param outFile 解压之后的输出文件 
  55.      * @throws IOException IO异常 
  56.      */  
  57.     public static void uncompress(String compressedFile,String outFile) throws IOException{  
  58.         String extension;//文件的后缀名  
  59.         extension =compressedFile.substring(compressedFile.length()-4);  
  60.         //得到压缩前的文件的后缀名  
  61.         String suffix=compressedFile.substring(compressedFile.length()-8,compressedFile.length()-4);  
  62.         //如果文件不合法则不执行解压操作  
  63.         if(!extension.equals(".hfm")){  
  64.             System.out.println("文件格式错误或者不是有效的压缩文件");  
  65.             return;  
  66.         }  
  67.         File file=new File(outFile);  
  68.         //如果已经存在同名文件  
  69.         if(file.exists()){  
  70.             System.out.println("该文件已经存在,请重新命名解压后的文件");  
  71.         }  
  72.         else{  
  73.             outFile+=(suffix+".uc");//输出文件的格式统一为uc格式  
  74.             //创建文件输入的缓冲流  
  75.             InputStream fin=new BufferedInputStream(new FileInputStream(compressedFile));  
  76.             //创建数据读入流  
  77.             DataInputStream in=new DataInputStream(fin);  
  78.             //创建哈弗曼压缩输入流  
  79.             HZIPInputStream hzin=new HZIPInputStream(in);  
  80.             //创建文件输出的缓冲流  
  81.             OutputStream fout=new BufferedOutputStream(new FileOutputStream(outFile));  
  82.             int ch;  
  83.             //解压并输出文件  
  84.             while((ch=hzin.read())!=-1){  
  85.                 fout.write(ch);  
  86.             }  
  87.             //关闭流  
  88.             hzin.close();  
  89.             fout.close();     
  90.         }  
  91.       
  92.     }  
  93.     public static void main(String args[]) throws IOException{  
  94.                 System.out.println("开始压缩");   
  95.         long start=System.currentTimeMillis();  
  96.         compress("d://a.txt","d://cd");  
  97.         System.out.println("压缩结束,用时:"+(System.currentTimeMillis()-start));  
  98.     }  
  99. }  

这个压缩还有很多值得改进的地方,比如当文件较小或过大时,压缩之后的文件比源文件还大,为什么呢?这是因为:

文件较小时,由于要写入编码表,所以造成了较大的空间占用。

而文件较大时,由于各种字节出现的频率已经趋于了相近的地步,那么我们再来回顾一下哈弗曼的压缩过程时会发现,极端情况下,当所有字节都出现过,且出现的次数相同时,每一种字节的编码长度都达到了8位(哈弗曼树的第9层刚好有256个叶子结点),达不到压缩的效果。

不过既然达不到压缩,做一个文件加密的工作也是不错的,怎么样,自己动手试试吧。

0 0
原创粉丝点击