哈夫曼编码的概念

哈夫曼编码是基于哈夫曼树实现的一种文件压缩方式。
哈夫曼树:一种带权路径最短的最优二叉树,每个叶子结点都有它的权值,离根节点越近,权值越小(根节点权值为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 = (char)i+"";Node<String> node = new Node<String>(ch,arrays[i]); //构建节点并传入字符和权值list.add(node); //添加节点}}return list;}       /*** 对链表中的元素排序* @param list* @return*/public void sortList(LinkedList<Node<String>> list){for(int i=list.size();i>1;i--){for(int j=0; j<i-1;j++){Node<String> node1 = list.get(j);Node<String> node2 = list.get(j+1);if(node1.getWeight()>node2.getWeight()){int temp ;                   temp = node2.getWeight();node2.setWeight(node1.getWeight());node1.setWeight(temp);String tempChar;tempChar = node2.getData();node2.setData(node1.getData());node1.setData(tempChar);Node<String> tempNode = new Node<String>(null, 0);tempNode.setLeft(node2.getLeft());tempNode.setRight(node2.getRight());node2.setLeft(node1.getLeft());node2.setRight(node1.getRight());node1.setLeft(tempNode.getLeft());node1.setRight(tempNode.getRight());}}         }}              /*** 建树的方法* @param list*/public Node<String> CreateHFMTree(LinkedList<Node<String>> list){while(list.size()>1){sortList(list); //排序节点链表Node<String> nodeLeft = list.removeFirst();Node<String> nodeRight = list.removeFirst();Node<String> nodeParent = new Node<String>( null ,nodeLeft.getWeight()+nodeRight.getWeight());            nodeParent.setLeft(nodeLeft);nodeParent.setRight(nodeRight);list.addFirst(nodeParent);}System.out.println("根节点的权重:"+list.get(0).getWeight());return list.get(0);//返回根节点}        public HashMap<String, String> getAllCode(Node<String> root){HashMap<String, String> map = new HashMap<>();inOrderGetCode("", map, root);return map;}    /*** 查询指定字符的哈夫曼编码(中序遍历)* @param code* @param st* @param root* @return*/public void inOrderGetCode(String code ,HashMap<String, String> map,Node<String> root){if(root!=null){inOrderGetCode(code+"0",map,root.getLeft());                     if(root.getLeft()==null&&root.getRight()==null)//存储叶子结点的哈夫曼编码{      System.out.println(root.getData());System.out.println(code);map.put(root.getData(), code);}            inOrderGetCode(code+"1",map,root.getRight());         }               }   /*** 中序遍历输出整棵树* @param root* @return*/public void inOrder(Node<String> root){if(root!=null){inOrder(root.getLeft());           if(root.getData()!=null)System.out.println(root.getData());            inOrder(root.getRight());           }               }
}

3、文件操作类(包括文件压缩对外的接口和文件解压对外的接口):

