Java利用最优二叉树实现哈夫曼编码对文本的压缩和解压

什么是哈夫曼编码

哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,哈夫曼编码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,该方法完全依据字符出现概率来构造异字头的平均长度最短的码字,有时称之为最佳编码,一般就叫做Huffman编码(有时也称为霍夫曼编码)

为什么哈夫曼编码能够实现文件的压缩

如果我们使用的定长编码方例如ASCII码,8-bit定长编码,使用8位(一个字节)代表一个字符,就会出现很多的byte出现了相同,所以我们考虑一种新的编码方式,变长编码的方式,原理很简单,以前是使用的8位表示一个字符,现在该用不固定长度,确定长度的依据是每个字符出现的次数,越是高频的字符其所用的bit(二进制位数)越短,这样就实现了整个文本的变短,

//举例我们有这么一个字符串:
String content = "abbcccddddeeeee";

当我们把这段文字发送给别人时,会转换成一段字节数组长成下面这个样子

字节数组:
[97, 98, 98, 99, 99, 99, 100, 100, 100, 100, 101, 101, 101, 101, 101]
二进制:
110000111000101100010110001111000111100011110010011001001100100110010011001011100101110010111001011100101

使用哈夫曼编码过后
先统计文本每个字符的 a : 1 b:2  c:3  d:4  e:5
每个字符对应的二进制 a->1110  b->1111  c->110   d->10 e->0
二进制:
010011011000000101010101111111111
字节数组:
[77, -127, 85, -1, 1] 

要想学习哈夫曼编码先了解哈夫曼树的基本概念

  • 路径和路径长度:在一棵树中,从节点往下可以达到的孩子或者孙子节点之间的通路,从树根节点到叶子节点的长度称为路径长度,从从根节点到第L层节点到路径长度为L-1
  • 节点的权及带权路径长度:给节点值我们就叫做节点的权值,节点带权路径的为: 路径长度 * 权值
  • 一整个树的带权路径长度会等于所有叶子节点的带权路径长度之和,称为WPL(weighted path length)
  • WPL最小时就叫做哈夫曼树,因此我们构建哈夫曼树时应该将最大权值的节点放在根节点附近

非最优二叉树1.png
非最优二叉树2

哈夫曼树.png

1、压缩算法实现部分

基本思路

  • 构造树节点node
  • 用生成的节点node构造哈夫曼树
  • 生成哈夫曼编码表
  • 用哈夫曼编码表生成压缩后的字节数组
构造树节点node

这是node类实现了Comparable借口,并且重写了compareTo可以后续用于所有node根据权值排序



public class Node implements Comparable<Node>{
	Byte data;    //存放数据本身
	int weight;  //权值
	Node left;	//指向左子节点
	Node right;	//指向右子节点
	public Node(Byte data, int weight) {
		this.data = data;
		this.weight = weight;
	}
	@Override
	public int compareTo(Node o) {
		// TODO Auto-generated method stub
		return this.weight-o.weight;
	}
	@Override
	public String toString() {
		return "Node [data=" + data + ", weight=" + weight + "]";
	}
}

根据文本信息转换成Byte数组,再创建一个HashMap 遍历整个byte数组key保存当前的byte的值,value用于统计相同字节出现的频率,再把这个HashMap中的节点转换成Node节点

public static void main(String[] args) {
   	// TODO Auto-generated method stub
   	String content = "abbcccddddeeeee";
   	byte[] contentBytes = content.getBytes();
 	List<Node> nodes = getNodes(bytes);
   }

private static List<Node> getNodes(byte[] bytes) {
   	// 创建Arrlist
   	ArrayList<Node> nodes = new ArrayList<Node>();
   	// 遍历bytes,统计byte出现的次数->map
   	Map<Byte, Integer> counts = new HashMap();
   	for (byte b : bytes) {
   		if (!counts.containsKey(b)) {
   			counts.put(b, 1);
   		} else {
   			counts.put(b, counts.get(b) + 1);
   		}
   	}
   	// 把每一个键对转成一个Node对象,并加入到nodes集合中
   	for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {
   		nodes.add(new Node(entry.getKey(), entry.getValue()));
   	}
   	return nodes;
   }

