6.4 赫夫曼树

  赫夫曼树是一棵压缩树。其基本理论是先统计字符串里的字符出现频率,然后用短码替换频率高的字符,用长码替换频率低的字符。
  但是要实现必须满足两个要求:
  一 替换码不能重复
  二 没有二义性
  赫夫曼选择了树来实现。赫夫曼树只用叶子来代表字符。以到根节点的路径来表示编码。左路径用0,右路径用1表示。
  这可以很轻松地理解替换码不重复。因为到根节点的路径是唯一的。
  下图是一个典型的赫夫曼树
6.4 赫夫曼树

  但是无二义性怎么理解呢?
  把上述赫夫曼树的编码显示出来是这个样子:
6.4 赫夫曼树

  无二义性是靠只用叶子来表示字符决定的。
  比如上图的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。压缩比接近四分之一,可见赫夫曼树算法的优越性。

上一篇:一篇文章教你用 11 行 Python 代码实现神经网络


下一篇:-Random Forest