用Java写一个简易压缩程序(基于哈夫曼编码,含解压)

原理:一个文本文件的内容本质上是一个长字符串,如果根据每个字符在总串中的出现次数构造一棵哈夫曼树,得到每个字符的哈夫曼编码,再把每个字符换成其对应的哈夫曼编码,就可以得到一串长二进制串,出现次数越多的字符其哈夫曼编码越短,占用位数越少,以此达到压缩的效果。最后把该二进制串转换成十进制字节写入文件,就可以得到一个压缩文件。

对于后续的解压操作,解压时读取文件并进行字节转换会获得一个长二进制字符串,为了还原成原字符串,我们需知道每个字符对应的哈夫曼编码,因此我们还需要一个码表记录每个字符对应的哈夫曼编码,并在写入压缩文件时把码表对象也一起写入。

代码实现:

1.为了获得哈夫曼编码,需要有:节点类Node,createHFMTree()方法构造哈夫曼树,getHFMCode()方法为每个节点设置哈夫曼编码。构造哈夫曼树过程中会出现权值比较,因此还可以写一个快排fastSort()。

//节点类
class Node{
	public String str; //存储的字符,也可用char
	public int times;//字符在字符串中出现次数
	public String code="";//节点的哈夫曼编码,初始值要设为""
	public Node left,right;
	//构造方法
	public Node() {
		
	}
	public Node(int times) {
		this.times = times;
	}
	public Node(String str,int times) {
		this.str = str;
		this.times = times;
	}
}
public class Compress {
	//private List<Node> nodeList;//节点数组
	private HashMap<String,String> codeMap = new HashMap<>();//码表<字符,哈夫曼编码>
	
