哈夫曼压缩

一、压缩

思路:一个文件中,都会出现重复的字节,有些字节出现的次数多,有些字节出现的次数少,这样我们就可以根据出现次数的多少,构造哈夫曼树,并进行编码,出现次数越多的编码长度越短,出现次数越少的编码长度越长。而我们有知道一个英文字母占用一个byte,一个中文占用两个byte。我们将文件中的字节化为对应的编码后,形成01串,每次取八个01串写入压缩后的文件。因为编码的长度大多情况下都比文件中数据占用bit数更少,所以就文件就压缩了。


具体步骤:

①、统计文件中字节出现的次数

②、根据次数构建哈弗曼树

③、获取每个结点的哈弗曼编码

④将字节用01字符数组表示

 

 

/**
 * 压缩编码表元素
 * @author hpw
 *
 */
public class CodeTableNode {
	private byte bt;
	private Byte[] code;
	/**
	 * 构造器,传入一个byte和代表这个byte的int数组code,code每个元素代表一个二进制位
	 * 只有0,1
	 * @param bt  传入的byte
	 * @param code2  对应byte的二进制编码
	 */
	public CodeTableNode(byte bt,Byte[] code2){
		this.bt=bt;
		this.code=code2;
	}
	public byte getByte() {
		return bt;
	}
	public Byte[] getCode() {
		return code;
	}
}

 

 

	/**
	 * 建立数组的方法(TreeNodeArray:类似优先队列的方法)
	 * 
	 * @return 根据byte出现次数进行排序的数组
	 * @throws IOException
	 */
	private TreeNodeArray getList() throws IOException {
		TreeNodeArray tList = new TreeNodeArray();
		// 创建输入输出流
		java.io.InputStream inputStr = new java.io.FileInputStream(file);
		java.io.BufferedInputStream bus=new java.io.BufferedInputStream(inputStr,2048);
		int bt=bus.read();
		while(bt!=-1){
			byte by=(byte)bt;
			TreeNode node = new TreeNode(by);
			tList.add(node);
			bt=bus.read();
		}
		inputStr.close();// 关闭输入流
		bus.close();
		
		return tList;
	}

	/**
	 * 通过节点队列创建二叉树,取出前2个节点组成二叉树,将根节点排序插入到队列中,
	 * 如此循环,最后剩下的就是根节点
	 * 
	 * @return 二叉树头节点
	 * @throws Exception
	 */
	public TreeNode getTree() throws Exception {

		TreeNodeArray list = getList();

		while (list.size() > 1) {
			TreeNode LNode = list.remove(0);
			TreeNode RNode = list.remove(0);

			TreeNode NNode = new TreeNode(LNode, RNode);
			
			list.insert(NNode);
		}
		TreeNode rootNode = list.get(0);
		
		return rootNode;
	}
	
	/**
	 * 生成编码表方法
	 * @return  CodeMap格式的编码表
	 * @throws Exception
	 */
	public CodeTable getMap() throws Exception{
		
		TreeNode rootNode=this.getTree();
		//遍历树
		TreeStack<Byte> rootStack=new TreeStack<Byte>();//堆栈记录路径
		CodeTable codes=new CodeTable();
		scanTree(rootNode,rootStack,codes);
		
		return codes;
	}
	
	/**
	 * 遍历树的方法,递归
	 * @param root  节点
	 * @param rootStk  堆栈记录,存储路径
	 * @param map   编码表
	 */
	private void scanTree(TreeNode root,TreeStack<Byte> rootStk,CodeTable map){
		if(root.getType()==1){
			//根节点
			TreeNode LNode=root.getLNode();
			rootStk.add((byte)0);
			//递归遍历左节点
			scanTree(LNode,rootStk,map);
			TreeNode RNode=root.getRNode();
			rootStk.add((byte)1);
			//递归遍历右节点
			scanTree(RNode,rootStk,map);
		}
		if(root.getType()==2){
			//叶节点
			Object[] cd=rootStk.getCode();
			Byte[] code=new Byte[cd.length];
			int ind=0;
			for(Object o:cd){
				byte ob=(Byte)o;
				code[ind]=ob;
				ind++;
			}
			byte bt=root.getB();
			CodeTableNode nd=new CodeTableNode(bt,code);
			map.add(nd);
		}
		rootStk.pop();//栈回溯
	}

 

⑤、每个取一个长度为八的字节数组,写入文件,若最后剩下的数组不足八位,则添加码表中没有的元素知道够8位。并写入码表

 