public class FileOperation {FileOutputStream fos;//申明文件输出流对象FileInputStream fis; //申明文件写入流对象     /*** 通过文件获取数组的方法* @param str*/public int[] getArrays(File file){int[] arrays = new int[256];try{FileInputStream fis = new FileInputStream(file);int ascii=0;while((ascii=fis.read())!=-1){arrays[ascii]++;}fis.close();}catch(IOException e){e.printStackTrace();}return arrays;}       /*** 读取文件获取01码*/public String GetStr(HashMap map,File file){String str="";   //定义字符串储存01码try{                        FileInputStream fis = new FileInputStream(file);int value=0;          while((value=fis.read())!=-1){str+=map.get((char)value+"");  //取单字符对应的01码,累加到字符串中}fis.close();}catch(IOException e){e.printStackTrace();}return str;}             /*** 写HashMap到文件(写入编码个数+第一个key+第一个value所占字节数+value最后一个字节的补0情况+第一个value的若干字节+下一个key+。。。。)*/public void writeHashMap(HashMap<String, String> map ,File file){int size = map.size(); //获取编码的个数,即HashMap中的键值对个数String temp=""; //存放临时8位01字符串int value=0; //存放01字符串转化得到的ASCII值try{fos = new FileOutputStream(file);fos.write(size);  //写HashMap长度Set<String> keySet = map.keySet(); //获取HashMap存放key的容器java.util.Iterator<String> it = keySet.iterator();//通过容器获取迭代器while(it.hasNext()) //迭代判断,有下一个key{String key = it.next(); //取出下一个keyString code = map.get(key); //取出codefos.write(key.charAt(0)); //写key值int a = code.length()/8;//能存满的字节数int b = code.length()%8;//剩余的位数int c =1; //值对应的存储的字节数if(b==0) //无剩余位{c=a;fos.write(c);  //写code的字节数fos.write(0);  //写补0数,为0个for(int i=0;i<a;i++) //写code值{temp="";for(int j=0;j<8;j++) {temp+=code.charAt(i*8+j);}value=StringToInt(temp);fos.write(value); //逐一把code的每一位写出去}}else {c=a+1;fos.write(c); //写code的字节数fos.write(8-b); //写补0数for(int i=0;i<8-b;i++) //补0{code+="0";}for(int i=0;i<c;i++){temp="";for(int j=0;j<8;j++){temp+=code.charAt(8*i+j);}value=StringToInt(temp);fos.write(value); //逐一写code,包括补的0}            }                               }}catch(IOException e){e.printStackTrace();}}   /*** 把文档转化为的HFM编码写入文件*/public void writeHFMcode(String HFMcode){int len = HFMcode.length();  //获取HFMcode长度int a = len/8;   //求出完整的字节的数目int b = len%8;   //求出剩余的位数String temp = ""; //临时存放8位数据int value = 0; //存放8位01转化得到的值try{if(b==0)  //无不足八位的部分,不需要补0{fos.write(0); //写补0数for(int i=0;i<a;i++){temp="";for(int j=0;j<8;j++){                              temp+=HFMcode.charAt(i*8+j);                     }value=StringToInt(temp);fos.write(value); //写HFMcode}}else   //需要补0{int c = 8-b; //计算补0数fos.write(c); //写补0数for(int i=0;i<c;i++) //补0{HFMcode+="0";}for(int i=0;i<a+1;i++) {temp="";for(int j=0;j<8;j++){temp+=HFMcode.charAt(i*8+j);}value=StringToInt(temp);fos.write(value); //写HFMcode}}fos.close(); //写完关闭资源}catch(IOException e){e.printStackTrace();}       }   /*** 把01字符串转化为ASCII码* @param temp* @return*/public int StringToInt(String temp){int value=0;for(int i=0;i<8;i++){int x = temp.charAt(i)-48;if(x==1)    //为1则累加入value{value+=Math.pow(2,7-i);  //表示2的(7-i)次方}}return value;}   /*** 把数值转化为01字符串* @param value*/public String IntToString(int value){String temp1=""; //存放反的字符串String temp="";  //存放正的字符串while(value>0) //逐渐取出各个二进制位数,字符串为反向的{temp1+=value%2;value=value/2;}        for(int i=temp1.length()-1;i>=0;i--){temp+=temp1.charAt(i);}return temp;}    /*** 把数值转化为01字符串,数值范围在0~255,01串不超过8位* @param value*/public String IntToStringEight(int value){String temp1=""; //存放反的字符串String temp="";  //存放正的字符串int add=0;while(value>0) //逐渐取出各个二进制位数,字符串为反向的{add++;temp1+=value%2;value=value/2;} add=8-add;for(int i=0;i<add;i++)//添0至8位{temp1+="0";}for(int i=temp1.length()-1;i>=0;i--) //反向的字符串获取正向的字符串{temp+=temp1.charAt(i);}return temp;}                        /*** 对外部的接口,实现把压缩后的数据和信息写入压缩文件* @param fileCompress*/public void compressFile(File fileCompress,HashMap<String, String> map,String HFMcode){writeHashMap(map, fileCompress);  //写HashMap的数据writeHFMcode(HFMcode); //继续写HFMcode 01字符串} /*** 解压获取HashMap* @param fileCompress*/public HashMap<String, String> readHashMap(File fileCompress){HashMap<String, String> mapGet = new HashMap<>();try{fis=new FileInputStream(fileCompress); int keyNumber = fis.read(); //读取key的数量String key = ""; //HashMap的键值对String code= ""; //未去0的字符串String codeRZ="";//去0的字符串int length=0; //表示还原后的字符串的理论长度,解决字符串前面的0的问题int byteNum=1; //当前code占了几个字节int addZero=0; //补0数int value=0; //临时储值int zeroLength=0;//code没有1的时候的字符串长度for(int i=0;i<keyNumber;i++){key = (char)fis.read()+""; //获取key值byteNum=fis.read(); //获取code的字节数addZero=fis.read(); //读取补0数量if(addZero==0) //没有补0,是整字节数{for(int k=byteNum-1;k>=0;k--){value+=fis.read()*(Math.pow(2, k*8));}code=IntToString(value);//把数值转为01codevalue=0;//清零length=8*byteNum-code.length();//计算在前面要补多少0if(code.length()==0)  //若code内数字都为0,只要去掉尾部即可{zeroLength=length-addZero;  //计算有多少个0for(int k=0;k<zeroLength;k++){codeRZ+="0";}}else    //code值不为0,补充前面的0,去掉后面的0{for(int k=0;k<length;k++){codeRZ+="0";}for(int k=0;k<code.length()-addZero;k++){codeRZ+=code.charAt(k);} }}else  //有补0{for(int k=byteNum-1;k>=0;k--){value+=fis.read()*(Math.pow(2, k*8));}code=IntToString(value);//把数值转为01codevalue=0;//清0length=8*byteNum-code.length();//计算在前面要补多少0                if(code.length()==0)  //若code内数字都为0,只要去掉尾部即可{zeroLength=length-addZero;  //计算有多少个0for(int k=0;k<zeroLength;k++){codeRZ+="0";}}else   //code值不为0,补充前面的0,去掉后面的0{for(int k=0;k<length;k++){codeRZ+="0";}for(int k=0;k<code.length()-addZero;k++) //不要后面的0{codeRZ+=code.charAt(k);}}                    }       mapGet.put(codeRZ , key ); //把读取到的键值对存入创建的HashMapcodeRZ=""; //清空}}catch(IOException e){e.printStackTrace();}return mapGet;}  /*** 获取压缩文件中的数据,还原哈夫曼编码01串*/public String readHFMStr(){String str1=""; //存放获取到的直接的01字符串String str=""; //存放去掉补0的字符串int value=0;String temp="";try{int addZero = fis.read(); //读取整个文件的补0个数          while((value=fis.read())!=-1){temp=IntToStringEight(value); //把每个字节的数据转化为八位的01str1+=temp;       }if(addZero!=0) //有补0,获取补0前的字符串{for(int i=0;i<str1.length()-addZero;i++) //补0的部分不赋值str+=str1.charAt(i)+""; return str;  }fis.close();}catch(IOException e){e.printStackTrace();}    return str1;}   /*** 写入文件的保存路径(写文件)* @param str* @param mapGet* @param fileCompress*/public void writeFile(String str , HashMap<String, String> mapGet,File fileCompress){try{fos = new FileOutputStream(fileCompress); //获取文件输出流int len = str.length();//获取01串的长度String temp=""; //临时存放段的01字符串for(int i=0;i<len;i++){temp+=str.charAt(i);if(mapGet.containsKey(temp)){fos.write(mapGet.get(temp).charAt(0)); //一个字符的字符串转字符然后写出temp="";                  }}fos.close();}catch(IOException e){e.printStackTrace();}}  /*** 对外部的接口,实现解压文件,获取HashMap和文件内容* @param fileCompress,压缩文件目录* @param fileUncompress,解压到的目录*/public void uncompressFile(File fileCompress,File fileUncompress){HashMap<String, String> mapGet = readHashMap(fileCompress); //获取哈希表String str = readHFMStr();  //获取01字符串writeFile(str,mapGet,fileUncompress);  //写文件到保存路径}
}

