Huffman编码

问题引入

什么是编码?

简单说就是建立【字符】到【数字】的对应关系,如下面大家熟知的 ASC II 编码表,例如,可以查表得知字符【a】对应的数字是十六进制数【0x61】

\000102030405060708090a0b0c0d0e0f
0000000102030405060708090a0b0c0d0e0f
0010101112131415161718191a1b1c1d1e1f
002020!"#$%&'()*+,-./
00300123456789:;<=>?
0040@ABCDEFGHIJKLMNO
0050PQRSTUVWXYZ[\]^_
0060`abcdefghijklmno
0070pqrstuvwxyz{|}~7f

注:一些直接以十六进制数字标识的是那些不可打印字符

传输时的编码

  • java 中每个 char 对应的数字会占用固定长度 2 个字节Byte

  • 如果在传输中仍采用上述规则,传递 abbccccccc 这 10 个字符

    • 实际的字节为 0061006200620063006300630063006300630063(16进制表示)

    • 总共 20 个字节,不经济

现在希望找到一种最节省字节的传输方式,怎么办?

假设传输的字符中只包含 a,b,c 这 3 个字符,有同学重新设计一张二进制编码表,见下图

  • 0 表示 a

  • 1 表示 b

  • 10 表示 c

现在还是传递 abbccccccc 这 10 个字符

  • 实际的字节为 01110101010101010 (二进制表示)

  • 总共需要 17 bits,也就是 2 个字节多一点,行不行?

不行,因为解码会出现问题,因为 10 会被错误的解码成 ba,而不是 c

  • 解码后结果为 abbbababababababa,是错误的

 怎么解决?必须保证编码后的二进制数字,要能区分它们的前缀(prefix-free)

用满二叉树结构编码,可以确保前缀不重复

  • 向左走 0,向右走 1

  • 走到叶子字符,累计起来的 0 和 1 就是该字符的二进制编码

再来试一遍

  • a 的编码 0

  • b 的编码 10

  • c 的编码 11

现在还是传递 abbccccccc 这 10 个字符

  • 实际的字节为 0101011111111111111(二进制表示)

  • 总共需要 19 bits,也是 2 个字节多一点,并且解码没有问题了,行不行?

这回解码没问题了,但并非最少字节,因为 c 的出现频率高(7 次)a 的出现频率低(1 次),因此出现频率高的字符编码成短数字更经济

观察下面的树:

  • 00 表示 a

  • 01 表示 b

  • 1 表示 c

现在还是传递 abbccccccc 这 10 个字符

  • 实际的字节为 000101 1111111 (二进制表示)

  • 总共需要 13 bits,这棵树就称之为 Huffman 树

  • 根据 Huffman 树对字符和数字进行编解码,就是 Huffman 编解码

Huffman树的构建

static class Node {
        Character ch; // 字符
        int freq;     // 频次
        Node left;
        Node right;
        String code;  // 编码

        public Node(Character ch) {
            this.ch = ch;
        }

        public Node(int freq, Node left, Node right) {
            this.freq = freq;
            this.left = left;
            this.right = right;
        }

        int freq() {
            return freq;
        }

        boolean isLeaf() {
            return left == null;//因为Huffman树是满二叉树
        }

        @Override
        public String toString() {
            return "Node{" +
                    "ch=" + ch +
                    ", freq=" + freq +
                    '}';
        }
    }

计算频次:

  // 功能1:统计频率
        char[] chars = str.toCharArray();
        for (char c : chars) {
            /*if (!map.containsKey(c)) {
                map.put(c, new Node(c));
            }
            Node node = map.get(c);
            node.freq++;*/
            Node node = map.computeIfAbsent(c, Node::new);//方法引用
            node.freq++;
        }

注意

  • Node::new 是一个 Function,根据 key(即字符)生成 Node 对象

  • 对应的是 public Node(Character ch) 有参构造

构造树

  // 功能2: 构造树
        PriorityQueue<Node> queue = new PriorityQueue<>(Comparator.comparingInt(Node::freq));
        queue.addAll(map.values());
        while (queue.size() >= 2) {
            Node x = queue.poll();
            Node y = queue.poll();
            int freq = x.freq + y.freq;
            queue.offer(new Node(freq, x, y));
        }
        Node root = queue.poll();

计算每个字符的编码

 // 功能3:计算每个字符的编码, 功能4:字符串编码后占用 bits
        int sum = dfs(root, new StringBuilder());
        for (Node node : map.values()) {
            System.out.println(node + " " + node.code);
        }
        System.out.println("总共会占用 bits:" + sum);
private int dfs(Node node, StringBuilder code) {
        int sum = 0;
        if (node.isLeaf()) {
            node.code = code.toString();
            sum = node.freq * code.length();
        } else {
            sum += dfs(node.left, code.append("0"));
            sum += dfs(node.right, code.append("1"));
        }
        if (code.length() > 0) {//首次不能删
            code.deleteCharAt(code.length() - 1);
        }
        return sum;
    }

完整代码:

import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;

public class HuffmanTree {

    /*
        Huffman 树的构建过程

        1. 将统计了出现频率的字符,放入优先级队列

        2. 每次出队两个频次较低的元素,给它俩找个爹
        3. 把爹重新放入队列,重复2~3
        4. 当队列只剩下一个元素时,Huffman 树构建完成
     */

    static class Node {
        Character ch; // 字符
        int freq;     // 频次
        Node left;
        Node right;
        String code;  // 编码

        public Node(Character ch) {
            this.ch = ch;
        }

        public Node(int freq, Node left, Node right) {
            this.freq = freq;
            this.left = left;
            this.right = right;
        }

        int freq() {
            return freq;
        }

        boolean isLeaf() {
            return left == null;//因为Huffman树是满二叉树
        }

        @Override
        public String toString() {
            return "Node{" +
                    "ch=" + ch +
                    ", freq=" + freq +
                    '}';
        }
    }

    String str;// 存一份原始子字符串
    Map<Character, Node> map = new HashMap<>();

    public HuffmanTree(String str) {
        this.str = str;
        // 功能1:统计频率
        char[] chars = str.toCharArray();
        for (char c : chars) {
            /*if (!map.containsKey(c)) {
                map.put(c, new Node(c));
            }
            Node node = map.get(c);
            node.freq++;*/
            Node node = map.computeIfAbsent(c, Node::new);//方法引用
            node.freq++;
        }
        // 功能2: 构造树
        PriorityQueue<Node> queue = new PriorityQueue<>(Comparator.comparingInt(Node::freq));
        queue.addAll(map.values());
        while (queue.size() >= 2) {
            Node x = queue.poll();
            Node y = queue.poll();
            int freq = x.freq + y.freq;
            queue.offer(new Node(freq, x, y));
        }
        Node root = queue.poll();
        // 功能3:计算每个字符的编码, 功能4:字符串编码后占用 bits
        int sum = dfs(root, new StringBuilder());
        for (Node node : map.values()) {
            System.out.println(node + " " + node.code);
        }
        System.out.println("总共会占用 bits:" + sum);
    }

    private int dfs(Node node, StringBuilder code) {
        int sum = 0;
        if (node.isLeaf()) {
            node.code = code.toString();
            sum = node.freq * code.length();
        } else {
            sum += dfs(node.left, code.append("0"));
            sum += dfs(node.right, code.append("1"));
        }
        if (code.length() > 0) {//首次不能删
            code.deleteCharAt(code.length() - 1);
        }
        return sum;
    }

    public static void main(String[] args) {
        new HuffmanTree("abbccccccc");
    }
}
Huffman 编解码

补充两个方法,注意为了简单期间用了编解码都用字符串演示,实际应该按 bits 编解码

编码

  // 编码
    public String encode() {
        char[] chars = str.toCharArray();
        StringBuilder sb = new StringBuilder();
        for (char c : chars) {
            sb.append(map.get(c).code);
        }
        return sb.toString();
    }

解码

// 解码
    public String decode(String str) {
        /*
            从根节点,寻找数字对应的字符
                数字是 0 向左走
                数字是 1 向右走
                如果没走到头,每走一步数字的索引 i++
            走到头就可以找到解码字符,再将 node 重置为根节点

            a 00
            b 10
            c 1

            i
            0 0 0 1 0 1 1 1 1 1 1 1 1
         */
        char[] chars = str.toCharArray();
        int i = 0;
        StringBuilder sb = new StringBuilder();
        Node node = root;
        while (i < chars.length) {
            if (!node.isLeaf()) { // 非叶子
                if(chars[i] == '0') { // 向左走
                    node = node.left;
                } else if(chars[i] == '1') { // 向右走
                    node = node.right;
                }
                i++;
            }
            if (node.isLeaf()) {
                sb.append(node.ch);
                node = root;
            }
        }
        return sb.toString();
    }

注意

  • 循环中非叶子节点 i 要自增,但叶子节点 i 暂不自增

  • 第一个非叶子的 if 判断结束后,仍需要第二个叶子的 if 判断,因为在第一个 if 内 node 发生了变化

练习:

题目编号题目标题算法思路
1167(Plus 题目)连接棒材的最低费用Huffman 树、贪心

 1+3 ==>4

 4+5 ==>9      ==>   4+9+17 = 30

 8+9 ==>17

/**
 * <h3>连接棒材的最低费用</h3>
 * <p>为了装修新房,你需要加工一些长度为正整数的棒材。如果要将长度分别为 X 和 Y 的两根棒材连接在一起,你需要支付 X + Y 的费用。 返回讲所有棒材连成一根所需要的最低费用。</p>
 */
public class Leetcode1167 {
    /*
        举例 棒材为 [1,8,3,5]

        如果以如下顺序连接(非最优)
        - 1+8=9
        - 9+3=12
        - 12+5=17
        总费用为 9+12+17=38

        如果以如下顺序连接(最优)
        - 1+3=4
        - 4+5=9
        - 8+9=17
        总费用为 4+9+17=30
     */
    int connectSticks(int[] sticks) {
        PriorityQueue<Integer> queue = new PriorityQueue<>();
        for (int stick : sticks) {
            queue.offer(stick);
        }

        int sum = 0;
        while (queue.size() >= 2) {
            Integer x = queue.poll();
            Integer y = queue.poll();
            int c = x + y;
            sum += c;
            queue.offer(c);
        }
        return sum;
    }

    public static void main(String[] args) {
        Leetcode1167 leetcode = new Leetcode1167();
        System.out.println(leetcode.connectSticks(new int[]{1, 8, 3, 5})); // 30
        System.out.println(leetcode.connectSticks(new int[]{2, 4, 3})); // 14
    }
}

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值