霍夫曼(Huffman)编码

一、定义

霍夫曼(Huffman)编码是一种编码方式,主要用于数据文件的压缩。它的主要思想是放弃文本文件的普通保存方式:不再使用7位或8位二进制数表示每一个字符,而是用较少的比特表示出现频率高的字符,用较多的比特表示出现频率低的字符

引例:假设需要对文本字符串“ABRACADABRA!”编码

  1. 一种方式是,用较短的比特表示所有可能的字符。
    如A-0、B-1、R-00、C-01、D-10、!-11,这样“ABRACADABRA!”的编码就是0 1 00 0 01 0 10 0 1 00 0 11。这种表示方法只用了17位,而7位的ASCII编码则用了77位。但是这种方法存在一个问题:当不存在分隔符的时候,我们无法根据一连串比特码区分字符与比特码的映射关系。如01000010100100011也可以表示成CRRDDCRCB或其它字符串。
  2. 第二种方式是,如果任一字符的编码都不是其它字符编码的前缀,那么就不需要分隔符了。
    如A-0、B-1111、R-1110、C-110、D-100、!-101。而霍夫曼编码就是寻找这种变长前缀的算法,且能使最终构造出的比特流最小。
霍夫曼(Huffman)编码_第1张图片

二、实现方式

霍夫曼编码,首先需要根据输入文本,构造一棵二叉树,树的左链接表示比特"0",右链接表示比特"1",叶子结点表示字符。字符所对应的霍夫曼编码值就是从根结点到叶子结点的链接值。

霍夫曼(Huffman)编码_第2张图片

总体实现步骤如下:
【压缩步骤】
压缩用于将原始文本转换成一条编码过的比特流。

  1. 读取输入;
  2. 统计输入中每个字符的频次;
  3. 根据频次,构造Huffman树;
  4. 构造编译表,用于将字符与变长前缀映射;
  5. 将Huffman树编码为比特字符串,并写入输出流;
  6. 将文本长度编码为比特字符串,并写入输出流;
  7. 压缩数据,即使用编译表翻译每个文本字符,写入输出流。

【解压缩步骤】
解压缩用于将一条编码过的比特流转换为原始文本。

  1. 读取Huffman树(编码在比特流的开头);
  2. 读取需要解码的字符数量;
  3. 根据步骤1还原的Huffman树解码压缩数据。

三、源码实现

3.1 构造Huffman树

构造一颗Huffman树的步骤如下:
1、遍历一遍文本,统计各个字符出现的频次;
2、对每个字符构造一个叶子结点,结点包含该个字符的频次;
3、每次从所有结点中选出频次最小的两个根结点,构造一个新结点,新结点作为根结点,两个根结点作为其左右子结点,新结点的频次为左右子根结点的频次和;
4、重复第3步,直到最后只剩一个根结点。

树结点定义:

private static class Node implements Comparable {
    private final char ch;         //字符
    private final int freq;        //每个结点保存以该结点为根的子树中的字符数量
    private final Node left, right;
    Node(char ch, int freq, Node left, Node right) {
        this.ch    = ch;
        this.freq  = freq;
        this.left  = left;
        this.right = right;
    }
    private boolean isLeaf() {
        return (left == null) && (right == null);
    }
    public int compareTo(Node that) {
        return this.freq - that.freq;
    }
}

构造Huffman树:

//构造Huffman树
private static Node buildTrie(int[] freq) {
    MinPQ pq = new MinPQ();     //优先级队列
    for (char i = 0; i < R; i++)            //R为字母表
        if (freq[i] > 0)
            pq.insert(new Node(i, freq[i], null, null));
 
    //只有一个结点,特殊处理
    if (pq.size() == 1) {
        if (freq['\0'] == 0) pq.insert(new Node('\0', 0, null, null));
        else                 pq.insert(new Node('\1', 0, null, null));
    }
 
    // merge two smallest trees
    while (pq.size() > 1) {
        Node left  = pq.delMin();
        Node right = pq.delMin();
        Node parent = new Node('\0', left.freq + right.freq, left, right);
        pq.insert(parent);
    }
    return pq.delMin();
}

3.2 构造编译表

