哈夫曼树与哈夫曼编码

1. 哈夫曼树(Huffman Tree)

哈夫曼树/赫夫曼树,又称最优树,是一种带权路径最短的二叉树,在信息检索中应用广泛。

特点:

  • 树的带权路径只考虑叶子节点,针对相同叶子结点个数与权值,可以构造出结构不同的二叉树。
  • 权值较大的结点离根较近,而权值较小的结点离根最远,以此保证树的带权路径长度最小。

路径和路径长度

  • 路径:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。
  • 路径长度:路径经过的分支数目称为路径长度,若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1。

结点的权值及带权路径长度

  • 权值:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。
  • 结点的带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积。

树的带权路径长度

  • 树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL:
    W P L = ∑ i = 1 n W i ∗ l i WPL=\displaystyle \sum^{n}_{i=1}{W_i * l_i} WPL=i=1nWili
  • n 为叶子结点的个数, W i W_i Wi 为第 i 个叶子结点的权值, l i l_i li 为第 i 个叶子结点的路径长度。

如下图:

哈夫曼树
3 棵树的带权路径长度分别如下:

  • a 树:WPL = (7 * 2) + (5 * 2) + (2 * 2) + (4 * 2) = 36
  • b 树:wpl = (7 * 3) + (5 * 3) + (2 * 1) + (4 * 2) = 46
  • c 树:WPL = (7 * 1) + (5 * 2) + (2 * 3) + (4 * 3) = 35
  • c 的形式就是最优二叉树,即哈夫曼树。

2. 构建哈夫曼树

构建哈夫曼树

步骤:

  • 1 将结点按照权值从小到大排序。
  • 2 取出权值最小的两个结点,将其的权值相加构成一个新的结点。
  • 3 删除相加的两个结点,并将新的结点添加到序列中,再次排序。
  • 4 重复以上 (1) (2) (3) 过程,直至只剩下一个结点,这个结点为根节点。

定义树节点:

class HuffmanTreeNode implements Comparable<HuffmanTreeNode> {
    public int weight;  // 结点权值
    public HuffmanTreeNode left;  // 左子树
    public HuffmanTreeNode right;  // 右子树

    public HuffmanTreeNode(int weight) {
        this.weight = weight;
    }

    //  先序遍历
    public void preOrder() {
        System.out.printf("%d\t", this.weight);  // 输出当前结点
        if (this.left != null) this.left.preOrder();  // 左递归
        if (this.right != null) this.right.preOrder();  // 右递归
    }

    @Override
    public String toString() {
        return "HuffmanTreeNode{" +
                "value=" + weight +
                '}';
    }

    /**
     * Comparable 接口中的方法,在 Collections 调用 sort 时调用此方法。
     *
     * return this.value - o.value,升序排序
     * return o.value - this.value,降序排序
     *
     * @param o 传入比较的对象
     * @return 两个对象的大小,取值为 负数、0和正数
     */
    @Override
    public int compareTo(HuffmanTreeNode o) {
        // 从小到大排序
        return this.weight - o.weight;
    }
}

创建哈夫曼树:

public class HuffmanTreeDemo {
    public static void main(String[] args) {
        int[] arr = {15, 4, 13, 7, 5, 2};
        //HuffmanTreeNode root = createHuffmanTree(arr);
        //root.preOrder();
        // 1. 创建存储树结点的集合 nodes
        List<HuffmanTreeNode> nodes = new ArrayList<>();
        // 2. 将数组中的元素封装为树结点,并存入 nodes 集合
        for (int num : arr) {
            nodes.add(new HuffmanTreeNode(num));
        }
        // 3. 将 nodes 中的结点生成哈夫曼树
        while (nodes.size() > 1) {
            // 3.1 根据权值,从小到达进行排序
            Collections.sort(nodes);
            // 3.2 取出权值最小的结点作为左右孩子进行组装
            HuffmanTreeNode left = nodes.get(0);
            HuffmanTreeNode right = nodes.get(1);
            HuffmanTreeNode temp = new HuffmanTreeNode(left.weight + right.weight);
            temp.left = left;
            temp.right = right;
            // 3.3 将新组装的结点加入原集合中,删除已用的两个最小的结点
            nodes.add(temp);
            nodes.remove(left);
            nodes.remove(right);
        }
        // 4. 当循环结束后,就生成了哈夫曼树,此时 nodes 中只剩下一个结点,即根结点
        HuffmanTreeNode root = nodes.get(0);
        // 5. 先序遍历验证结果
        root.preOrder();
    }
}

