Huffman压缩解压的思考

转自于http://imba-marlboro.iteye.com/blog/889143

历时10多天,文件压缩终于能用了。从最开始的构造huffman树到最后的解压缩,遇到了不少纠结的难题。下面就和大家分享一下做文件压缩的心得。 

          1.构建huffman树。由于数据结构基础有限,也想不到高明的压缩方法,便只能用书上的一种简单而实用的方法---huffman编码。Huffman树是一种特殊的二叉树,我把构建它分成了4部,建立了4个方法:
          a. 统计传入数据中每个字符出现的次数,这个数据可以是一个文件,也可以是一个字符串。 字符作为key,次数作为value存入一个map中
          b.将上面得到的map作为参数,建立一个由TreeNode组成的 ArrayList其中map的key值作为TreeNode的flag,map中的value作为TreeNode的weight。
          c.上面一步便得到了由huffman树叶子节点组成的ArrayList,然后就是建树了,由于huffman树的叶结点的个数为n,那么它的中间结点的个数肯定是n-1,由次每次对ArrayList由小到大排序,取出前面的最小值和次小值,新建一个结点,对这3个结点进行连接,然后新节点入队,前面两个出队,循环n-1次最终可得到连接好的huffman树。
          d.将得到的huffman树进行编码,得到编好码的huffman树。
          这4步c和d是最难的,d步的代码想了很久也没有想出来,最后还是问了别人才搞到递归算法,后来自己还设计了一种非递归的算法,递归和非递归的代码如下:
Java代码 复制代码  收藏代码
  1. /**  
  2.   * 先序遍历huffman树,并得到其编码(递归算法)  
  3.   * @param root: 要遍历的huffman树  
  4.   * @param hfmCode: 所遍历结点的huffman编码  
  5.   */  
  6. public void preVisitTree(TreeNode root , String hfmCode){   
  7.     //如果根不为0   
  8.     if(root != null){   
  9.         root.setHuffmanCode(hfmCode);   
  10.         String lCode = hfmCode + "0";   
  11.         preVisitTree(root.getLchild() , lCode);   
  12.         String rCode = hfmCode+"1";   
  13.         preVisitTree(root.getRchild() , rCode);   
  14.     }   
  15. }  
/**
  * 先序遍历huffman树,并得到其编码(递归算法)
  * @param root: 要遍历的huffman树
  * @param hfmCode: 所遍历结点的huffman编码
  */
public void preVisitTree(TreeNode root , String hfmCode){
	//如果根不为0
	if(root != null){
		root.setHuffmanCode(hfmCode);
		String lCode = hfmCode + "0";
		preVisitTree(root.getLchild() , lCode);
		String rCode = hfmCode+"1";
		preVisitTree(root.getRchild() , rCode);
	}
}

Java代码 复制代码  收藏代码
  1. /**  
  2.   * 非递归获取哈夫曼编码  
  3.   * @param root: 哈夫曼树根节点  
  4.   */  
  5. public void setHfmCode(TreeNode root){   
  6.     java.util.ArrayList<TreeNode> treeList = new java.util.ArrayList<TreeNode>();   
  7.     treeList.add(root);   
  8.     TreeNode tempNode;//临时引用   
  9.     String hfmCode = "";   
  10.     //对huffman树层次遍历   
  11.     while(!treeList.isEmpty()){   
  12.         tempNode = treeList.remove(0);//头元素出队进行运算   
  13.         hfmCode = tempNode.getHuffmanCode();   
  14.         if(tempNode.getLchild() != null){   
  15.             treeList.add(tempNode.getLchild());   
  16.             hfmCode = tempNode.getHuffmanCode();   
  17.             tempNode.getLchild().setHuffmanCode(hfmCode + "0");   
  18.         }   
  19.         if(tempNode.getRchild() != null){   
  20.             treeList.add(tempNode.getRchild());   
  21.             hfmCode = tempNode.getHuffmanCode();   
  22.             tempNode.getRchild().setHuffmanCode(hfmCode + "1");   
  23.         }   
  24.     }   
  25. }  
/**
  * 非递归获取哈夫曼编码
  * @param root: 哈夫曼树根节点
  */
