我的伟大航路(12)

今天是哈夫曼树

此处知识来自b站讲解

哈夫曼树(最优二叉树)的主要思想是,叶子带有权值,而访问叶子,则需要从根节点开始遍历,若想要树的带权路径长度(WPL)最小,则需要权大的,路径小。
不同的带权路径长度
同样的数据,WPL却不同。

那么让我们开拓一下思想,前面将权值越大的越靠前,那我们是不是可以将这个权值表达别的意思,也就是不仅仅是在于数字上的意义,那么哈夫曼编码就是很聪明的了

在正常的数据的传输中,若想传输一串字符串,则每个字符,都需要有对应的一串二进制数字来表示,每个byte由8个bit组成,每个字符(一个字母之类的)由一个byte组成(应该没错🙃),那么就会出现这样一个情况,8位长的byte,对于一些不经常使用的字符,也需要存储同样大的空间。

那么哈夫曼编码如何解决呢?

先筛选出在这串字符中最经常使用的字符,并形成一个使用频率降序的数组,当叶子在二叉树的左边时,编码为0,右边为1,又因为哈夫曼树是二叉树,两个子叶满后,会在一个子叶上开辟新的子树,有了深度,同时也保证了编码的唯一
在这里插入图片描述
先上每个节点的代码

//实现了Comparable的接口,便于排序
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) {
		return o.weight - this.weight;
	}

	// 重写的toString
	@Override
	public String toString() {
		return "Node [data=" + data + ", weight=" + weight + "]";
	}

}

然后是哈夫曼编码的代码

import java.util.*;
import java.util.Map.Entry;

public class TestHuffmanCode {

	public static void main(String[] args) {
		// 需要压缩的字符串
		String msg = "can you can a can as a can canner can a can.";
		// 将字符串用再带的api转成byte数组
		byte[] bytes = msg.getBytes();
		// 进行哈夫曼编码
		byte[] b = huffmanZip(bytes);
		System.out.println(bytes.length);
		System.out.println(b.length);
	}

	/**
	 * 哈夫曼压缩(主)
	 * 
	 * @param bytes
	 * @return byte[]
	 */
	private static byte[] huffmanZip(byte[] bytes) {
		// 先统计每一个byte出现的次数,并放入一个链表的集合中
		List<Node> nodes = getNodes(bytes);
		// 创建一棵合并过的哈夫曼树
		Node tree = createHuffmanTree(nodes);
		// 创建一个哈夫曼编码表
		Map<Byte, String> huffCodes = getCodes(tree);
		// System.out.println(huffCodes);
		// 编码
		byte[] b = zip(bytes, huffCodes);
		return b;
	}

	/**
	 * 进行哈夫曼编码
	 * 
	 * @param bytes
	 * @param huffCodes2
	 * @return
	 */
	private static byte[] zip(byte[] bytes, Map<Byte, String> huffCodes) {
		StringBuilder sb = new StringBuilder();
		// 把需要压缩的byte数组处理成一个二进制的字符串
		for (byte b : bytes) {
			sb.append(huffCodes.get(b));
		}
		// 定义长度给下面byte数组定义长度,每8位是一组,不足8位,仍占一组
		// byte是八位的二进制
		int len;
		if (sb.length() % 8 == 0) {
			len = sb.length() / 8;
		} else {
			len = sb.length() / 8 + 1;
		}
		// System.out.println(sb.toString());
		// 用于存储压缩后的byte
		byte[] by = new byte[len];
		// 记录新byte的位置,用以截取字符串
		int index = 0;
		// 遍历sb这个String,i以8进
		for (int i = 0; i < sb.length(); i += 8) {
			String strByte;
			// 当下一组不够8位的时候
			if (i + 8 > sb.length()) {
				// 获取剩下所有的字符
				strByte = sb.substring(i);
			} else {
				// 正常情况,获取当下第i个到第i+8个的字符
				strByte = sb.substring(i, i + 8);
			}
			// 先将获取的字符串编成二进制,在将其强转位byte类型
			byte byt = (byte) Integer.parseInt(strByte, 2);
			by[index] = byt;
			index++;
		}
		return by;
	}

