哈夫曼压缩总结

实现哈弗曼压缩的步骤:
一.读取文件   
     保存每一个字符对应的出现次数,该步骤利用数组和字符对应的ACISS值来存储,字符对应的ACISS值作为数组的下标,字符没出现一次,相应下标的数组元素加一。
代码如下:
try {
			// 实例化文件输入流对象
			FileInputStream fis = new FileInputStream(path);
			while (fis.available() > 0) {
				// 开始读取文件
				int i = fis.read();
				// System.out.print(i+" ");
				// 读到的整形数据相应的下标加一
				ByteCount[i]++;
			}
			//关闭输入流
			fis.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}


二.排序   
      根据相应字符对应的出现次数来排序,该步骤实现利用优先队列,由于后面建树的需要所以存入队列的相应字符实例化的节点对象。
   节点属性包括:
           1.父节点
           2.左子节点、右子节点、父节点
           3.节点对应的ACISS值
           4.原始的哈夫曼编码
           5.补0后的哈夫曼编码(由于写头文件需要,最小单位是一个字节,所以当哈弗吗编码不是8的倍数,就要补0)
           6.哈夫曼编码补0的个数(后面再读取头文件的时候要,要还原哈夫曼编码,所以在写头文件时必须写入每个编码对应的补0个数)
/**
 * 
 * 树的节点类
 *
 */
public class TreeNode implements Comparable<TreeNode>{
	private TreeNode left;//声明树的左子节点属性
	private TreeNode right;//声明树的右子节点属性
	private TreeNode parent;//声明树的父节点属性
	private int count;//声明字符出现的次数
	private int ACISS;//声明节点的数据属性,即字符对应的ACISS值
	private String hfmcode="";//声明节点对应的哈弗曼编码
	private int o_count=0;//声明节点编码个数变成八的倍数要添加的0的个数
	private int code_length=0;//声明编码长度属性
	private String HFMCode="";//未加0前的哈夫曼编码
	public String getHFMCode() {
		return HFMCode;
	}
	public void setHFMCode(String code) {
		HFMCode = code;
	}
	/**
	 * 获取节点对应的ACISS值
	 * @return返回ACISS值
	 */
	public int getACISS() {
		return ACISS;
	}
	/**
	 * 
	 * @param aciss
	 */
	public void setACISS(int aciss) {
		ACISS = aciss;
	}
	/**
	 * 获取编码长度
	 * @return编码长度属性
	 */
	public int getCode_length() {
		return code_length;
	}
	/**
	 * 设置编码长度属性
	 * @param code_length新的编码长度属性
	 */
	public void setCode_length(int code_length) {
		this.code_length = code_length;
	}
	/**
	 * 获取节点对应的哈弗曼编码
	 * @return返回哈弗曼编码
	 */
	public String gethfmCode() {
		return hfmcode;
	}
	/**
	 * 设置节点对应的哈弗曼编码
	 * @param 新的哈弗曼编码
	 */
	public void setCode(String code) {
		this.hfmcode = code;
	}
	/**
	 * 获取节点哈弗曼编码添加0的个数
	 * @return返回添加0的个数
	 */
	public int getO_count() {
		return o_count;
	}
	/**
	 * 设置节点对应编码添加0的个数
	 * @param o_count新的0的个数值
	 */
	public void setO_count(int o_count) {
		this.o_count = o_count;
	}
	/**
	 * 构造方法
	 * @param left左子节点
	 * @param right右子节点
	 * @param parent父节点
	 * @param object节点数据
	 */
	public TreeNode(TreeNode left, TreeNode right, TreeNode parent,
			int count) {
		this.left = left;
		this.right = right;
		this.parent = parent;
		this.count=count;
	}
	/**
	 * 构造方法
	 * @param object节点数据
	 */
	public TreeNode(int ACISS ,int count) {
		this.ACISS=ACISS;
		this.count=count;
	}
	/**
	 * 	获取左子节点的方法
	 * @return左子节点
	 */
	public TreeNode getLeft() {
		return left;
	}
	/**
	 * 设置左子节点的方法
	 * @param left
	 */
	public void setLeft(TreeNode left) {
		this.left = left;
	}
	/**
	 * 获取右子节点的方法
	 * @return右子节点
	 */
	public TreeNode getRight() {
		return right;
	}
	/**
	 * 设置右子节点的方法
	 * @param right
	 */
	public void setRight(TreeNode right) {
		this.right = right;
	}
	/**
	 * 获取父节点的方法
	 * @return返回父节点
	 */
	public TreeNode getParent() {
		return parent;
	}
	/**
	 * 设置父节点的方法
	 * @param parent
	 */
	public void setParent(TreeNode parent) {
		this.parent = parent;
	}
	/**
	 * 获取节点数据的方法
	 * @return返回节点数据
	 */
	public int getCount() {
		return count;
	}
	/**
	 * 设置节点数据的方法
	 * @param object
	 */
	public void setCount(int count) {
		this.count=count;
	}
	/**
	 * 获取数据的方法
	 * @param data数据
	 * @return返回数据
	 */
	public int getData(int data){
		return data;
	}
	
	