用生成的节点node构造哈夫曼树

  • 权值越大,带权路径长度应该最短越接近root节点,原因是用0和1分别表示二叉树的左右字节点可达路径,
  • 对传入的list中的node从小到大进行排序,拿出第一个最小的两个元素,构成新的节点放入list中
  • 对新的节点不赋值,权重是两个,把它的左右节点初始化为最小的两个元素,权重是两个是两个子节点的和
  • 重复以上的步骤直到list中的元素小2时候停止
示例 String content = "abbcccddddeeeee";
a : 1 b:2  c:3  d:4  e:5

步骤1

步骤2

步骤3

步骤4

具体代码如下:

	// 可以通过list创建哈夫曼树
	/**
	 * @param nodes 要处理的nodes
	 * @return 返回root节点
	 */
	public static Node createHuffmanTree(List<Node> nodes) {
		// 传入的list中大于1个节点时候才构建二叉树
		while (nodes.size() > 1) {
			// 对集合里面的元素进行排序
			Collections.sort(nodes);
			// 取出权值最小的节点
			Node left = nodes.get(0);
			Node right = nodes.get(1);
			// 构建一个新的二叉树节点,并且初始化它的值为null
			Node parent = new Node(null, left.weight + right.weight);
			// 把两个节点挂在一个非子节点上
			parent.left = left;
			parent.right = right;
			// 移除左右两个节点
			nodes.remove(left);
			nodes.remove(right);
			// 把这个非叶子节点加入到存放到nodes中
			nodes.add(parent);
		}
		return nodes.get(0);
	}

构建哈夫曼树的输出结果

Node [data=null, weight=15]
Node [data=null, weight=6]
Node [data=99, weight=3]
Node [data=null, weight=3]
Node [data=97, weight=1]
Node [data=98, weight=2]
Node [data=null, weight=9]
Node [data=100, weight=4]
Node [data=101, weight=5]

生成哈夫曼编码表

首先要创建huffmanCodes哈夫曼编码表 ,表的key - node.data(a,b,c) value - 0101,重载getCodes函数定义一个StringBuilder用于字符串010101的连接,递归处理之前生成好的哈夫曼树,往左走拼接 ‘0’ ,往右走拼接’1’只有当我们遍历到叶子节点才把它加入huffmanCodes中

	// 声明哈夫曼编码表,static修饰,在多个方法中共同使用
	public static Map<Byte, String> huffmanCodes = new HashMap<Byte, String>();	

	// 生成哈夫曼树对应的哈夫曼编码
	// 思路:
	// 将哈夫曼编码存放在map中比较合适 Map<Byte,String>
	// e->0  d->10 c->110 b->1111 a->1110 

	// 为了调用方便,重载getCodes
	private static Map<Byte, String> getCodes(Node root) {
		if (root == null) {
			return null;
		}
    //处理只有root节点的特殊情况
		if (root.left == null && root.right == null) {
			huffmanCodes.put(root.data, "0");
		}
		// 在生成哈夫曼编码表时,需要去拼接路径,定义一个StringBuilder 存储叶子节点的路径
		StringBuilder builder = new StringBuilder();
		// 处理左子树
		getCodes(root.left, "0", builder);
		// 处理右子树
		getCodes(root.right, "1", builder);
		return huffmanCodes;
	}

	/**
	 * 功能:将传入的node节点的所有叶子节点的哈夫曼得到,并放入huffmanCodes中
	 * 
	 * @param node    //传入节点
	 * @param code    //路径:左子节点是0 右子节点1
	 * @param builder //用于拼接路径
	 */
	private static void getCodes(Node node, String code, StringBuilder builder) {
		// 重建StringBuilder是为了防治地址引用一值,保持生成字符不共用同一字符串
		StringBuilder builder2 = new StringBuilder(builder);
		// 将code加入到StringBuilder
		builder2.append(code);
		if (node != null) { // 如果为空不进行处理
			// 判断当前是叶子还是非叶子节点
			if (node.data == null) {
				// 递归处理
				// 左边递归
				getCodes(node.left, "0", builder2);
				// 右边递归
				getCodes(node.right, "1", builder2);
			} else {
				// 找到了叶子节点
				huffmanCodes.put(node.data, builder2.toString());
			}
		}
	}