/**

	 * 获取文件重新编码,并直接写入文件
	 * 
	 * @param file
	 *            文件对象
	 * @param map
	 *            编码表
	 * @throws Exception
	 */
	private void getFileCode(File file, OutputStream op, CodeTable map)
			throws Exception {
		java.io.InputStream in = new java.io.FileInputStream(file);
		Byte[] nBt = new Byte[8];// 储存字节的每位,8位
		int emptyN = 8;// 剩余的空位,候补
		int btn = in.read();
		while (btn != -1) {
			byte bn = (byte) btn;
			Byte[] code = map.get(bn);// 获取b对应的编码
			for (int t = 0; t < code.length; t++) {// 插入值到bit位数组中
				nBt[8 - emptyN] = code[t];
				emptyN--;
				// 写满一个字节后加入队列
				if (emptyN == 0) {
					byte nByte = toByte(nBt);
					// 写入文件
					System.out.println("写入byte:"+nByte);
					byte[] bt = { nByte };
					op.write(bt);
					emptyN = 8;// 空位重置
					nBt = new Byte[8];// 数组重置
				}
			}
			btn = in.read();
		}
		in.close();
		// 写完后检测是否写满最后一个字节,如未满则填充一个map中不存在的值
		if (emptyN > 0) {
			Byte[] addCode = new Byte[emptyN];
			// 检测到可行的结尾填充
			if (scanUsableAddons(map, addCode, 0)) {
				for (byte bi : addCode) {
					nBt[8 - emptyN] = bi;
					emptyN--;
				}
				byte endB = toByte(nBt);
				// 添加到重新编码区结尾
				byte[] bt = { endB };
				op.write(bt);
			}
		}
	}

 /**

	 * 将CodeMap编码表对象转换成byte数组 规则:
	 * 每条编码为3部分,第一部分占1个byte,储存编码的, 第二部分占一个byte,存储原byte值
	 * 第三部分为一个byte[]数组,储存对应的编码,每个二进制位占一个byte,只为0或1
	 * 
	 * @param map
	 *            传入一个CodeMap对象
	 * @return 转换完成的byte数组
	 */
	private byte[] getMapByte(CodeTable map) {
		List<Byte> allBy = new ArrayList<Byte>();
		for (CodeTableNode mNode : map.getAllNodes()) {
			// 取得map中MapNode对象
			byte bt = mNode.getByte();// 取得byte值
			Byte[] co = mNode.getCode();// 取得编码
			Byte len = (byte) co.length;// 取得编码长度
			allBy.add(len);
			allBy.add(bt);
			for (byte b : co) {// 取得编码中每个二进制位值传入队列
				allBy.add(b);
			}
		}
		// 转换队列为byte[]数组
		byte[] abts = new byte[allBy.size()];
		int index = 0;
		for (byte b : allBy) {
			abts[index] = b;
			index++;
		}
		return abts;
	}

 

 

private void compress() {
		String src = srcField.getText();
		String tar = tarField.getText();
		File srcFile = new File(src);
		File tarFile = new File(tar);
		
		lab.setText("开始压缩   O(∩_∩)O");
		//lab.setText("正在压缩。。。  →_→");
		
		ReadFile sf = new ReadFile(srcFile);
		CodeTable map = null;// 获取编码表
		
		try {
			//得到码表
			map = sf.getMap();
			byte[] maptoByte = getMapByte(map);

			tarFile.createNewFile();// 创建文件
			// 建立输出流
			java.io.OutputStream out = new java.io.FileOutputStream(tarFile);
			// 建立对象输出流
			int mapLen = maptoByte.length;
			java.io.DataOutputStream dou = new java.io.DataOutputStream(out);
			dou.writeInt(mapLen);
			dou.flush();
			// 输出编码表
			out.write(maptoByte);
			
			// 写入转码后的文件数据
//			lab.setText("正在压缩。。。  →_→");
			
			getFileCode(srcFile, out, map);// 直接队文件转码并写入
			out.flush();
			out.close();
			
			//lab.setText("压缩成功!  (^o^)");
		} catch (Exception e1) {
			e1.printStackTrace();
		}
	}

 

 


二、解压

思路:从压缩的文件中读取字节+码表,将每个字节转化为一个八位的字符数组,逐一对比码表,若有相同的则根据码表转化为相应的字节,若没有一致的,则继续读取字节化为八位的字数数组,如此循环,知道读取完文件。然后将获取的字节写入文件,这样解压就算完成了。其实解压是压缩的逆过程,关键就是写入码表与获取码表,这是压缩与解压万和城呢过的关键。

 

