1. 程式人生 > 其它 >【演算法4】5.5.6.哈夫曼編碼

【演算法4】5.5.6.哈夫曼編碼

哈夫曼編碼(又稱 Huffman Coding / 霍夫曼編碼)。

在傳輸資料時,資料最終需要被編碼成 01 再寫入資料流。
哈夫曼編碼提供了一種動態編碼的方案,每個字元不再以固定的長度進行編碼(比如 8 位長度),
而是使用較少的位元表示出現頻率高的字元,用較多的位元表示出現頻率較低的字元,以此來降低總位元數。

字首編碼

所有字元編碼都不會成為其他字元編碼的字首,含有這種性質的編碼規則叫做字首碼。

字首碼不需要分隔符,並且所有的字首碼的解碼方式/結果是唯一的。

字首碼的單詞查詢樹

表示字首碼的一種簡便方法就是使用單詞查詢樹。

單詞查詢樹的葉子節點儲存需要編碼的字元,左連結表示 0,右連結表示 1,
每個字元的編碼就是從根結點到該結點的路徑表示的位元字串。

哈夫曼編碼演算法能夠為任意的字串構造一棵能夠將位元流最小化的單詞查詢樹。

程式碼分解

資料壓縮流程:

  • 壓縮
    • 構建哈夫曼樹
    • 構建編譯表(根據哈夫曼樹生成的字元-字元編碼對映表)
    • 輸出(哈夫曼樹、字元數量、字元編碼)位元流
  • 展開
    • 讀取哈夫曼樹、字元數量
    • 讀取剩下的位元流,根據位元流重複遍歷哈夫曼樹得到每個字元

完整程式碼示例

單詞查詢樹的結點:

/**
 * 哈夫曼樹結點(單詞查詢樹)
 * */
public class Node implements Comparable<Node> {
    private char c; // 葉子結點儲存被編碼的字元
    private int freq; // 字元在位元流出現的頻率
    private Node left;
    private Node right;

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

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

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

使用字首碼(哈夫曼樹)展開:從位元流中讀取 01,0 向左遍歷單詞樹,1 向右遍歷單詞樹,遇到葉子結點輸出字元並回到根結點。

// 解碼過程
public static void expand(BinaryIn binaryIn, BinaryOut binaryOut) {
    Huffman.binaryIn = binaryIn;
    Huffman.binaryOut = binaryOut;
    // 讀取哈夫曼樹
    Node root = readTire();
    // 讀取字元總數
    int N = binaryIn.readInt();
    // 讀取位元流並根據哈夫曼樹進行解碼
    for (int i = 0; i < N; i++) {
        Node x = root;
        // 根據位元流遍歷哈夫曼樹直到葉子結點
        while (!x.isLeaf()) {
            if (binaryIn.readBoolean()) {
                x = x.right();
            } else {
                x = x.left();
            }
        }
        // 輸出葉子結點儲存的字元
        Huffman.binaryOut.write(x.c());
    }
    Huffman.binaryOut.close();
}

使用字首碼(哈夫曼樹)壓縮:

// 編碼過程
public static void compress(BinaryIn binaryIn, BinaryOut binaryOut) {
    Huffman.binaryIn = binaryIn;
    Huffman.binaryOut = binaryOut;
    // 讀取字串
    String s = binaryIn.readString();
    char[] input = s.toCharArray();
    // 統計每個字元出現的頻率
    int[] freq = new int[R];
    for (int i = 0; i < input.length; i++) {
        freq[input[i]]++;
    }
    // 根據字元頻率構造哈夫曼樹
    Node root = buildTire(freq);
    // 構造編譯表(ASCII 字元 -> 位元字串)
    String[] table = buildTable(root);
    // 輸出哈夫曼樹、字元數量和每個字元的字元編碼
    writeTire(root);
    binaryOut.write(input.length);
    for (int i = 0; i < input.length; i++) {
        String code = table[input[i]]; // 從編譯表中查出字元對應的字元編碼
        for (int j = 0; j < code.length(); j++) {
            if (code.charAt(j) == '1') {
                binaryOut.write(true);
            } else {
                binaryOut.write(false);
            }
        }
    }
    binaryOut.close();
}

根據哈夫曼樹構造編譯表:

// 根據哈夫曼樹構造編譯表(字元 -> 位元字串)
private static String[] buildTable(Node root) {
    String[] table = new String[R];
    buildTable(table, root,  "");
    return table;
}

private static void buildTable(String[] table, Node node, String code) {
    if (node.isLeaf()) {
        table[node.c()] = code;
        return;
    }
    buildTable(table, node.left(), code + "0");
    buildTable(table, node.right(), code + "1");
}

因為字串只使用 ASCII 字元,所以可以使用陣列儲存字元到位元字串(字元編碼)的對映

構建哈夫曼樹:

// 使用哈夫曼編碼演算法構造單詞查詢樹
private static Node buildTire(int[] freq) {
    // 根據字元頻率將所有字元放到優先佇列
    MinPQ<Node> pq = new MinPQ<>();
    for (int i = 0; i < freq.length; i++) {
        if (freq[i] > 0) {
            pq.add(new Node((char)i, freq[i], null, null));
        }
    }

    // 從出現頻率較低的字元開始構建哈夫曼樹,父結點的頻率是子結點的頻率之和
    while (pq.size() > 1) {
        Node x = pq.remove();
        Node y = pq.remove();
        Node parent = new Node('\0', x.freq() + y.freq(), x, y);
        pq.add(parent);
    }
    return pq.remove();
}

將哈夫曼樹寫入位元流:

private static void writeTire(Node node) {
    if (node.isLeaf()) {
        binaryOut.write(true);
        binaryOut.write(node.c());
        return;
    }
    binaryOut.write(false);
    writeTire(node.left());
    writeTire(node.right());
}

從位元流中讀取哈夫曼樹:

private static Node readTire() {
    if (binaryIn.readBoolean()) {
        return new Node(binaryIn.readChar(), 0, null, null);
    }
    return new Node('\0', 0, readTire(), readTire());
}