public void setHfmCode(TreeNode root){
	java.util.ArrayList<TreeNode> treeList = new java.util.ArrayList<TreeNode>();
	treeList.add(root);
	TreeNode tempNode;//临时引用
	String hfmCode = "";
	//对huffman树层次遍历
	while(!treeList.isEmpty()){
		tempNode = treeList.remove(0);//头元素出队进行运算
		hfmCode = tempNode.getHuffmanCode();
		if(tempNode.getLchild() != null){
			treeList.add(tempNode.getLchild());
			hfmCode = tempNode.getHuffmanCode();
			tempNode.getLchild().setHuffmanCode(hfmCode + "0");
		}
		if(tempNode.getRchild() != null){
			treeList.add(tempNode.getRchild());
			hfmCode = tempNode.getHuffmanCode();
			tempNode.getRchild().setHuffmanCode(hfmCode + "1");
		}
	}
}
     
         2.压缩文件到指定目录。至此,压缩的第一部就已经完成,我们得到了一个由file中所有字节作为flag,其个数作为weight的带huffman编码的树。
          我们压缩后的文件应该分为两部分:码表和压缩体。
          码表是一个由字节作为key,字节对应的huffman编码作为value的map对象。个人认为如果将码表压缩在文件前端,对解压缩应该要方便一些。因为解压缩一开始就可以把码表读出来保存在map对象中。码表压缩时第一位应该保存一个int值,是map的size()。而且压缩码表的时候可以用一个小技巧,压缩时的码表是map<Byte , String>由于解压缩是是用字符串找字节,而map中的方法containsKey()和get()都只能是由key值找value值,为了方便可以先存value(String)再存key(Byte),当然也可以先存key(Byte)再存value(String)上一部就留给解压缩码表时完成。值得注意的是无论先存什么,为了后来操作方便都应该在存入String前存入一个int值是这个String(huffman)编码的长度。
      压缩压缩体的时候也有许多小技巧,为了方便操作,第一个存入的数据可以是这个压缩体的大小。而第二个则是字符串末尾补零的个数,因为将文件的所有字节转换为一个字符串,这个串不一定就是8的倍数,最后很有可能要补0,为此我专门写了一个方法来计算压缩体的大小和补零的个数,利用的则是层次遍历第一部的到的huffman树,代码如下:
Java代码 复制代码  收藏代码
  1. /**  
  2.   * 得到补零的个数和压缩后文件的字节数   
  3.   * @param src:源文件地址  
  4.   * @param root:压缩用huffman树根节点  
  5.   */  
  6. public void getCompressInfo(String src, TreeNode root) {   
  7.     // 辅助计数器   
  8.     int tempCount = 0;    
  9.     // 建立一个结点队列   
  10.     java.util.ArrayList<TreeNode> treeList = new java.util.ArrayList<TreeNode>();   
  11.     treeList.add(root);    
  12.     TreeNode tempNode;// 临时变量   
  13.     while (!treeList.isEmpty()) {   
  14.         tempNode = treeList.remove(0);   
  15.         // 如果是叶子结点,则累加其1,0个数   
  16.         if (tempNode.getLchild() == null && tempNode.getRchild() == null) {   
  17.             // 统计补零个数   
  18.             numOfZero += tempNode.getWeight() * tempNode.getHuffmanCode().length();   
  19.             numOfZero = numOfZero % 8;   
  20.             // 统计字节数   
  21.             tempCount += tempNode.getWeight() * tempNode.getHuffmanCode().length();   
  22.             count += tempCount / 8;   
  23.             tempCount = tempCount % 8;   
  24.         }   
  25.         // 如果有左右孩子则入队   
  26.         if (tempNode.getLchild() != null) {   
  27.             treeList.add(tempNode.getLchild());   
  28.         }   
  29.         if (tempNode.getRchild() != null) {   
  30.             treeList.add(tempNode.getRchild());   
  31.         }   
  32.     }   
  33.     if (tempCount != 0) {   
  34.         count++;   
  35.     }   
  36.     numOfZero = 8 - numOfZero;   
  37. }  
/**
  * 得到补零的个数和压缩后文件的字节数 
  * @param src:源文件地址
  * @param root:压缩用huffman树根节点
  */
public void getCompressInfo(String src, TreeNode root) {
	// 辅助计数器
	int tempCount = 0; 
	// 建立一个结点队列
	java.util.ArrayList<TreeNode> treeList = new java.util.ArrayList<TreeNode>();
	treeList.add(root); 
	TreeNode tempNode;// 临时变量
	while (!treeList.isEmpty()) {
		tempNode = treeList.remove(0);
		// 如果是叶子结点,则累加其1,0个数
		if (tempNode.getLchild() == null && tempNode.getRchild() == null) {
			// 统计补零个数
			numOfZero += tempNode.getWeight() * tempNode.getHuffmanCode().length();
			numOfZero = numOfZero % 8;
			// 统计字节数
			tempCount += tempNode.getWeight() * tempNode.getHuffmanCode().length();
			count += tempCount / 8;
			tempCount = tempCount % 8;
		}
		// 如果有左右孩子则入队
		if (tempNode.getLchild() != null) {
			treeList.add(tempNode.getLchild());
		}
		if (tempNode.getRchild() != null) {
			treeList.add(tempNode.getRchild());
		}
	}
	if (tempCount != 0) {
		count++;
	}
	numOfZero = 8 - numOfZero;
}

     存好这两个数据以后则将源文件中的字节一个个的读出来,读出一个,从map中找到其对应的huffmanCode,一步步累加,当这个huffmanCode大于等于8的时候则将前八位转换成一个int(这里可以自己写方法也可以用效率更高的Java API方法Integer.parseInt),写入后八位。然后截取掉前八位,这两个步骤都可以用String类中的substring方法。