用哈夫曼编码表生成压缩后的字节数组

最后一步就是用我们生成好的哈夫曼编码表对我们的全部文本进行生成处理过后的字节数组,遍历整个字节数组用字节数组的每个数据去hashmap中匹配010101生成二进制字符串,因为哈夫曼编码表对应的二进制是可变长的,所以最后字符串的长度可能就不是8的倍数,要进行特殊处理多加一个字节,最后一个字节如果不满8bit,我们在这里要用一个static int endLen记录下它的长度,在后面逆向解压中将要用到 , 再遍历整个字符串数组用Byte数组来封装好整个字符串,1Byte=8bit, 具体的代码如下:

	// 编写一个方法,将字符串对应的byte[]数组,通过生成哈夫曼编码表,返回一个哈夫曼编码处理后的byte[]数组
	/**
	 * @param bytes       原始数据要处理的byte数组
	 * @param huffmanCode //哈夫曼编码表
	 * @return //返回处理后的byte[]
	 */
	public static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCode) {
		// 利用huffmanCodes将byte转成哈夫曼对应的字符串
		StringBuilder builder = new StringBuilder("");
		// 遍历数组
		for (byte b : bytes) {
			builder.append(huffmanCodes.get(b));
		}
		// 将"10101000..." 转成byte[]
		// 统计返回的哈夫曼编码有多长
		int len = (builder.length() % 8) == 0 ? builder.length() / 8 : builder.length() / 8 + 1;
		endLen = builder.length() % 8;
		// 创建 存储后的bytes压缩数组
		byte[] huffmanCodeBytes = new byte[len];
		int index = 0;// 记录是第几个byte
		for (int i = 0; i < builder.length(); i += 8) {// 因为是每8位对应一个byte,所以步长+8
			String strByte;
			// 两种情况i+8超过最后位置和不超过的分别赋值

			strByte = i + 8 > builder.length() ? builder.substring(i) : builder.substring(i, i + 8);
			// 后面一个参数2表示转换成二进制
			huffmanCodeBytes[index++] = (byte) Integer.parseInt(strByte, 2);
		}
		return huffmanCodeBytes;
	}

处理之后的结果

处理前的字节数组:[97, 98, 98, 99, 99, 99, 100, 100, 100, 100, 101, 101, 101, 101, 101] 长度=15
处理后的字节数组:[77, -127, 85, -1, 1] 长度=5
当重复字符较多时,压缩效率还是相当可观的

对整个压缩流程创建一个接口函数方便调用

	

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		String content = "abbcccddddeeeee";
		byte[] contentBytes = content.getBytes();
		byte[] huffmanCodeBytes = huffmanZip(contentBytes);
  }

	// 创建一个接口函数封装好实现的细节
	/**
	 * 
	 * @param bytes 原始字符串对应的字节数组
	 * @return 返回处理后的字节数组
	 */
	public static byte[] huffmanZip(byte[] bytes) {
		System.out.println("处理前的字节数组:"+Arrays.toString(bytes)+" 长度="+bytes.length);
		List<Node> nodes = getNodes(bytes);
		// 创建哈夫曼树
		Node huffmanTreeRoot = createHuffmanTree(nodes);
		// 对应的哈夫曼编码
		Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
		// 根据生成的哈夫曼编码,得到压缩后的数组
		byte[] huffmanCodeBytes = zip(bytes, huffmanCodes);
		System.out.println("处理后的字节数组:"+Arrays.toString(huffmanCodeBytes)+" 长度="+huffmanCodeBytes.length);
		preOrder(huffmanTreeRoot);
		return huffmanCodeBytes;
	}

2、解压算法实现部分

基本思路

  • 字节数组转换成二进制字符串
  • 逆向处理生成好的哈夫曼编码表
  • 根据逆向生成的哈夫曼表查询生成原来的字节数组

字节数组转换成二进制字符串