编译表就是将每个字符与它的比特字符串相关联的符号表。

// 构造编译表
private static String[] buildCode(Node root) {
    String[] table = new String[R];
    buildCode(table, root, "");
    return table;
}
private static void buildCode(String[] st, Node x, String s) {
    if (!x.isLeaf()) {
        buildCode(st, x.left, s + '0');
        buildCode(st, x.right, s + '1');
    } else {
        st[x.ch] = s;
    }
}

3.3 编码Huffman树,并压缩数据

采用先序遍历对Huffman树编码。(之所以要对Huffman树编码是为了从压缩后的比特流中还原出Huffman树,这样后续才能解码)
从根结点开始,遇到内部结点写入比特"0";遇到叶子结点,写入比特"1",然后写入字符比特值。

霍夫曼(Huffman)编码_第3张图片
/**
 * 使用先序遍历将Huffman树编码为比特流
 */
private static void writeTrie(Node x) {
    if (x.isLeaf()) {
        BinaryStdOut.write(true);
        BinaryStdOut.write(x.ch, 8);
        return;
    }
    BinaryStdOut.write(false);
    writeTrie(x.left);
    writeTrie(x.right);
}

/**
 * 压缩数据 最终的比特流结果为:Huffman树编码值+文本长度+压缩后的数据
 */
public static void compress() {
    // read the input
    String s = BinaryStdIn.readString();
    char[] input = s.toCharArray();
 
    // 统计频次
    int[] freq = new int[R];
    for (int i = 0; i < input.length; i++)
        freq[input[i]]++;
 
    // 构建Huffman树
    Node root = buildTrie(freq);
 
    // 构建编译表
    String[] st = new String[R];
    buildCode(st, root, "");
 
    // 编码Huffman树
    writeTrie(root);
 
    // 写入文本长度
    BinaryStdOut.write(input.length);
 
    // 压缩数据
    for (int i = 0; i < input.length; i++) {
        String code = st[input[i]]; // 根据编译表,得到字符对应的变长前缀
 
        for (int j = 0; j < code.length(); j++) {
            if (code.charAt(j) == '0') {
                BinaryStdOut.write(false);
            } else if (code.charAt(j) == '1') {
                BinaryStdOut.write(true);
            } else
                throw new IllegalStateException("Illegal state");
        }
    }
    BinaryStdOut.close();
}

3.4 解码Huffman树,并解压缩数据

/**
 * 将比特流解码为Huffman树
 * 
 * @return 返回Huffman树的根结点
 */
private static Node readTrie() {
    boolean isLeaf = BinaryStdIn.readBoolean();
    if (isLeaf) {
        return new Node(BinaryStdIn.readChar(), -1, null, null);
    } else {
        return new Node('\0', -1, readTrie(), readTrie());
    }
}
/**
 * 解压缩数据
 */
public static void expand() {
    // 解码比特流,还原Huffman树
    Node root = readTrie();
 
    // 解码文本长度
    int length = BinaryStdIn.readInt();
 
    // 解码文本数据
    for (int i = 0; i < length; i++) { // i追踪字符,每次循环还原一个文本字符
        Node x = root;
        while (!x.isLeaf()) {
            boolean bit = BinaryStdIn.readBoolean();
            if (bit)
                x = x.right;
            else
                x = x.left;
        }
        BinaryStdOut.write(x.ch, 8);
    }
 
    BinaryStdOut.close();
}

3.5 完整源码

public class Huffman {
    private static final int R = 256; // 字母表
    private Huffman() {
    }

    // 树结点定义
    private static class Node implements Comparable {
        private final char ch; // 字符
        private final int freq; // 子树中所有字符的频次
        private final Node left, right;

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

        private boolean isLeaf() {
            return (left == null) && (right == null);
        }

        public int compareTo(Node that) {
            return this.freq - that.freq;
        }
    }

