引言
在计算机科学领域,数据结构与算法是构建高效软件系统的重要基石。其中,哈夫曼树(Huffman Tree)作为一种特殊的二叉树,以其在数据压缩领域的卓越表现而闻名。本文将深入探讨哈夫曼树的原理、构造过程以及实际应用场景。
一、什么是哈夫曼树?
哈夫曼树,又称最优二叉树或最小带权路径长度树,是一种带权重的二叉树,由美国计算机科学家戴维·A·哈夫曼于1952年提出。它的特点是通过自底向上、结合的方式构建,使得树中每个叶子节点代表一个字符,且整棵树的加权路径长度(所有叶子节点到根节点的路径长度之和)最小。
二、哈夫曼树的构造过程
-
频率统计:首先统计给定字符集中的每个字符出现的频率,形成字符-频率对。
-
构建最小堆:将每个字符-频率对视为一个节点,根据频率大小构建一个最小堆,保证父节点的频率总是小于或等于其子节点。
-
节点合并:从最小堆中取出两个频率最小的节点,创建一个新的内部节点作为它们的父节点,其频率为两个子节点频率之和。然后将新节点重新插入到最小堆中。
-
重复操作:不断重复第三步,直到最小堆中只剩下一个节点,这个节点即为哈夫曼树的根节点。
三、哈夫曼编码与数据压缩
哈夫曼编码 是基于哈夫曼树的一种前缀编码方式。对于哈夫曼树中的每一个叶子节点(对应一个字符),从根节点到该叶子节点的路径定义了该字符的编码。由于高频字符对应的路径较短,低频字符对应的路径较长,因此哈夫曼编码具有“短码长优先”的特点,能够实现数据的高效压缩。
四、哈夫曼树的应用场景
-
数据压缩:哈夫曼编码广泛应用于文件压缩工具,如文本压缩、图片压缩、视频压缩等领域。通过使用哈夫曼编码进行无损压缩,可以显著减少存储空间需求。
-
网络传输:在网络通信中,为了节省带宽资源,也会采用哈夫曼编码对传输数据进行压缩。
-
操作系统内核:在某些操作系统内核中,用于进程调度的数据结构也借鉴了哈夫曼树的思想,优化调度效率。
-
搜索引擎:在信息检索领域,哈夫曼树可用于构建倒排索引,提高查询速度。
五、哈夫曼树的代码实践
1.节点类
class Node1 implements Comparable<Node1> {
Byte data; //存放数据字符本身 a -> 97
int weight; //权值,字符的次数
Node1 lift;
Node1 right;
public Node1(Byte data, int weight) {
this.data = data;
this.weight = weight;
}
@Override
public String toString() {
return "Node{" + "data=" + data + ", weight=" + weight + '}';
}
@Override
public int compareTo(Node1 o) {
return this.weight - o.weight;
}
// 前序遍历
public void preOrder() {
System.out.println(this);
if (this.lift != null) {
this.lift.preOrder();
}
if (this.right != null) {
this.right.preOrder();
}
}
}
2.构建哈夫曼树的集合
/**
* @param bytes 接收字节的数组
* @return 返回构建哈夫曼树的集合
*/
public static List<Node1> getNodes(byte[] bytes) {
List<Node1> node1s = new ArrayList<>();
HashMap<Byte, Integer> counts = new HashMap<>();
for (byte data : bytes) {
Integer count = counts.get(data);
if (count == null) {
counts.put(data, 1);
} else {
counts.put(data, count + 1);
}
}
// 构建node对象
counts.entrySet().forEach((item) -> {
node1s.add(new Node1(item.getKey(), item.getValue()));
});
return node1s;
}
3.构建哈夫曼树
//根据构建哈夫曼的集合创建哈夫曼树
public static Node1 createHuffmanTree(List<Node1> list) {
while (list.size() > 1) {
// 先进行排序
Collections.sort(list);
// 取出两个权值最小的节点
Node1 liftNode = list.get(0);
Node1 rightNode = list.get(1);
// 重新构建一个二叉树 根节点没有data数据只有权值
Node1 parent = new Node1(null, liftNode.weight + rightNode.weight);
parent.lift = liftNode;
parent.right = rightNode;
// 将父节点加入到集合
list.add(parent);
// 删除两个小节点
list.remove(liftNode);
list.remove(rightNode);
}
// 返回的就是根节点
return list.get(0);
}
4.获取哈夫曼编码
// 生成哈夫曼树对应的哈夫曼编码
// 思路
// 1.将哈弗曼编码存放在Map<Byte,String>形式
static Map<Byte, String> huffmanCodes = new HashMap<Byte, String>();
// 2.在生成哈夫曼编码表示,需要去拼接路径,定义一个StringBuilder,存储某个叶子节点的路径
static StringBuilder stringBuilder = new StringBuilder();
// 为了调用方便将生成哈夫曼编码方法简化
public static Map<Byte, String> getCodes(Node1 root) {
if (root == null) {
throw new RuntimeException("节点为空!");
}
getCodes(root.lift, "0", stringBuilder);
getCodes(root.right, "1", stringBuilder);
return huffmanCodes;
}
/**
* 目的:将传入的node节点的哈夫曼编码得到并放入huffmanCodes中 生成哈夫曼编码
*
* @param node1 传入节点
* @param code 路径:左子节点为0,右子节点为1
* @param stringBuilder 用于拼接路径
*/
public static void getCodes(Node1 node1, String code, StringBuilder stringBuilder) {
StringBuilder stringBuilder1 = new StringBuilder(stringBuilder);
stringBuilder1.append(code);
if (node1 != null) {
// 判断当前节点是叶子节点还是非叶子节点
if (node1.data == null) { //非叶子节点
// 向左递归处理
getCodes(node1.lift, "0", stringBuilder1);
// 向右递归
getCodes(node1.right, "1", stringBuilder1);
} else { //叶子节点
// 表示找到某个叶子节点的最后
huffmanCodes.put(node1.data, stringBuilder1.toString());
}
}
}
5.哈夫曼数据压缩
// 根据字符串对应的byte[] 和哈夫曼编码表,返回一个哈夫曼编码压缩后的byte[]
/**
* @param bytes 原始字符串对应的数组
* @param huffmanCodes 生成的哈夫曼编码
* @return 返回根据哈夫曼编码压缩的只有01的编码
*/
public static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
StringBuilder stringBuilder = new StringBuilder();
for (byte aByte : bytes) {
stringBuilder.append(huffmanCodes.get(aByte));
}
int len;
if (stringBuilder.length() % 8 == 0) {
len = stringBuilder.length() / 8;
} else {
len = stringBuilder.length() / 8 + 1;
}
byte[] huffmanCodeBytes = new byte[len];
int index = 0;
for (int i = 0; i < stringBuilder.length(); i += 8) {
String strByte;
if (i + 8 > stringBuilder.length()) {
strByte = stringBuilder.substring(i);
} else {
strByte = stringBuilder.substring(i, i + 8);
}
huffmanCodeBytes[index] = (byte) Integer.parseInt(strByte, 2);
index++;
}
return huffmanCodeBytes;
}
6.哈夫曼数据解码
// 将根据哈夫曼编码压缩后的数组转换为原始数组
/**
* 将一个压缩后的byte转化为二进制字符串
*
* @param bytes 压缩后的数组
* @param flag 标识是否需要补高位 TRUE 补 FALSE 不补
* @return 该bytes对应的二进制按补码返回
*/
public static String byteToBitString(boolean flag, byte bytes) {
int temp = bytes;
if (flag) {
temp |= 256;
}
String binaryString = Integer.toBinaryString(temp);
if (flag) {
return binaryString.substring(binaryString.length() - 8);
} else {
return binaryString;
}
}
// 对压缩数据的解码方法
/**
* @param huffmanCodes 哈夫曼编码表
* @param huffmanBytes 被哈夫曼编码表编码的字节数组
* @return 原来字符串对应的数组
*/
public static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < huffmanBytes.length; i++) {
// 判断是否为最后一个字节
boolean flag = (i == huffmanBytes.length - 1);
stringBuilder.append(byteToBitString(!flag, huffmanBytes[i]));
}
Map<String, Byte> map = new HashMap<>();
for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
map.put(entry.getValue(), entry.getKey());
}
List<Byte> list = new ArrayList<>();
for (int i = 0; i < stringBuilder.length(); ) {
int count = 1;
boolean flag = true;
Byte b = null;
while (flag) {
String key = stringBuilder.substring(i, i + count);
b = map.get(key);
if (b == null) {
count++;
} else {
flag = false;
}
}
list.add(b);
i += count;
}
byte[] b = new byte[list.size()];
for (int i = 0; i < b.length; i++) {
b[i] = list.get(i);
}
return b;
}
六、总结
哈夫曼树凭借其独特的优势,在数据压缩和信息处理等多个领域都发挥着重要作用。理解和掌握哈夫曼树的构造原理及哈夫曼编码方法,不仅有助于我们解决实际工程问题,还能深化对数据结构与算法设计的理解。随着数据量的爆炸性增长,哈夫曼树及其编码技术将在未来继续扮演关键角色,助力我们更高效地管理和利用海量数据资源。