3.将文件解压缩。 解压缩是压缩的逆过程,但比压缩更加纠结。解压缩第一步就是读出码表将其保存如一个map<String , Byte>中。
读完码表后便可以对压缩体解压。解压时需要注意的是要和压缩时压缩进去的数据一一对应。错一点点就会产生蝴蝶效应,全部解错。另外和压缩一样,解压缩也可以读一个换成String然后在码表中找到对应的字节,剩下的找不到的前加到下一个String,其中最要注意的就是在用Integer.toBinaryString时,前面位的0可能丢失,如果得到的字符串不足八位,则前面一定要补零。实现这一步的关键代码如下:
Java代码 复制代码  收藏代码
  1. // 处理前面所有未补零的字节   
  2. while (count > 1) {   
  3.     b = dis.read();//从压缩文件中读取第一个压缩体的字节      String tempStr = Integer.toBinaryString(b);// 得到压缩后的字节,并将其转化为字符串   
  4.     int strLen = tempStr.length();//如果tempStr不足八位.前面补零   
  5.     if(strLen < 8){   
  6.         for(int i = 0 ; i < 8-strLen ; i++){   
  7.             tempStr = "0" + tempStr;   
  8.         }   
  9.     }   
  10.     str = str + tempStr;   
  11.     count--;   
  12.     // 一位一位的查找map中是否由对应的字节,有则写入   
  13.     while (str.length() != 0) {   
  14.         if (str.length() == 1) {   
  15.             firstCode += str;   
  16.             str = "";   
  17.         }   
  18.         if (str.length() > 1) {   
  19.             firstCode += str.substring(01);   
  20.             str = str.substring(1);   
  21.         }   
  22.         if (map.containsKey(firstCode)) {   
  23.             dos.write(map.get(firstCode));// 得到串对应的value值,并写入   
  24.             firstCode = "";// 将firstCode重新初始化   
  25.         }   
  26.     }   
  27. }   
  28.   
  29. if(numOfZero != 0){//处理最后的补零位   
  30.     b = dis.read();   
  31.     String tempStr = Integer.toBinaryString(b);   
  32.     if(tempStr.length() < 8){   
  33.         int k = 8 - tempStr.length();   
  34.         for(int i = 0 ; i < k ; i++){   
  35.             tempStr = "0" + tempStr;   
  36.         }   
  37.     }   
  38.     tempStr = tempStr.substring(0,8 - numOfZero);   
  39.     str = str+tempStr;   
  40.     for(int i = 0 ; i < tempStr.length() ; i++){   
  41.         // 一位一位的查找map中是否由对应的字节,有则写入   
  42.         if(str.length() == 1){   
  43.             firstCode += str;   
  44.             str = "";   
  45.         }   
  46.         if (str.length() > 1) {   
  47.             firstCode += str.substring(0,1);   
  48.             str = str.substring(1);   
  49.         }   
  50.         if (map.containsKey(firstCode)) {   
  51.             dos.write(map.get(firstCode));// 得到串对应的value值,并写入   
  52.             firstCode = "";// 将firstCode重新初始化   
  53.         }   
  54.     }   
  55. }  
// 处理前面所有未补零的字节
while (count > 1) {
	b = dis.read();//从压缩文件中读取第一个压缩体的字节		String tempStr = Integer.toBinaryString(b);// 得到压缩后的字节,并将其转化为字符串
	int strLen = tempStr.length();//如果tempStr不足八位.前面补零
	if(strLen < 8){
		for(int i = 0 ; i < 8-strLen ; i++){
			tempStr = "0" + tempStr;
		}
	}
	str = str + tempStr;
	count--;
	// 一位一位的查找map中是否由对应的字节,有则写入
	while (str.length() != 0) {
		if (str.length() == 1) {
			firstCode += str;
			str = "";
		}
		if (str.length() > 1) {
			firstCode += str.substring(0, 1);
			str = str.substring(1);
		}
		if (map.containsKey(firstCode)) {
			dos.write(map.get(firstCode));// 得到串对应的value值,并写入
			firstCode = "";// 将firstCode重新初始化
		}
	}
}

if(numOfZero != 0){//处理最后的补零位
	b = dis.read();
	String tempStr = Integer.toBinaryString(b);
	if(tempStr.length() < 8){
		int k = 8 - tempStr.length();
		for(int i = 0 ; i < k ; i++){
			tempStr = "0" + tempStr;
		}
	}
	tempStr = tempStr.substring(0,8 - numOfZero);
	str = str+tempStr;
	for(int i = 0 ; i < tempStr.length() ; i++){
		// 一位一位的查找map中是否由对应的字节,有则写入
		if(str.length() == 1){
			firstCode += str;
			str = "";
		}
		if (str.length() > 1) {
			firstCode += str.substring(0,1);
			str = str.substring(1);
		}
		if (map.containsKey(firstCode)) {
			dos.write(map.get(firstCode));// 得到串对应的value值,并写入
			firstCode = "";// 将firstCode重新初始化
		}
	}
}

     由这三步,压缩基本就可以实现了,在这次做压缩的过程中,遇到了许多的bug,我们应当直面惨淡的bug,不断的System.out.println()测试每一步数据,找到他再仔细思考,找到一个修改一个。让写出的程序可以针对每一种特殊情况的发生。  
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值