在这里插入图片描述

生成结果

  • 与动图结果一致。

3. 哈夫曼编码

  • 对于二叉树而言,规定向左分支标记为0,向右的分支标记为1,从根节点到达叶子节点所经过分支构成的代码序列就是哈夫曼编码。

哈夫曼编码树


如上图所示中,A、B、C、D 的编码分别如下:

  • A:0
  • B:10
  • C:110
  • D:111

哈夫曼编码是一种前缀编码,任意两个叶子节点的编码不会重复且不会产生歧义。


示例:

对字符串进行编码和解码操作。

要求:

  • 统计字符串中出现的每一个字符的出现次数。
  • 将每个字符的出现次数作为权值,并将该字符与字符出现次数封装成树节点,生成哈夫曼树。
  • 遍历哈夫曼树,生成哈夫曼编码表和哈夫曼解码表。
  • 根据哈夫曼编码表对字符串进行编码,根据哈夫曼解码表对哈夫曼编码进行解码恢复原字符串。

定义树节点:

public class HuffmanEncodingNode implements Comparable<HuffmanEncodingNode> {
    public Character data;  // 存放具体的字符
    public int weight;  // 字符对应的权值
    public HuffmanEncodingNode left;
    public HuffmanEncodingNode right;

    public HuffmanEncodingNode(Character data, int weight) {
        this.data = data;
        this.weight = weight;
    }

    @Override
    public String toString() {
        return "HuffmanEncodingNode{" +
                "data=" + data +
                ", weight=" + weight +
                '}';
    }
    
    @Override
    public int compareTo(HuffmanEncodingNode o) {
        return this.weight - o.weight;
        //return o.weight - this.weight;
    }
}

定义哈夫曼编码与解码功能的类:

public class HuffmanEncoding {
    public Map<Character, String> huffmanCodeTable = new HashMap<>();  // 哈夫曼编码表
    public StringBuilder huffmanCode = new StringBuilder();  // 拼接每个叶子结点的编码

    /**
     * 获取哈夫曼编码结果
     *
     * @param s 待编码的字符串
     * @return 编码字符串
     */
    public String getHuffmanCode(String s) {
        // 1. 封装结点,以字符出现的次数作为权值,字符串作为数据
        List<HuffmanEncodingNode> nodes = getNodes(s.toCharArray());
        // 2. 将结点集合生成哈夫曼树并获取根结点
        HuffmanEncodingNode root = createHuffmanTree(nodes);
        // 3. 遍历哈夫曼树,生成哈夫曼编码表
        createHuffmanCodeTable(root, "", huffmanCode);
        // 4. 根据哈夫曼编码表,对原字符串进行编码,并返回结果。
        return huffmanCoding(s.toCharArray());
    }

    /**
     * 根据哈夫曼编码进行解码
     *
     * @param code 哈夫曼编码
     * @return 解码后的字符串
     */
    public String decode(String code) {
        // 生成一个解码表,将哈夫曼编码表 huffmanCodeTable 的 key 与 value 置换
        Map<String, Character> huffmanDecodeTable = new HashMap<>();
        for (Map.Entry<Character, String> entry : huffmanCodeTable.entrySet()) {
            huffmanDecodeTable.put(entry.getValue(), entry.getKey());
        }
        // 根据哈夫曼解码表,对哈夫曼编码进行解码
        int l = 0;
        int r = 0;
        // 拼接解码后的字符
        StringBuilder res = new StringBuilder();
        while (r < code.length()) {
            // substring,左闭右开,取 (l, r + 1)
            Character c = huffmanDecodeTable.get(code.substring(l, r + 1));
            if (c == null) {  // 没有匹配到编码,右指针后移一位继续匹配
                r += 1;
            } else {  // 匹配到编码,拼接当前字符
                res.append(c);
                r += 1;  // 右指针后移以为,继续匹配
                l = r;  // 左指针也指向后指针,继续向后截取编码
            }
        }
        return res.toString();
    }