思路就是用flag来标识是否时最后一位,通过之前的endLen得到最后一位如果不满八位就进行特殊处理

代码处理主要用二进制的操作,byte的取值范围为-128-127,如果出现负数则要进行补高位

	// 完成数据转成对应的二进制字符串'10101000...'
	/**
	 * 将一个byte 转成一个二进制的字符串
	 * 
	 * @param b    传入的是一个字节b
	 * @param flag 标志是否为最后一个字节,true表示不是最后一个字节,false表示是最后一个字节
	 * @return 是该b对应对二进制对字符串,(注意是按照补码返回)
	 */
	public static String byteToBitString(boolean flag, byte b) {
		// 使用一个变量保持b
		int temp = b; // 将b转成int
		temp |= 256;
		String str = Integer.toBinaryString(temp);// 返回的是temp对应的二进制补码
		if (flag || (flag == false && endLen == 0)) {
      //字符串的截取,只拿后八位
			return str.substring(str.length() - 8);
		} else {
      //不满8bit有多少位拿多少位
			return str.substring(str.length() - endLen);
		}

逆向处理生成好的哈夫曼编码表

用循环把编码表key(a,b,c)-value(0101) 倒转过来

		// 把哈夫曼编码表进行调换,因为要进行反向查询
		Map<String, Byte> map = new HashMap<String, Byte>();
		for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
			map.put(entry.getValue(), entry.getKey());
		}

根据逆向生成的哈夫曼表查询生成原来的字节数组

取生成好的010101二进制字符串,根据哈夫曼编码表查出原来的每个字节,采用双指针移动的方式去hashmap中查询,查到了对应的字节,吧它加入到我们的list中,再把list转换成byte数组进行返回

	// 编写一个方法,完成对压缩数据对解码
	/**
	 * @param huffmanCodes 哈夫曼编码表
	 * @param huffmanBytes 哈夫曼编码得到对字节数组
	 * @return 就是原来对字符串对应对数组
	 */
	public static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
		// 1.先得到 huffmanBytes 对应对二进制字符串,形式10101000...
		StringBuilder builder = new StringBuilder();
		// 2.将byte数组转成二进制的字符串
		for (int i = 0; i < huffmanBytes.length; i++) {
			byte b = huffmanBytes[i];
			// 判断是不是最后一个字节
			boolean flag = (i == huffmanBytes.length - 1);
			builder.append(byteToBitString(!flag, b));
		}
		// 创建集合,存放byte
		List<Byte> list = new ArrayList<>();
		for (int i = 0; i < builder.length();) {
			int count = 1; // 小的计数器
			boolean flag = true;
			Byte b = null;
			while (flag) {
				// 取出一个bit '1'或者'0'
				String key = builder.substring(i, i + count); // i 不动 让count移动,直到匹配到一个字符
				b = map.get(key);
				if (b == null) {// 没有匹配到
					count++;
				} else {
					flag = false;
				}
			}
			list.add(b);
			i = i + count;
		}
		// 当for循环结束以后,list中存放了所有当字符
		// 把list中的数据放入到byte[] 并返回
		byte b[] = new byte[list.size()];
		for (int i = 0; i < b.length; i++) {
			b[i] = list.get(i);
		}
		return b;
	}

所有调用过程main函数

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		String content = "abbcccddddeeeee";
		byte[] contentBytes = content.getBytes();
		byte[] huffmanCodeBytes = huffmanZip(contentBytes);
		byte[] source = decode(huffmanCodes, huffmanCodeBytes);
		System.out.println("原来的字符串=" + new String(source));
	}

压缩-解压的处理结果

处理前的字节数组:[97, 98, 98, 99, 99, 99, 100, 100, 100, 100, 101, 101, 101, 101, 101] 长度=15
处理后的字节数组:[77, -127, 85, -1, 1] 长度=5
原来的字符串=abbcccddddeeeee

总结

以上就就是关于所有哈java实现夫曼编码的所有细节,希望你对此有更清晰的的了解
完整源代码移步github

声明

1、如果在文章当中发现有描述错误的地方,还请您不吝指出,万分感谢!
2、此文章系本人原创作品,转发请注明出处!

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值