    /**
     * 构建Haffman树
     */
    private static Node buildTrie(int[] freq) {
        MinPQ pq = new MinPQ();
        for (char i = 0; i < R; i++)
            if (freq[i] > 0)
                pq.insert(new Node(i, freq[i], null, null));
        if (pq.size() == 1) {
            if (freq['\0'] == 0)
                pq.insert(new Node('\0', 0, null, null));
            else
                pq.insert(new Node('\1', 0, null, null));
        }
        // merge two smallest trees
        while (pq.size() > 1) {
            Node left = pq.delMin();
            Node right = pq.delMin();
            Node parent = new Node('\0', left.freq + right.freq, left, right);
            pq.insert(parent);
        }
        return pq.delMin();
    }

    /**
     * 构造编译表
     */
    private static void buildCode(String[] st, Node x, String s) {
        if (!x.isLeaf()) {
            buildCode(st, x.left, s + '0');
            buildCode(st, x.right, s + '1');
        } else {
            st[x.ch] = s;
        }
    }

    /**
     * 压缩数据 最终的比特流结果为:Huffman树编码值+文本长度+压缩后的数据
     */
    public static void compress() {
        // read the input
        String s = BinaryStdIn.readString();
        char[] input = s.toCharArray();

        // 统计频次
        int[] freq = new int[R];
        for (int i = 0; i < input.length; i++)
            freq[input[i]]++;

        // 构建Huffman树
        Node root = buildTrie(freq);

        // 构建编译表
        String[] st = new String[R];
        buildCode(st, root, "");

        // 编码Huffman树
        writeTrie(root);

        // 写入文本长度
        BinaryStdOut.write(input.length);

        // 压缩数据
        for (int i = 0; i < input.length; i++) {
            String code = st[input[i]]; // 根据编译表,得到字符对应的变长前缀

            for (int j = 0; j < code.length(); j++) {
                if (code.charAt(j) == '0') {
                    BinaryStdOut.write(false);
                } else if (code.charAt(j) == '1') {
                    BinaryStdOut.write(true);
                } else
                    throw new IllegalStateException("Illegal state");
            }
        }

        BinaryStdOut.close();
    }

    /**
     * 解压缩数据
     */
    public static void expand() {
        // 解码比特流,还原Huffman树
        Node root = readTrie();

        // 解码文本长度
        int length = BinaryStdIn.readInt();

        // 解码文本数据
        for (int i = 0; i < length; i++) { // i追踪字符,每次循环还原一个文本字符
            Node x = root;
            while (!x.isLeaf()) {
                boolean bit = BinaryStdIn.readBoolean();
                if (bit)
                    x = x.right;
                else
                    x = x.left;
            }
            BinaryStdOut.write(x.ch, 8);
        }

        BinaryStdOut.close();
    }

    /**
     * 将比特流解码为Huffman树
     * 
     * @return 返回Huffman树的根结点
     */
    private static Node readTrie() {
        boolean isLeaf = BinaryStdIn.readBoolean();
        if (isLeaf) {
            return new Node(BinaryStdIn.readChar(), -1, null, null);
        } else {
            return new Node('\0', -1, readTrie(), readTrie());
        }
    }

    /**
     * 使用先序遍历将Huffman树编码为比特流
     */
    private static void writeTrie(Node x) {
        if (x.isLeaf()) {
            BinaryStdOut.write(true); // 叶子结点写入"1"
            BinaryStdOut.write(x.ch, 8); // 写入字符的比特值
            return;
        }
        BinaryStdOut.write(false); // 内部结点写入"0"
        writeTrie(x.left);
        writeTrie(x.right);
    }

    /**
     * 用例: Execution: java Huffman - < input.txt (compress) Execution: java
     * Huffman + < input.txt (expand)
     */
    public static void main(String[] args) {
        if (args[0].equals("-"))
            compress();
        else if (args[0].equals("+"))
            expand();
        else
            throw new IllegalArgumentException("Illegal command line argument");
    }
}

四、最优性证明

为什么根据Huffman算法,生成的字符变长前缀之和是最优的?

首先明确一个概念,即加权外部路径长度:表示Huffman树中叶子结点的频次 X 叶子结点高度的和。
*Huffman树的加权外部路径长度,就是文本编码后总比特长度。
要证明Huffman算法构造的变长前缀是最优的,就是证明Huffman树的加权外部路径长度是最短的。可以采用数学归纳法得到证明,具体可以参考《算法导论》或相关论文。

你可能感兴趣的:(霍夫曼(Huffman)编码)