1、基本介绍
如下:
2、应用实例
创建赫夫曼树
生成赫夫曼编码和赫夫曼编码后的数据
使用赫夫曼编码解码
package tree;
import java.util.*;
/*
实现步骤:
① 将对应字符串转为字节数组(105, 32, 108, 105, 107, 101, 32, 108, 105, 107......)
② 将字节数组转为类型为Node的List集合(Node{data=32, weight=9}, Node{data=97, weight=5}......)
③ 根据List集合构建哈夫曼树(Node{data=null, weight=40}Node{data=null, weight=17}Node{data=null, weight=8}....)
④ 通过哈夫曼树产生每个字符对应的哈夫曼编码(97(a) -> 100、100(d) -> 11000......)
⑤ 根据哈夫曼编码集合,将原字节数组的每个字符拼接成由0、1组成的哈夫曼编码字符串(10101000......)
⑥ 根据⑥生成的哈夫曼编码字符串,将其按8位进行切割,再转成十进制,放到字节数组中并返回(-88, -65, -56, -65, -56, -65, -55......)
⑦ 根据⑦返回的经压缩的字节数组,可结合哈夫曼编码表huffmanCodes进行解压,解压后的字节数组即为原字符串(105, 32, 108, 105.....)
*/
public class HuffmanCodeDemo {
// 下面两个东西用于生成对应的哈夫曼编码时使用(173行):
public static Map<Byte, String> huffmanCode = new HashMap<>(); // 用于暂存存放哈夫曼编码
public static StringBuilder stringBuilder = new StringBuilder(); // 用于暂存并拼接路径上的0、1个数
public static void main(String[] args) {
String sourceStr = "i like like like java do you like a java";
// 1、将字符串 转为 字节数组
byte[] strBytes = sourceStr.getBytes(); // 此时有40个字符
System.out.println(Arrays.toString(strBytes));
// 2、将字节数组 转为 类型为Node的List集合
List<HNode> nodes = getNodes(strBytes);
System.out.println(nodes);
// 3、根据List集合构建哈夫曼树
HuffmanTree huffmantree = createHuffmanTree(nodes);
huffmantree.preOrderTraverse();
// 4、根据哈夫曼树,生成对应的哈夫曼编码
Map<Byte, String> huffmanCodes = getCodes(huffmantree.root, "", stringBuilder);// 根节点没有路径,设置为空
for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
System.out.print(entry.getKey() + " -> " + entry.getValue());
}
// 5、根据哈夫曼编码表,将原字节数组压缩,并返回压缩后的以补码形式表示的哈夫曼编码字节数组
byte[] huffmanCodesBytes = encode(strBytes, huffmanCodes); // 此时有17个字符
System.out.println(Arrays.toString(huffmanCodesBytes));
// 6、根据哈夫曼编码表,对哈夫曼编码字节数组进行解压,并返回压解压后的原字符串
byte[] targetStr = decode(huffmanCodesBytes, huffmanCodes);
System.out.println(new String(targetStr));
}
// 将被压缩字节数组的每一位字符转为0、1字符串
public static String toBinaryString(byte b, boolean flag) {
/*
个人理解:
1、遍历数组每一位时,对负数需要截取后8位,同时还需要考虑对字节数组中的正数进行高位补齐
2、但当遍历到字节数组最后一位时:
2、1.如果是正数,则无需补齐
2、2.如果是负数,则需要截取后八位
*/
int temp = b; // 将其转为int类型,用于获取对应的二进制字符串
String str = null; // 存放二进制字符串
if (flag) { // 为true:表示该字符为字节数组的最后一位,如果是正数,表示该字符无需高位补齐。如果是负数,则需要进行截取
if (temp < 0) {
str = Integer.toBinaryString(temp);
return str.substring(str.length() - 8); // 截取后8位
} else {
return Integer.toBinaryString(temp);
}
} else {
temp |= 256; // 为false,表该字符不是字节数组的最后一位
str = Integer.toBinaryString(temp);
return str.substring(str.length() - 8); // 截取后8位
}
}
private static byte[] decode(byte[] huffmanCodesBytes, Map<Byte, String> huffmanCodes) {
// 1、将哈夫曼编码字节数组huffmanCodesBytes的每一个字符(-88, -65, -56, -65, -56)拼接成二进制字符串(10101000......)
StringBuilder stringBuilder = new StringBuilder(); // 用于拼接二进制字符串
for (int i = 0; i < huffmanCodesBytes.length; i++) {
boolean flag = (i == huffmanCodesBytes.length - 1); // 字节数组最后一位需特殊处理,要考虑其是否满八位的情况
stringBuilder.append(toBinaryString(huffmanCodesBytes[i], flag)); // 逐个读取字符,从而获取到对应的二进制字符串
}
System.out.println(stringBuilder.toString());
// 2、按照哈夫曼编码,对二进制字符串进行解码
// 因为要查字符串多个二进制位 对应 的字符,而原哈夫曼编码集合形式是<Byte,String>,所以需要将哈夫曼编码表进行反转
Map<String, Byte> temp = new HashMap<>();
for (Map.Entry entry : huffmanCodes.entrySet()) {
temp.put((String) entry.getValue(), (Byte)entry.getKey());
}
List<Byte> ch = new ArrayList<>();
// 3、遍历二进制字符串,从而获取到对应的字符
for (int i = 0; i < stringBuilder.length(); ) {
// 通过循环不断地截取对应的二进制个数,判断temp集合中是否有该哈夫曼编码对应的字符
int count = 1;
boolean flag = true;
Byte b = null;
while (flag) {
String key = stringBuilder.substring(i, i + count); // 一个一个二进制位匹配
b = temp.get(key); // 判断key这个哈夫曼编码在temp集合中是否存在
if (b == null) {
count++; // 如果不存在,则遍历到下一个二进制位
} else {
flag = false; // 如果存在,则已经获取到一个哈夫曼编码对应的字符,则退出循环
}
}
ch.add(b);
i += count; // i移动到count的位置,寻找下一个哈夫曼编码对应的字符
}
// 将List集合中的数据放到字节数组,并返回
byte[] bytes = new byte[ch.size()];
for (int i = 0; i < bytes.length; i++) {
bytes[i] = ch.get(i);
}
return bytes;
}
/*
1、根据哈夫曼编码集合huffmanCodes的哈夫曼编码,将原始的字节数组strBytes每一个字符拼接成一个由0、1组成的字符串
2、然后将字符串按8位进行分割,再转成十进制,返回byte类型的数组
*/
private static byte[] encode(byte[] strBytes, Map<Byte, String> huffmanCodes) {
StringBuilder stringBuilder = new StringBuilder(); // 用于拼接字节数组每一个字符所对应的哈夫曼编码
int len = 0; // 统计哈夫曼编码字节数组的长度
int index = 0; // 作为哈夫曼编码字节数组的下标,用于存放1个字节对应的数据
String temp; // 暂存所截取的二进制字符串
// 1、将字节数组转换成由0、1组成的哈夫曼编码字符串
for (byte b : strBytes) {
// 获取字节数组的每一个字符,然后去哈夫曼编码集合中,获取该字符对应的哈夫曼编码(0、1字符串)
stringBuilder.append(huffmanCodes.get(b));
}
System.out.println(stringBuilder.toString());
// 2、将哈夫曼编码字符串中的0、1按8位进行分割
if (stringBuilder.length() % 8 == 0) { // 判断该哈夫曼编码字符串可以组成多少个十进制数
len = stringBuilder.length() / 8;
} else {
len = stringBuilder.length() / 8 + 1;
}
byte[] huffmanCodeBytes = new byte[len]; // 存放哈夫曼编码字符串对应的以补码形式表示的字符
for (int i = 0; i < stringBuilder.length(); ) {
if (i + 8 > stringBuilder.length()) { // 考虑到字符串末尾时,不足八位的情况
temp = stringBuilder.substring(i); // 则从当前位开始截取,直到字符串末尾
huffmanCodeBytes[index] = (byte) Integer.parseInt(temp, 2); // 将其转为二进制的形式
break;
}
temp = stringBuilder.substring(i, i + 8);
huffmanCodeBytes[index] = (byte)Integer.parseInt(temp, 2); // 将其转为二进制的形式
i+=8; // 每8位存放一个字符
index++;
}
return huffmanCodeBytes; // 将对应的哈夫曼编码字节数组返回(全为补码)
}
private static Map<Byte, String> getCodes(HNode node, String code, StringBuilder stringBuilder) {
// 用于拼接每个叶子节点上的0、1
StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
// 将路径上的0、1拼接到StringBuilder
stringBuilder2.append(code);
// 处理不为null的节点
if (node != null) {
// 为null时,表明该节点是非叶子节点,则递归统计根节点到该节点路径上0、1的个数
if (node.data == null) {
getCodes(node.left, "0", stringBuilder2);
getCodes(node.right, "1", stringBuilder2);
} else {
// 不为null时,则表明该节点是叶子节点,这时将根节点到当前叶子结点上的哈夫曼编码添加到map集合中
huffmanCode.put(node.data, stringBuilder2.toString());
}
}
return huffmanCode;
}
private static HuffmanTree createHuffmanTree(List<HNode> nodes) {
while (nodes.size() > 1) {
// 1、对List<Node>集合进行排序
Collections.sort(nodes);
// 2、取出两棵权值最小的二叉树
HNode node1 = nodes.get(0);
HNode node2 = nodes.get(1);
// 3、构建一个棵新的二叉树,并重新添加到List集合
HNode root = new HNode(null, node1.weight + node2.weight);
root.left = node1;
root.right = node2;
nodes.add(root);
// 4、将原来那两棵二叉树从List集合中移除
nodes.remove(node1);
nodes.remove(node2);
}
// 5、当List集合仅剩一个元素时,该元素即为哈夫曼树的根节点
return new HuffmanTree(nodes.get(0));
}
private static List<HNode> getNodes(byte[] strBytes) {
List<HNode> nodes = new ArrayList<>(); // 用于存放Node节点的List集合
Map<Byte, Integer> map = new HashMap<>(); // 用于暂存每个字符及出现次数的映射关系
for (byte b : strBytes) {
Integer count = map.get(b); // 判断当前字符在map中是否有值
if (Objects.isNull(count)) { // 返回结果为null,表示当前字符还未放入map集合
map.put(b, 1); // 将当前字符放入map集合,key为字符本身,value为字符出现的次数
} else {
map.put(b, count + 1); // 不为null,表明map集合中已有该字符,则将其出现次数加1并再次放到map集合中
}
}
// 将map中每一个[key为字符,value为字符出现个数]的元素 转化为Node对象 并放入到List集合中
for (Map.Entry entry : map.entrySet()) {
nodes.add(new HNode((Byte) entry.getKey(), (Integer) entry.getValue()));
}
return nodes;
}
}
class HuffmanTree {
public HNode root;
public HuffmanTree(HNode root) {
this.root = root;
}
// 前序遍历
public void preOrderTraverse() {
if (this.root == null) {
return;
} else {
System.out.print("前序遍历:");
this.root.preOrderTraverse();
}
}
}
class HNode implements Comparable<HNode> { // 对HNode对象进行集合排序
public Byte data; // 字符对应的ASCII
public Integer weight; // 权值
public HNode left;
public HNode right;
public HNode(Byte data, Integer weight) {
this.data = data;
this.weight = weight;
this.left = null;
this.right = null;
}
// 前序遍历
public void preOrderTraverse() {
System.out.print(this);
if (this.left != null) {
this.left.preOrderTraverse();
}
if (this.right != null) {
this.right.preOrderTraverse();
}
}
@Override
public int compareTo(HNode o) {
// 升序排序
return this.weight - o.weight;
}
@Override
public String toString() {
return "Node{" +
"data=" + data +
", weight=" + weight +
'}';
}
}