压缩、解压效果

1、压缩文件所占内存小于原文件,解压后的文件和原文件大小相同。如图data1是原文件,data2是压缩文件,data3的解压后的文件。我们可以发现压缩后的压缩包所占内存3KB<5KB。

2、原文件和解压后的文件的内容展示:
data1.txt:

data3.txt:

解压后txt的信息和原文件完全一致。

哈夫曼编码实现文件的压缩和解压相关推荐

  1. 基于哈夫曼编码对文件进行压缩和解压缩(详细讲解)

    基于哈夫曼编码对文件进行压缩和解压缩(详细讲解) 本文对应c++代码实现链接 一.背景 利用特定的算法来压缩数据的工具,压缩后生成的文件称为压缩包.如果想使用其中的数据,就得用压缩软件对数据进行解压. ...

  2. 使用哈夫曼编码实现数据的压缩和解压(java版)

    1.哈夫曼树 哈夫曼编码使用哈夫曼树的数据结构,哈夫曼树图解如下,即构造一个带权路径最小的数: 2.哈夫曼编码 使用哈夫曼树生成哈夫曼编码,已实现减少传输中数据的冗余:截取网络课程中的几张图来说明: ...

  3. 哈夫曼编码及文本文件的压缩解压(c++SourceCode)

    哈夫曼编码是一种编码方式,是可变字长编码(VLC)的一种.以哈夫曼树-即最优二叉树,带权路径长度最小的二叉树,经常应用于数据 压缩. 在计算机信息处理中,"哈夫曼编码"是一种一致性 ...

  4. 使用哈夫曼编码实现文件压缩__win10,c++

    系统:win10 工具:vc6.0 //我加了个计时,用int存储字符总数 //因此增加一个限制:文件不可大于2GB #include<iostream> #include<time ...

  5. 哈夫曼编码与文件压缩

    一:哈夫曼树与哈夫曼编码 大家知道,文件压缩的原理么? 假如我们有一个文件,文件当中仅有 A.B.C.D.E 五种字符,这五种字符出现的频率分别为 5次.4次.3次.2次.1次. 我们知道每个英文字母 ...

  6. 哈夫曼树实现文件的压缩与解压缩

    利用哈夫曼树实现文件的压缩与解压缩 压缩: 1.统计出文件中相同字符出现的次数 2.获取哈夫曼编码 次数作为权值构建哈夫曼树 3.重新编码,写回压缩文件 保存头文件: 源文件后缀 编码信息的行数 每个 ...

  7. 哈夫曼编码解压缩文件 - Java实现

    文章目录 前言 一.文件压缩 二.文件解压 结语 前言 不了解哈夫曼树的可以移步查看我的另一篇博客:哈夫曼树(最优二叉树) 使用哈夫曼编码压缩文件,其实就是将每个字符的哈夫曼编码得到,每8位转为一个字 ...

  8. 【Linux】文件的压缩和解压

    欢迎来到博主 Apeiron 的博客,祝您旅程愉快 ! 时止则止,时行则行.动静不失其时,其道光明. 目录 1.压缩格式 2.压缩软件 3.tar  命令简介 4.tar  命令压缩 5.总结 1.压 ...

  9. Linux下文件的压缩和解压

    文件的压缩和解压 Gzip格式的:    tar   -czvf    要存的名字.tar.gz     要打包的东西或目录 bzip2格式的:    tar   -cjvf 要存的名字.tar.bz ...