    /**
     * 统计字符串中出现的字符及该字符出现的次数
     * <p>
     * 将每个字符及其出现次数封装为树结点,data 存储字符,weight 存储出现次数。
     *
     * @param bytes 字符串转化的字节数组
     * @return 封装为结点后的集合
     */
    public List<HuffmanEncodingNode> getNodes(char[] bytes) {
        // 1. 创建 ArrayList 存储哈夫曼树的结点
        List<HuffmanEncodingNode> nodes = new ArrayList<>();
        // 2. 创建存储字符及其出现的 Map 集合
        Map<Character, Integer> counts = new HashMap<>();
        // 3. 开始统计字符出现次数
        for (Character b : bytes) {
            counts.merge(b, 1, Integer::sum);
        }
        // 4. 将 Map 集合中的数据封装成结点,存入 List 中
        for (Map.Entry<Character, Integer> entry : counts.entrySet()) {
            nodes.add(new HuffmanEncodingNode(entry.getKey(), entry.getValue()));
        }
        // 5. 返回封装结果
        return nodes;
    }

    /**
     * 生成哈夫曼树
     *
     * @param nodes 待生成哈夫曼树的结点集合
     * @return 哈夫曼树的根节点
     */
    public HuffmanEncodingNode createHuffmanTree(List<HuffmanEncodingNode> nodes) {
        while (nodes.size() > 1) {
            // 1. 先升序排序集合
            Collections.sort(nodes);
            // 2. 取出最小的两个结点,作为左右孩子结点,组成新的二叉树结构
            HuffmanEncodingNode left = nodes.get(0);
            HuffmanEncodingNode right = nodes.get(1);
            // 3. 组成新的结点,新节点不存数据。
            HuffmanEncodingNode temp = new HuffmanEncodingNode(null, left.weight + right.weight);
            temp.left = left;
            temp.right = right;
            // 4. 将新结点添加到结点数组中,并删除已用的两个结点。
            nodes.add(temp);
            nodes.remove(left);
            nodes.remove(right);
        }
        // 5. 循环结束后,就生成了哈夫曼树,此时结点集合 nodes 中只剩下一个结点是根节点,返回即可。
        return nodes.get(0);
    }

    /**
     * 递归生成哈夫曼编码表
     *
     * @param node        当前待编码结点
     * @param code        上个结点到当前结点的编码值,左孩子编码 0,右孩子编码 1
     * @param huffmanCode 从根结点到当前结点的编码值
     */
    private void createHuffmanCodeTable(HuffmanEncodingNode node, String code, StringBuilder huffmanCode) {
        // 先存储上个结点到当前结点的编码值
        StringBuilder temp = new StringBuilder(huffmanCode);
        temp.append(code);
        // 如果当前结点不为为 null 时,处理当前结点
        if (node != null) {
            // 非叶子结点,data == null,继续向后递归
            if (node.data == null) {
                // 左递归时,编码 0,并传入已编码好的路径
                createHuffmanCodeTable(node.left, "0", temp);
                // 右递归时,编码 1,并传入已编码好的路径
                createHuffmanCodeTable(node.right, "1", temp);
            } else {// 如果遇到叶子结点,就将当前结点的 data 与编码存入哈夫曼编码表 huffmanCodeTable
                huffmanCodeTable.put(node.data, temp.toString());
            }
        }
    }

    /**
     * 根据哈夫曼编码表对原字符串进行编码
     *
     * @param s 待编码字符串的字符数组
     * @return 哈夫曼编码
     */
    private String huffmanCoding(char[] s) {
        StringBuilder code = new StringBuilder();
        for (Character b : s) {
            code.append(huffmanCodeTable.get(b));
        }
        return code.toString();
    }
}

测试编码和解码操作:

public class TestDemo {
     public static void main(String[] args) {
        HuffmanEncoding demo = new HuffmanEncoding();
        String s = "编码解码!";
        System.out.println("原字符串:" + s);
        // 哈夫曼编码
        String huffmanCode = demo.getHuffmanCode(s);
        System.out.println("编码值:" + huffmanCode);

        // 打印当前编码表
        System.out.println("当前编码表:" + demo.huffmanCodeTable);

        // 解码
        String decode = demo.decode(huffmanCode);
        System.out.println("解码后的字符串:" + decode);
    }
}

测试结果

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值