	//根据节点的times值快排,从小到大
	public void fastSort(List<Node> nodeList,int low,int high) {
		if (low >= high) return;
	    int i = low, j = high;
	    Node t;
	    Node tmp = nodeList.get(low);
	    while (i < j) {
	        while (i < j && nodeList.get(j).times >= tmp.times) j--;
	        while (i < j && nodeList.get(i).times <= tmp.times) i++;
	        if (i < j) {
	            t = nodeList.get(i);
	            nodeList.set(i, nodeList.get(j));
	            nodeList.set(j, t);
	        }
	    }
	    nodeList.set(low, nodeList.get(i));
	    nodeList.set(i, tmp);
	    fastSort(nodeList, low, i - 1);
	    fastSort(nodeList, i + 1, high);
	}
	//创建哈夫曼树
	public Node createHFMTree(List<Node> nodeList) {
		while(nodeList.size()>1) {
			fastSort(nodeList,0,nodeList.size()-1);
			//每次取出节点前对数组快排,确保取出的头两个节点一直为times最小节点
			Node node1 = nodeList.remove(0);
			Node node2 = nodeList.remove(0);
			//创建新的父节点并把其压入队列
			Node node = new Node(node1.times + node2.times);
			node.left = node1;
			node.right = node2;
			nodeList.add(node);
		}//循环结束时,队列中仅剩一个节点,为整棵哈夫曼树的根节点
		return nodeList.remove(0);//返回哈夫曼树的根节点
	}
	//为每个节点计算哈夫曼编码,左节点+0,右节点+1
	public void getHFMCode(Node root) {
		if(root.left!=null) {
			root.left.code = root.code + '0';
			getHFMCode(root.left);
		}
		if(root.right!=null) {
			root.right.code = root.code + '1';
			getHFMCode(root.right);
		}
	}

由样例图可以看出哈夫曼树的根节点实际上并没有对应字符,只用来辅助建树,因此没必要为其设置哈夫曼编码;把每个节点的默认编码code设为空串(不是null),从第二层开始为每个节点设置哈夫曼编码。

2.上述操作后,每个字符都有对应的编码,创建码表进行存储。

//编写码表,key为字符,value为哈夫曼编码
	public void createcodeMap(Node node) {
		if(node!=null) {
			//遍历整棵树,只有叶子节点才需要写入码表,非叶子节点只是构造树时的支点
			if(node.left==null && node.right==null) {
				codeMap.put(node.str, node.code);
			}
			createcodeMap(node.left);
			createcodeMap(node.right);
		}
	}

3.准备工作做完后,接下来是压缩部分,首先需要读取文本文件,获得文本文件的字符整串

//读取文件所有字符,返回一串长字符串
	public String readFile(String path) throws Exception{
		File file = new File(path);		
		FileReader fr = new FileReader(file);//获取文件的字符输入流
		BufferedReader br = new BufferedReader(fr);//用缓冲输入流读取,提高效率
		StringBuilder sb = new StringBuilder();//用于拼接读取后的总字符串
		String curstr = "";//读取字符串
		//逐行读取,若返回字符串curstr不为空,则拼接到总字符串
		while((curstr=br.readLine()) != null) {
			sb.append(curstr);
		}
		return sb.toString();
	}

4.构造哈夫曼树需要先创建一个节点数组nodeList,需要为每个字符创建一个节点,且统计每个字符在整串中的出现次数,统计次数可以用一个哈希表实现,注意此处的哈希表不是码表。

//统计每个字符出现次数
	public HashMap<String,Integer> countTimes(String str) {
		HashMap<String,Integer> hm = new HashMap<>();//key为字符,value为出现次数
		for(int i=0;i<str.length();i++) {
			//containKey()判断哈希表中是否有目标字符
			//hm的key值为String类型,charAt()返回char型,char+"" 会转成String
			if(hm.containsKey(str.charAt(i)+"")) {//如果表中已有,数值+1
				hm.put(str.charAt(i)+"", hm.get(str.charAt(i)+"")+1);
			}else {//如果表中没有,存入
				hm.put(str.charAt(i)+"", 1);
			}
		}
		return hm;
	}
	//构造节点数组
	public List<Node> createnodeList(HashMap<String,Integer> hm) {
		List<Node> nodeList = new ArrayList<Node>();//全局变量中的nodeList只是声明变量,此处为创建
		//遍历哈希表key值
		for(String str:hm.keySet()) {
			//为每个字符创建节点,存入字符和出现次数
			Node node = new Node(str,hm.get(str));
			nodeList.add(node);
		}
		return nodeList;
	}

5.接下来是创建哈夫曼树,为每个字符设置哈夫曼编码,然后把字符整串按照哈夫曼编码转换得到一个哈夫曼编码串,也就是要写入文件的二进制串。

//把长字符串的每个字符替换成哈夫曼编码,得到一串二进制码
	public String getHFMStr(String str) {
		//给一串字符串,把其转换成二进制码
		//str-->nodeList
		HashMap<String,Integer> hm = countTimes(str);
		List<Node> nodeList = createnodeList(hm);
		//nodeList-->HFMTree
		Node root = createHFMTree(nodeList);
		getHFMCode(root);
		//HFMTree-->codeMap
		createcodeMap(root);
		
		//依次取出每个字符,把其哈夫曼码存入新字符串
		StringBuilder sb = new StringBuilder();
		for(int i=0;i<str.length();i++) {
			sb.append(codeMap.get(str.charAt(i)+""));
		}
		return sb.toString();
	}

6.最后是把二进制串写入文件,由于是以字节流写入,所以需要把二进制串转换成字节再写入。字节的大小是8位比特,所以每8位二进制串转成1个字节,这样就不会出现位数丢失的情况。对于最后不足8位的二进制串,我们需手动补零补成8位,同时把补零个数也写入文件,这样解压的时候才知道要删除几个零。别忘记把码表也写入文件。

//把二进制码写入文件
	public void writeIn(String codestr) throws Exception{
		File file = new File("D:\\java\\teach\\receive.txt");//用于接收压缩内容的文件
		//获取文件的输出流
		FileOutputStream fos = new FileOutputStream(file);
		ObjectOutputStream oos = new ObjectOutputStream(fos);
		oos.writeObject(codeMap);//先写入码表,便于解压时读取
		//fos是按字节流写入,因此编码每8位转成一个int,而int在写入时会自动转型成byte,8位int转8位byte,不会出现位数丢失的情况
		int num = codestr.length()/8; //能完整写入的编码串数量
		int lastnum = codestr.length()%8; //剩余不足8位编码数,需手动补0
		int data;//二进制编码转成的int值
		for(int i=0;i<num;i++) {
			//切割出8位字符串并转成整型,写入文件
			data = Integer.parseInt(codestr.substring(i*8,(i+1)*8),2);
			fos.write(data);
			fos.flush();
		}
		String zero = ""; //补零字符串
		for(int i=0;i<8-lastnum;i++) {
			zero += '0';
		}
		//把剩余字符串补成8位,写入文件
		String laststr = codestr.substring(num*8, num*8+lastnum) + zero;
		data = Integer.parseInt(laststr,2);
		fos.write(data);
		//把最后的补零个数也写入文件,便于解压
		fos.write(8-lastnum);
		fos.flush();
	}

7.最后是对上述方法包装,方便一次性调用。

//压缩,把所给文件内容压缩到指定文件
	public void compress(String path) throws Exception{
		String str = readFile(path);//获取所给文件要压缩的总字符串
		String codestr = getHFMStr(str);//把总字符串转成二进制串
		System.out.println("压缩时转换成的二进制编码串为:"+codestr);
		writeIn(codestr);//写入文件
	}
	
}

运行结果: 

压缩时转换成的二进制编码串为:1101111111010100000

8.为了方便使用,可以写一个可视化界面。