	// 用于临时存储路径
	static StringBuilder sb = new StringBuilder();
	// 用于存储哈夫曼编码
	static Map<Byte, String> huffCodes = new HashMap<>();

	/**
	 * 根据哈夫曼树获取和父母编码
	 * 
	 * @param tree
	 * @return
	 */
	private static Map<Byte, String> getCodes(Node tree) {
		// 排除树空的情况,并作为递归结束的条件
		if (tree == null) {
			return null;
		}
		// 用递归的想法,处理整棵树
		// 左子树编码为0
		getCodes(tree.left, "0", sb);
		// 右子树编码为1
		getCodes(tree.right, "1", sb);
		return huffCodes;
	}

	/**
	 * 获取哈夫曼编码
	 * 
	 * @param node
	 * @param code
	 * @param sb
	 */
	private static void getCodes(Node node, String code, StringBuilder sb) {
		// 初始化一个Stirng
		StringBuilder sb2 = new StringBuilder(sb);
		// 将这个string添加上code(用以分别左右子树,0和1)
		sb2.append(code);
		// 仍是递归,结束添加是节点的数据为空,也就是父节点
		if (node.data == null) {
			getCodes(node.left, "0", sb2);
			getCodes(node.right, "1", sb2);
		} else {
			// 将之前创建的全局变量存储节点信息
			huffCodes.put(node.data, sb2.toString());
		}
	}

	/**
	 * 创建哈夫曼树
	 * 
	 * @param nodes
	 * @return
	 */
	private static Node createHuffmanTree(List<Node> nodes) {
		// 每次循环将链表中最小的两个树合并,以得到最后只有一棵树的哈夫曼树
		// 经过哈夫曼编码的树,最后仅剩余一棵没有数据,只有所有树的权的和的权
		while (nodes.size() > 1) {
			// 排序(使用Collections的排序,需要将Node节点实现Comparable并泛型为Node,重写compareTo)
			Collections.sort(nodes);
			// 取出两个权值最低的二叉树
			Node left = nodes.get(nodes.size() - 1);
			// 因为已经经过了降序排序,所以最小的两个叶子为链表的最后两个
			Node right = nodes.get(nodes.size() - 2);
			// 创建一棵新的二叉树,作为两颗删除的叶子的父节点,像这样的父节点,没有数据只有删去节点的两个权值和
			Node parent = new Node(null, left.weight + right.weight);
			// 把之前取出来的两颗二叉树设置为新创建的二叉树的字数
			parent.left = left;
			parent.right = right;
			// 把之前取出来的而二叉树删除
			nodes.remove(left);
			nodes.remove(right);
			// 把新创建的二叉树放入集合中
			nodes.add(parent);
		}
		return nodes.get(0);
	}

	/**
	 * 把byte数组转为node集合
	 * 
	 * @param bytes
	 * @return
	 */
	private static List<Node> getNodes(byte[] bytes) {
		// 初始化一个链表,泛型限制为Node
		List<Node> nodes = new ArrayList<>();
		// 用HashMap存储每一个byte出现了多少次
		Map<Byte, Integer> counts = new HashMap<>();
		// 遍历传进来的byte数组,以统计每一个byte出现的次数
		for (byte b : bytes) {
			// 用Integer对象进行计数
			Integer count = counts.get(b);
			// 便于判断map中是否存在当前正在遍历的byte
			if (count == null) {
				// 不存在,添加新的,赋值value是1
				counts.put(b, 1);
			} else {
				// 存在则覆盖以当前byte为key的节点的value(即加一)
				counts.put(b, count + 1);
			}
		}
		// 把每一个键值对转为一个node对象
		// 这里值得注意的是,因为是要使用HashMap中的entrySet,并且HashMap的Entry是基础Map的,所以使用
		// foreach的时候,单个的对象需要为Map.Entry
		for (Entry<Byte, Integer> entry : counts.entrySet()) {
			nodes.add(new Node(entry.getKey(), entry.getValue()));
		}
		return nodes;
	}
}

这里我输出了编码前与编码后,传输的代码的长度

44
16

原本的字符串长度为44,但是在编码以后,长度变为了16

后面再更新解码的代码

我想成为一个温柔的人,因为曾被温柔的人那样对待,深深了解那种被温柔相待的感觉。
——夏目友人帐

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值