十六、哈夫曼编码(Huffman Coding)
1、概念
它是哈夫曼树(Huffman Tree)在电讯通信领域中的经典应用。广泛应用于数据文件的压缩,压缩率一般在20% ~ 90%
之间,属于**可变长编码(VLC)**的一种,重复的内容越多,压缩率越好。
但是一个文件如果已经是压缩过的,那么再次压缩,其文件大小可能不会有明显的变化。
它属于无损压缩。
2、原理
定长编码,将信息的原本形式,先转换为字节数组(byte[]
),然后再获取每一个字节所对应的二进制(低8位),再进行数据的传输。
变长编码,将信息的原本形式,按照某种规则,进行转换,得到对应的二进制(低8位),再进行数据的传输。
以哈夫曼编码为例,它会将每个字节转换为新的二进制(不一定够8位),即统计每个字节的重复次数,重复得越多,其对应的二进制也就越小,并创建一个字典用于映射二进制和字节之间的关系,每个二进制都不能是其它二进制的前缀,这也叫前缀编码。
3、编码过程
- 统计出每个字节的重复次数,作为哈夫曼树节点的权重。字节即哈夫曼树节点的值。构造出一棵哈夫曼树(字节重复次数较多的,距离根节点较近)。
- 依次遍历每一个节点,遍历左子节点时,添加“0”,遍历右子节点时,添加“1”。最终生成每个叶子节点的前缀编码,和对应的字节-前缀编码映射关系。
- 再遍历原信息字节数组,转换为新的对应的前缀编码字节字符串。
- 最后遍历前缀编码字节字符串,以8位为一个字节,转换为二进制字节元素,添加到字节数组中,最后的几位,如果不足8位,则直接添加每位到字节数组中。这个字节数组,即哈夫曼编码。
- 将哈夫曼编码和字节-前缀编码映射关系,一起压缩序列化。
4、解码过程
- 先解压反序列化出哈夫曼编码和字节-前缀编码映射关系。
- 根据字节-前缀编码映射关系,得到前缀编码-字节映射关系。
- 遍历哈夫曼编码(字节数组),将每个字节元素,按位或运算256(补充高位),并取低8位,倒数7个字节元素(有一定概率需要补高位,也可以优化算法,记录倒数几个字节元素不需要补充高位),需要判断是否就是字节值,即0和1,如果是,则直接返回。这样得到每个字节元素所对应的前缀编码,拼接起来,即前缀编码字节字符串。
- 遍历前缀编码字节字符串,根据前缀编码-字节映射关系,得到对应的字节元素,添加到字节数组中,即原信息字节数组。
5、示例
哈夫曼树
class TreeNode implements Comparable<TreeNode> {
private final Byte data;
private final int weight;
private TreeNode left;
private TreeNode right;
public TreeNode(Byte data, int weight) {
this.data = data;
this.weight = weight;
}
@Override
public int compareTo(TreeNode o) {
return Integer.compare(this.weight, o.weight);
}
public Byte getData() {
return data;
}
public int getWeight() {
return weight;
}
public TreeNode getLeft() {
return left;
}
public void setLeft(TreeNode left) {
this.left = left;
}
public TreeNode getRight() {
return right;
}
public void setRight(TreeNode right) {
this.right = right;
}
}
编码
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class HuffmanCodeEncoder {
/**
* 转换为哈夫曼树,并获取根节点
*/
public TreeNode getTreeRootNode(byte[] bytes) {
Map<Byte, Integer> byteCountMap = new HashMap<Byte, Integer>();
for (int i = 0, len = bytes.length; i < len; i++) {
byte b = bytes[i];
Integer count = byteCountMap.get(b);
if (null == count) {
byteCountMap.put(b, 1);
} else {
byteCountMap.put(b, count + 1);
}
}
List<TreeNode> list = new ArrayList<TreeNode>(byteCountMap.size());
for (Map.Entry<Byte, Integer> entry : byteCountMap.entrySet()) {
Byte data = entry.getKey();
Integer weight = entry.getValue();
list.add(new TreeNode(data, weight));
}
while (1 < list.size()) {
Collections.sort(list);
TreeNode left = list.get(0);
TreeNode right = list.get(1);
TreeNode parent = new TreeNode(null, left.getWeight() + right.getWeight());
parent.setLeft(left);
parent.setRight(right);
list.add(parent);
list.remove(left);
list.remove(right);
}
return list.get(0);
}
/**
* 获取字节-编码映射关系
*/
public Map<Byte, String> getByteCodeMap(TreeNode root) {
if (null == root.getLeft() && null == root.getRight()) {
Byte data = root.getData();
if (null == data) {
return null;
}
Map<Byte, String> byteCodeMap = new HashMap<Byte, String>();
byteCodeMap.put(data, "0");
return byteCodeMap;
}
Map<Byte, String> byteCodeMap = new HashMap<Byte, String>();
fillByteCodeMap(root, "", "", byteCodeMap);
return byteCodeMap;
}
/**
* 填充字节-编码映射关系
*/
private void fillByteCodeMap(TreeNode node, String code, String concat, Map<Byte, String> byteCodeMap) {
if (null == node) {
return;
}
code = code.concat(concat);
fillByteCodeMap(node.getLeft