	public int compareTo(TreeNode o) {
		// TODO Auto-generated method stub
		return count - o.getCount();
	}
}


三.构建哈弗曼树   
      将前面排好的序列,每次取出出现次数最少的对应的字符的两个节点对象,(取出两个对象后该队列也就删除了这两个对象),实例化两个节点的父节点,该节点中对应的出现次数是两个子节点的和,并将该父节点加入队列中,(由于优先队列每次删除加入新的元素后都能自动排序,后面只要一直去执行这个步骤就能把哈弗曼树建好了)。
四.获取叶子节点对象的哈弗曼编码
       从根节点开始到对应的每一个 叶子节点,以左0右1的规则便可获得每个叶子节点所对应的哈弗曼编码,并利用队列来保存这些节点(由于优先队列中的节点在建树时已经被删除,后面这些很多地方都要用到这些叶子节点,所以要实例化一个队列来保存)
public void creat_tree() {
		// 调用实例化节点和往队列中添加节点的方法
		CreatNode();
		// 开始建树
		while (nodeQueue.size() > 1) {
			// 取出队列中出现次数最少的两个节点
			TreeNode minnode1 = nodeQueue.poll();
			//System.out.print(minnode1.getCount() + "   ");
			TreeNode minnode2 = nodeQueue.poll();
			//System.out.print(minnode2.getCount() + "\n");
			// 实例化一个新的节点作为上面两个节点的父节点
			TreeNode parentnode = new TreeNode(0, minnode1.getCount()+ minnode2.getCount());
			parentnode.setLeft(minnode1);
			parentnode.setRight(minnode2);
			minnode1.setParent(parentnode);
			minnode2.setParent(parentnode);
			//System.out.println(parentnode.getCount());
			// 把父节点添加到队列中
			nodeQueue.add(parentnode);
		}
		// 取出剩下的一个节点作为根节点
		root = nodeQueue.peek();
		//设置根节点的一些属性,防止由于只有一个节点的时候出现错误
		root.setCode_length(1);
		root.setHFMCode("1");
		root.setCode("1");
		root.setO_count(7);
		System.out.println("+++++++++++"+root.getACISS());
	}


五.向压缩文件中写入
1.头文件的写入 
       写入信息包括  1.头文件长度
