赫夫曼树是一棵压缩树。其基本理论是先统计字符串里的字符出现频率,然后用短码替换频率高的字符,用长码替换频率低的字符。
但是要实现必须满足两个要求:
一 替换码不能重复
二 没有二义性
赫夫曼选择了树来实现。赫夫曼树只用叶子来代表字符。以到根节点的路径来表示编码。左路径用0,右路径用1表示。
这可以很轻松地理解替换码不重复。因为到根节点的路径是唯一的。
下图是一个典型的赫夫曼树
但是无二义性怎么理解呢?
把上述赫夫曼树的编码显示出来是这个样子:
无二义性是靠只用叶子来表示字符决定的。
比如上图的s,编码是0。因为s是叶子,所以不会出现00和01这样的编码。所以就不存在二义性。
假如00 表示字符A,而0又表示字符B。那么就出现了二义性。当出现00时,是两个B,还是一个A呢?
理解了了重复和二义性后,赫夫曼树最难的理论知识是长短码问题。
赫夫曼树每个节点都有权重字段。对于每个叶子节点来说,权重就是它代表的字符的出现次数。
如上图,M出现1次,那么权重就是1。
M和p加起来权重是3,才仅仅和i同级。
所以赫夫曼树是以权重相加来构建层级关系的。
那么具体的构建过程是怎么样的?
需要用到两种数据结构,一是堆,二是赫夫曼树。
其构建过程如下:
一 统计出现频率,可以使用hashmap来实现。
二 将所有字符和对应权重构建成单root节点赫夫曼树,放入堆中。
三 每次从堆中取出最小权重的两棵树,权重相加合并成新的赫夫曼树之后放回堆中,直到堆中只有一棵树。
四 遍历赫夫曼树,将所有叶子的编码以map存储。
五 将原字符串替换为赫夫曼编码。
经过这五步,压缩完成。
代码如下:
public HuffmanTree(String text) {
// 首先计算出各个字母出现的频率
final char[] chars = text.toCharArray();
this.frequency = new HashMap<>();
for (char c : chars) {
if (frequency.containsKey(c)) {
frequency.put(c, frequency.get(c) + 1);
} else {
frequency.put(c, 1);
}
}
// 组成一片森林,加入到优先队列里
final PriorityQueue<HuffmanTree> forest = new PriorityQueue<>();
frequency.forEach((key, value) -> {
forest.add(new HuffmanTree(key, value));
});
while (forest.size() > 1) {
// 取出两个来然后加回去
final HuffmanTree first = forest.poll();
final HuffmanTree second = forest.poll();
HuffmanTree merged = merge(first, second);
forest.add(merged);
}
codeMap = new HashMap<>();
this.root = forest.poll().root;
this.bfs(x -> {
HuffmanNode node = (HuffmanNode)x;
if (((HuffmanNode) x).getElement() == null) {
return true;
}
codeMap.put(node.getElement(), node.getFullCode());
return true;
});
StringBuilder stringBuilder = new StringBuilder();
for (char c : chars) {
final String s = codeMap.get(c);
stringBuilder.append(s);
}
this.result = stringBuilder.toString();
}
因为仅仅是演示赫夫曼编码原理,所以示例代码中没有将结果的01系列变成byte数组,而是把0和1的系列放入字符串中。
例如Mississippi
编码结果是100110011001110110111
原字符串是88 bit,压缩后是21bit。压缩比接近四分之一,可见赫夫曼树算法的优越性。