public class UI {
	public void create() {
		JFrame jf = new JFrame("文本压缩");//窗体
		jf.setSize(450,500);//设置大小
		jf.setLocationRelativeTo(null);//居中显示
		jf.setDefaultCloseOperation(3);//自动关闭后台
		jf.setLayout(new FlowLayout());//流动式布局
		
		JTextField jtf = new JTextField();//文本框,用于显示可选择文件
		jtf.setPreferredSize(new Dimension(330,30));//设置大小
		jf.add(jtf);//添加到窗体
		
		JButton jbu1 = new JButton("选择文件");//按钮
		JButton jbu2 = new JButton("确认压缩");
		ButtonListener listener = new ButtonListener(jf,jtf);//监听器
		jbu1.addActionListener(listener);//为按钮添加监听器
		jbu2.addActionListener(listener);
		jf.add(jbu1);//添加到窗体
		jf.add(jbu2);
		
		jf.setVisible(true);//设置窗体可见
	}
	public static void main(String[]args) {
		new UI().create();
	}
}
//监听器类
class ButtonListener implements ActionListener{
	private JFrame jf; //压缩成功后程序停止,退出窗体
	private JTextField jtf;//选定文件时,在文本框上显示该文件路径
	private String path="";//选定文件的路径
	//构造方法
	public ButtonListener(JFrame jf,JTextField jtf) {
		this.jf = jf;
		this.jtf = jtf;
	}
	public void actionPerformed(ActionEvent e) {
		String s = e.getActionCommand();//获取选择按钮的内容
		switch(s) {
			case "选择文件":
				JFileChooser jfc = new JFileChooser();//文件选择器
				int returnValue = jfc.showOpenDialog(null);
				//showOpenDialog()会打开界面框,选择不同按钮会返回不同int值,APPROVE_OPTION是选择"确定"的返回值
				if(returnValue == jfc.APPROVE_OPTION) {
					File file = jfc.getSelectedFile();//获取选定的文件
					path = file.toString();//获取文件路径
					jtf.setText(path);//在文本框上显示选定文件的路径
				}
				break;
			case "确认压缩":
			try {
				new Compress().compress(path);
			} catch (Exception e1) {
				// TODO Auto-generated catch block
				e1.printStackTrace();
			}//压缩
				jf.dispose();//退出窗体,程序结束
				break;
		}
	}
}

 

以上就是压缩软件的实现代码,接下来是解压。

1.首先需要读取压缩文件,获得一个个字节,把字节转成二进制编码就得到了哈夫曼编码串。注意字节转哈夫曼编码时系统可能会自动消除前缀的0, 当字节转成的二进制码不足8位时要在前缀补零到8位。最后获得整二进制串要在后缀消除压缩时补的零。

public class Decompress {
	private HashMap<String,String> codeMap;//码表
	
	//读取文件中的字节,并转成二进制字符串
	public String read(File file) throws Exception{
		FileInputStream fis = new FileInputStream(file);//字节输入流
		ObjectInputStream ois = new ObjectInputStream(fis);//对象输入流
		List<Integer> dataList = new ArrayList<>();//字节数组,存储读取文件中的所有字节
		int data;//每次读出的字节
		//压缩时是先写入码表,因此解压时也是先读取码表对象
		codeMap = (HashMap<String,String>)ois.readObject();
		//读取字节
		while((data=fis.read())!=-1) {//如果到达文件末尾会返回-1,代表读取完毕
			dataList.add(data);
		}
		StringBuilder sb = new StringBuilder();
		//压缩时最后写入了后缀补零个数,因此解压时记得取出
		int lastzero = dataList.get(dataList.size()-1);
		for(int i=0;i<dataList.size()-1;i++) {
			/*Integer.toBinaryString会把整型转成二进制码,但可能会出现前缀的0被自动消除的情况,
			因此转换后的二进制码若不足8位,要手动在前缀补零
			*/
			String s = Integer.toBinaryString(dataList.get(i));//整型转成二进制码
			if(s.length()!=8) {
				StringBuilder forezero = new StringBuilder();
				for(int j=0;j<8-s.length();j++) {
					forezero.append("0");
				}
				s = forezero + s;//补成8位二进制码
			}
			sb.append(s);
		}
		//不要忘记后缀消零
		String str = sb.substring(0, sb.length()-lastzero);
		return str;
	}

2.把二进制串转成对应的字符串。对于二进制串,从一位开始往后取子串,每取出一位,就判断码表中是否有对应的编码值,若有,则根据码表把对应字符加入新字符串中。为了方便操作,可以新建一个反转码表,便于比较编码。

//把二进制码根据码表转成字符
	public String codetoStr(String codestr) {
		//为了方便操作,创建一个反转码表,新码表的key是字符的哈夫曼编码,value是字符值
		HashMap<String,String> reMap = new HashMap<>();
		for(String s:codeMap.keySet()) {//原码表的key是字符,value是哈夫曼编码
			reMap.put(codeMap.get(s),s);
		}
		int i=0,index=0;//i和index用于移动切割二进制码串
		StringBuilder sb = new StringBuilder();//转换后的字符串
		while(i<codestr.length()) {
			index++;//从一位二进制码开始
			String key = codestr.substring(i,index);
			if(reMap.containsKey(key)) {//若二进制等于码表的哈夫曼码
				sb.append(reMap.get(key));//拼接对应字符
				i = index;//更新流动下标
			}
		}
		return sb.toString();
	}
}

这样解压部分也完成了,后续也可以跟解压一样写一个可视化界面,这里不再重复。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值