/**

	 * 解压方法,先获取编码表,再对文件进行解码
	 * 
	 * @throws Exception
	 */
	private void inCompress() throws Exception {
		File srcF = new File(srcField.getText());
		File tarF = new File(tarField.getText());
		
//		lab.setText("开始解压   O(∩_∩)O");
		lab.setText("正在解压。。。  →_→");
		tarF.createNewFile();
		java.io.InputStream inStr = new java.io.FileInputStream(srcF);
		java.io.DataInputStream daIns = new java.io.DataInputStream(inStr);
		java.io.OutputStream ouStr = new java.io.FileOutputStream(tarF);
		
		// 取得编码表的长度
		int byteCount = daIns.readInt();
		int readByte = inStr.read();
		byteCount--;
		CodeTable getMap = new CodeTable();// 编码表对象,备用
		
		while (byteCount > 0) {
			int readCount = readByte;// 读取时的计数变量,以确定当前读取的数据类型
			// 获取本条编码的长度为readCount
			Byte[] byteCode = new Byte[readCount];
			readByte = inStr.read();
			byteCount--;// 读取了一个字节,计数器减一
			if (readByte == -1) {// 异常文件结尾,抛出
				throw new Exception("Unexcepted End");
			}
			byte bt = (byte) readByte;// 当前编码对应的字节
			for (int i = 0; i < readCount; i++) {
				readByte = inStr.read();
				byteCount--;// 读取了一个字节,计数器减一
				if (readByte == -1) {// 异常文件结尾,抛出
					throw new Exception("Unexcepted End");
				}
				byte tb = (byte) readByte;
				byteCode[i] = tb;
			}
			// 一条编码读取完毕,建立一个编码表节点对象并导入了
			CodeTableNode getNode = new CodeTableNode(bt, byteCode);
			getMap.add(getNode);
			readByte = inStr.read();
			byteCount--;// 读取下一个数据
		}
		
//		lab.setText("正在解压。。。  →_→");
		
		// 编码表建立完毕,开始读取文件正文并转码
		// 转码方法:每次读取一位,在编码表中检索,如果存在则转码,不存在则增加一位并重新检测
		List<Byte> bs = new java.util.ArrayList<Byte>();// 存储编码的byte队列
		while (readByte != -1) {
			byte b = (byte) readByte;
			
			byte[] bArray = toByteArray(b);// 将字节每位提出转成数组
			
			for (byte bn : bArray) {
				bs.add(bn);
				byte[] nNode = checkMapCode(getMap, bs);
				if (nNode != null) {
					ouStr.write(nNode);
					ouStr.flush();
					bs.clear();// 清空队列
				}
				if (bs.size() > getMap.getAllNodes().size()) {
					// 队列长度已经大于Map中的编码条数
					throw new Exception("Erro File,Not Found Code");
				}
			}
			readByte = inStr.read();// 继续读取
		}
		
//		lab.setText("解压成功!  (^o^)");
		
		// 关闭输入输出流
		inStr.close();
		ouStr.close();
	}
 

 

/**
	 * 递归检索编码表map中无对应值的结尾二进制位作为结尾
	 * 
	 * @param map
	 *            编码表
	 * @param bits
	 *            剩余的二进制位
	 * @param index
	 *            目前的指向
	 * @return 找到了则返回true,检索完毕都未找到则返回false
	 */
	private boolean scanUsableAddons(CodeTable map, Byte[] bits, int index) {
		if (index != bits.length - 1) {
			bits[index] = 0;
			if (scanUsableAddons(map, bits, index + 1)) {
				return true;
			}
			bits[index] = 1;
			if (scanUsableAddons(map, bits, index + 1)) {
				return true;
			}
		}
		if (index == bits.length - 1) {
			bits[index] = 0;
			if (map.get(bits) == null) {
				return true;
			}
			bits[index] = 1;
			if (map.get(bits) == null) {
				return true;
			}
		}
		return false;
	}
 


三、感想

做完哈夫曼压缩后,感想最大的就是速度,速度太慢,就像蜗牛爬一样慢,相对于自己用地360压缩是简直没得比,压缩个7M多点的要16分钟,解压更是成倍增长。还有就是对于一些doc格式、ppt格式的等,压缩比也不大高,可能是文件大小的原因。

 

 

 

界面图:


哈夫曼压缩_第1张图片

 

源文件、压缩后、解压后的对比图:


哈夫曼压缩_第2张图片

你可能感兴趣的:(压缩)