最新文章

  1. 来做一做你是三国里的哪一个人物呢
  2. Spring MVC 函数式编程进阶
  3. 【干货】运维需要掌握的 17 个实用技巧
  4. idea安装sbt插件linux,【idea】kafkasbt+idea安装配置与测试
  5. Netty工作笔记0008---NIO的Buffer的机制及子类
  6. 原型化系统---失物招领APP
  7. Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'userinfo.
  8. 数据库优先生成EF CRUD演示
  9. Pycharm连接Mysql问题: Server returns invalid timezone. Go to 'Advanced' tab and set 'serverTimezon
  10. 最好的免费在线UML图表工具
  11. 按键精灵定位坐标循环_[按键精灵手机版教程]DNF遍历背包卖物
  12. ASO优化方法_获取ASO关键词指数接口
  13. 【转】死链-百度百科
  14. MongoBD命令大全
  15. 对一批编号为1~100,全部开关朝上(开)的灯进行以下操作:凡是1的倍数反方向拨一次开关;2的倍数反方向又拨一次开关;3的倍数反方向又拨一次开关……问:最后为关熄状态的灯的编号。
  16. [NWERC 2019] E. Expeditious Cubing 浮点数精度判断
  17. 中国10大PCBA加工厂商排名
  18. 基于C90标准的C语言开发工具
  19. 荣耀30可以升级鸿蒙系统,惊喜!荣耀手机也能升鸿蒙:这5款机型用户有福了
  20. [SAP顾问之路] ​MM货源清单及配额协议-阿龙学习MM PA 笔记(2)

热门文章

  1. 基于android的车辆违章停放执法移动APP(ssm+uinapp+Mysql)-计算机毕业设计
  2. node.js爬虫之下载图片,批量下载图片,控制下载图片并行上限
  3. 计算机医学英语论文,医学英语论文.doc
  4. ES9JavaScript新特性
  5. 硬件系列(九)--------串口扫码头数据读写
  6. idea设置背景颜色为绿色,保护眼睛
  7. java中KMP模式_朴素模式匹配算法、kmp模式匹配算法、kmp模式匹配算法改进。java代码...
  8. H5,JS仿微信输入法,键盘上面带input输入框,兼容安卓、ios
  9. 汽车方向盘助力转向器如何接线_汽车方向盘锁死了怎么办?如何解开方向盘锁...
  10. Flink读写系列之-读HBase并写入HBase