2.每个叶子节点对应的ACISS(源文件的字符)
3.每个叶子节点对应的补0后的哈夫曼编码对应的Byte数,(由几个字节组成)
4.每个叶子节点对应的哈夫曼编码(补0后的哈夫曼编码,补了0字符串长度变长8的倍数,按字节写入)
5.每个叶子节点哈夫曼编码对应的补0个数(便于后面读取头文件编码的还原)
/**
			 *文件头信息  
			 *
			 *文件头长度  源文件补0个数
			 *字符对应的ACISS值  字符对应编码Byte个数
			 *字符对应的编码补0后按一个整形写入  编码补0个数
			 *  
			 */
			try {
				//定义头文件长度的变量
				int headfile_length = 0;
				//计算出文件头长度
				for(int j=0;j<arraylist.size();j++){
					headfile_length+=(3+arraylist.get(j).gethfmCode().length()/8);
				}
				System.out.println("文件头长度是:"+headfile_length);
				fos.write(headfile_length);//写入头文件长度
				
				for (int i=0; i < arraylist.size(); i++) {
					fos.write(arraylist.get(i).getACISS());// 写入对应的字符
					System.out.println("写入的ACISS是:"+arraylist.get(i).getACISS());
					// 写入对应编码byte个数
					fos.write((arraylist.get(i).getCode_length() + arraylist.get(i).getO_count())/8);
					System.out.println("写入的Byte数时:"+(arraylist.get(i).getCode_length() + arraylist.get(i).getO_count())/8);
					// System.out.println("<><><><><><><><><>");
					//如果编码长度大于8,每次写入8个为一个字节
					if (arraylist.get(i).gethfmCode().length()>8) {
						int k = 0;
						while (k<arraylist.get(i).gethfmCode().length()) {
							String string = "";
							for (int n=k; n<k+8;n++) {
								string += arraylist.get(i).gethfmCode().charAt(n);
							}
							System.out.println("<><><>" + string);
							k+=8;
							System.out.println("写入的编码补0后的编码是"+string+"   变成整数是:"+string_to_int(string));
							fos.write(string_to_int(string));// 写入前八位编码
						}
					}else{
						System.out.println("写入的编码补0后的编码是"+arraylist.get(i).gethfmCode()+"   变成整数是:"+string_to_int(arraylist.get(i).gethfmCode()));
						fos.write(string_to_int(arraylist.get(i).gethfmCode()));
					}
					fos.write(arraylist.get(i).getO_count());// 写入编码补0个数
					System.out.println("写入的补0个数是;"+arraylist.get(i).getO_count());
				}
			


2.源文件信息的写入(这边才真正的实现压缩理)            读取源文件每个字符对应的ACISS,然后遍历存储节点的队列,找出和该ACISS值相等对应下的节点,获取该节点对应的原始哈夫曼编码(未补0的哈夫曼编码),若该编码长度大于等于8,读取前八位按一个字节写入文件,若不足8位,再读取源文件下一个字符对应叶子节点下的哈夫曼编码,加到前面的编码后,再做长度判断。直至把源文件全部写入。
           注意点:注意最后读取的源文件尾字符,或者读取的源字符对应的哈夫曼编码和前面编码相连的长度不是8的倍数,就要补0填成8的倍数再写入。记录文件末尾补0个数,并写入文件。(便于后面的文件还原)
           压缩原理:由于哈夫曼树的构建获取的每个叶子节点对应的编码有这样的特点:
                      1.字符在源文件中出现次数多的,对应的哈夫曼编码长度就比较短(比8要少),字符出现次数的对应的字符长度相对就较长一点。但出现比8大的情况比较少。平衡下来整个文件对应的01串便会减短。这样也就实现了源文件的压缩。
                     2.由于添加了一个头文件,所以这边刚接触时难免有一些困惑,但由于头文件只有一个最多也就0—255都有。所以对相对比较大的文件的压缩这个是毫不影响的。但有些情况这种压缩方法是实现不了压缩的,比如说一个字符的文件,源文件一个字节,按我这种做法压缩后的文件变成了6个字节。
当然这种情况在压缩中是不值一提的,我们之所以要实现压缩是用来压缩大文件的,小文件压它干嘛,你说是吧?

         由于java中没有提供二进制写入的方法,所以要将每一个字节对应的哈弗曼编码转换成字节才能写入,一个字节八个位,但并不是所有字符对应的哈弗曼编码都刚刚好是八的整数倍,所以必须给每个字符对象的哈弗曼编码补0使其变成8的整数倍,这样就能用字节的形式往文件中写入。每次都要计入相应每个字符补0的个数,方便解压之用。
	
			/**
			 * 
			 * 开始写入源文件内容
			 * 
			 * 
			 */
				String HFMcode="";//定义从源文件读取的字节对应的哈夫曼编码变量
				int ACISS;
				System.out.println("****************************");
				while(fis.available()>0){
					 ACISS=fis.read();
					String write="";//定义要写入的01串变量
					//遍历队列,找出和读到的值相等的对象的节点对象,获取哈夫曼编码,写入头文件
					for(int j=0;j<arraylist.size();j++){
						TreeNode node=arraylist.get(j);//获取队列中的节点
						//如果源文件字符与
						if(node.getACISS()==ACISS){
							//获取对应的哈夫曼编码
							HFMcode=HFMcode+node.getHFMCode();
							//System.out.println("编码是:"+HFMcode);
							//System.out.println("编码是"+HFMcode);
							//System.out.println("现在编码的长度是:"+HFMcode.length()+"  "+"编码是"+HFMcode);
							//System.out.println("对应的哈夫曼编码是:"+node.getACISS()+"   "+node.getHFMCode());
							if(HFMcode.length()>=8){
								//获取前8位
								for(int k=0;k<8;k++){
									write=write+HFMcode.charAt(k);
								}
								//将前八位写入文件
								System.out.println("文件字节对应的8位转成int型写入的是:"+write);
								fos.write(string_to_int(write));
								//将原来编码的前八位删掉
								String str="";
								for(int k=8;k<HFMcode.length();k++){
									str=str+HFMcode.charAt(k);
								}
								HFMcode=str;
								}else{
								//如果小于8,在读取一个字节
								int ACISS2=fis.read();
								//System.out.println("不足8为在读取"+ACISS);
								for(int k=0;k<arraylist.size();k++){
									TreeNode node2=arraylist.get(k);
									if(node2.getACISS()==ACISS2){
										HFMcode=HFMcode+node2.getHFMCode();
										//System.out.println("对应的哈夫曼编码是:"+node2.getACISS()+"   "+node2.getHFMCode());
										//System.out.println("补上后的编码"+HFMcode);
									}
								}
							}
						}
					}	
				}
				System.out.println("源文件最后一个编码是:"+HFMcode);
				if(HFMcode.length()%8!=0){
					file__0=(8-HFMcode.length()%8);
				}
				//文件末尾添加0,然后写入文件
					for(int i=0;i<file__0;i++){
						HFMcode=HFMcode+"0";
					}
				System.out.println("文件最后一个添0个数是"+file__0);
				System.out.println("文件的最后一个代码添加0后是:"+HFMcode);
				if(HFMcode!=""){
					fos.write(string_to_int(HFMcode));
				}
				//关闭文件流
				fis.close();
				fos.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

  
      以上五个步骤就实现了哈夫曼压缩。



解压
          解压就是一个逆过程,怎么向压缩文件写东西就怎么从压缩文件读取东西还原后写入解压文件。

步骤:
           1.读取头文件信息,(我们之前写入一开始就写入了文件头的长度,所以我们读出的第一个也就是文件头的长度,这个是非常重要的一个数据,我们可以知道头文件在什么地方截止)。读取原来写入头文件的ACISS值,在读取补0后的哈夫曼编码Byte数,用for循环读取Byte次就能读出相应ACISS对应的补完0的哈夫曼编码,在读取补0个数,去掉0还原哈夫曼编码。用数组存储。相应下标为ACISS的值,元素值为还原后的哈夫曼编码。这样文件头我们解压要用的信息就全部被存储在数组中了。
2.读取压缩文件内源文件信息,一次读取一个字节转换成相应的01串,用for循环每次读01串中的一个数(每次数累加)。遍历数组和数组中存储的哈夫曼编码做对比,若有相等的,将对应的下标写入解压文件,这样对应的字符便被写入了。写入后循环起始索引改成末尾索引。若没有相等的将原来的for循环的末尾索引加1让每次读取的01串比原来长1,在做对比。这样一直执行下去,直至读到末尾倒数第二个字节。最后读取最后一个字节,并读出末尾补0个数,去掉0还原编码,再执行上面的过程,这样解压就完成了。
public void read_and_write(){
		try {
			//实例化一个文件输出流对象,用来向解压后的文件写入内容
			FileOutputStream fos=new FileOutputStream(newpath);
			//实例化一个文件输入流对象用来读取压缩文件内容
			FileInputStream fis=new FileInputStream(y_path);
			//读取文件头长度
			int head_length=fis.read();
			System.out.println("读取的文件头长度是:"+head_length);
			/**
			 * 
			 * 
			 * 读取头文件的所有信息
			 */
			while(head_length>0){
				int ACISS=fis.read();//读取字符对应的ACISS值
				System.out.println("字符的ACISS:"+ACISS);
				int Byte_count=fis.read();//读取编码的Byte数
				System.out.println("哈夫曼编码字节数:"+Byte_count);
				String hfm_code_0="";
				for(int i=0;i<Byte_count;i++){
					hfm_code_0+=int_to_string(fis.read());
				}
				System.out.println("补0后的哈夫曼编码是:"+hfm_code_0);
				int count_0=fis.read();//读取补0个数
				String HFMcode="";//定义去掉0后的哈夫曼编码变量
				for(int j=0;j<hfm_code_0.length()-count_0;j++){
					HFMcode+=hfm_code_0.charAt(j);
				}
				System.out.println("去掉0后的哈夫曼编码是:"+HFMcode);
				array[ACISS]=HFMcode;//将哈夫曼编码和对应的字符的ACISS相应的保存在数组中
				head_length-=(3+Byte_count);
			}
			for(int i=0;i<array.length;i++){
				if(array[i]!=null){
					System.out.println("数组中ACISS:"+i+"  对应的哈夫曼编码是:"+array[i]);
				}
			}
			/**
			 * 读取源文件内容
			 * 
			 * 
			 */
			String file_01="";
			
			while(fis.available()>1){
				int count_writed_01=0;
				//读取的01字符串
				file_01+=int_to_string(fis.read());
				System.out.println("源文件的01串是:"+file_01);
				int k=0;
				int count=1;
				while(count<=file_01.length()){
					//定义哈夫曼编码变量
					String HFMcode="";
					for(int i=k;i<count;i++){
						HFMcode+=file_01.charAt(i);
					}
					for(int i=0;i<array.length;i++){
						if(array[i]!=null&&array[i].equals(HFMcode)){
							System.out.println("哈夫曼编码是"+HFMcode);
							count_writed_01+=HFMcode.length();
							System.out.println("写入了"+count_writed_01+"个");
							k=count;
							fos.write(i);
							System.out.println("内容写入了"+i);
						}
					}
					count++;
				}
				System.out.println("写入后的file_01是"+file_01);
				String file_01x="";
				for(int i=count_writed_01;i<file_01.length();i++){
					file_01x+=file_01.charAt(i);
				}
				file_01=file_01x;
				System.out.println("清空后的file_01是"+file_01);
			}
			//读取最后一个字符串
			String str_end=file_01+int_to_string(fis.read());
			System.out.println("最后一个字符串是"+str_end);
			int file__0=ya_suo.getFile__0();//获取问价末尾补0个数
			//把最后读取的字符串补的0去掉
			String str="";
			for(int i=0;i<str_end.length()-file__0;i++){
				str+=str_end.charAt(i);
			}
			str_end=str;
			System.out.println("把0删掉后变成:"+str_end);
			int count=1;
			int k=0;
			while(count<=str_end.length()){
				str="";
				for(int i=k;i<count;i++){
					str+=str_end.charAt(i);
				}
				System.out.println("str是:"+str);
				for(int i=0;i<array.length;i++){
					if(array[i]!=null&&array[i].equals(str)){
						System.out.println("&&&&&&&&&&&");
						fos.write(i);
						k=count;
					}
				}
				count++;
			}
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}


在压缩时涉及到将01串转化成整形,在解压时涉及到将整形转化成01串,这个可以看做是10进制和2进制的转化问题。
01串转化成整形
/**
	 * 将01字符串转成整形的方法
	 */
	public int string_to_int(String str) {
		return (str.charAt(0) - 48) * 128 + (str.charAt(1) - 48) * 64
				+ (str.charAt(2) - 48) * 32 + (str.charAt(3) - 48) * 16
				+ (str.charAt(4) - 48) * 8 + (str.charAt(5) - 48) * 4
				+ (str.charAt(6) - 48) * 2 + (str.charAt(7) - 48) * 1;
	}


整形转化成01串
/**
	 * 将整形数据转化成8个01串的方法
	 * @param Int
	 * @return
	 */
	public String int_to_string(int Int){
		String str="";
		if(Int==0){
			str="00000000";
		}else{
			while(Int!=0){
				int  mod=Int%2;
				str+=mod;
				Int/=2;	
			}
		}
		if(str.length()!=8){
			int count=8-str.length();
			for(int i=0;i<count;i++){
				str+=0;
			}
		}
		String str2="";
		for(int i=str.length()-1;i>=0;i--){
			str2+=str.charAt(i);
		}
		//System.out.println(">>>>>>>>>>>>>>>>"+Int+"变成字符串是:"+str2);
		return str2;
	}

听的、看的都很模糊,很乱。关键还是自己动手写。这样才能有真正的体会。

猜你喜欢

转载自lixiongzhi-m.iteye.com/blog/1613350