算法#19--霍夫曼壓縮(數據壓縮)

定義

我們現在來學習一種能夠大幅壓縮自然語言文件空間(以及許多其他類型文件)的數據壓縮技術。

它的主要思想是放棄文本文件的普通保存方式:不在使用7位或8位二進制數表示每一個字符,而是用較少的比特表示出現頻率高的字符,用較多的比特表示出現頻率低的字符。

簡而言之,不在用ASCII編碼表示,而是用較短的前綴碼錶示。

前綴碼

什麼是前綴碼?如果所有字符編碼都不會成爲其他字符編碼的前綴,符合這種規則的叫前綴碼。

比如,如果A的編碼爲0,R的編碼爲00,那麼A的編碼是R的前綴,這不屬於前綴碼。那麼前綴碼的例子是什麼樣的呢?如下:

所有的前綴碼的解碼方式都和它一樣,是唯一的。因此前綴碼被廣泛應用於實際生產中。注意,像7位ASCII編碼編碼這樣的定長編碼也是前綴碼。

霍夫曼單詞查找樹

表示前綴碼的一種簡便方法就是使用單詞查找樹。

任意含有M個空鏈接的單詞查找樹都沒M個字符定義了一種前綴碼方法:我們將空鏈接替換爲指向葉子結點(含有兩個空鏈接的結點)的鏈接,每個葉子結點都含有一個需要編碼的字符。這樣,沒個字符的編碼都是從根結點到該結點的路徑表示的比特字符串,其中左鏈接表示0,右鏈接表示1.

如果構造這樣一棵樹?

首先,樹的結點包含left和right,和一個字符頻率變量freq,以及字符ch。以下爲構造一顆霍夫曼單詞查找樹的過程:

  1. 將需要被編碼的字符放在葉子結點中並在每個結點中維護了一個名爲freq的實例變量來表示以它爲根結點的子樹種的所有字符出現的頻率。
  2. 創建一片由許多隻有一個結點(即葉子結點)的樹所組成的森林。每棵樹都表示輸入流的一個字符,每個結點中的freq表示它在輸入流中的出現頻率。
  3. 找到兩個頻率最小的結點,然後創建一個以二者爲子結點的新結點(新結點的頻率爲它的兩個子結點的頻率之和)。
  4. 不斷重複第3過程,最終所有的結點會被合併爲一顆單獨的單詞查找樹。

特點:

  • 樹的葉子結點包含freq和字符。
  • 頻率高的離根結點最近,頻率低的在樹的底層。
  • 根結點頻率值等於輸入中的字符數量。
  • 該樹表示的編碼壓縮比其他樹更多,是一種最優的前綴碼

壓縮

對於任意單詞查找樹,都能產生一張將樹中的字符和比特字符串(用由0和1組成的String字符串表示)相對應的編譯表。其實就是字符和它的比特字符串的符號表。在這裏我們用st[]數字表示。在構造符號表時buildCode()遞歸遍歷整棵樹,併爲每個結點維護一條從根結點到它的路徑所對應的二進制字符串(左鏈接表示0,右鏈接表示1)。到達一個葉子結點後,就將結點的編碼設爲該二進制字符串。如下圖的編譯表:

然後,壓縮就很簡單了,只需要在其中找到輸入字符所對應的編碼即可。

上圖字符串ABRACADABRA!的編碼爲:首先是0(A的編碼),然後是111(B的編碼),然後是110(R的編碼),最後得到完整編碼爲0111110010110100011111001010.

解壓

首先readTrie()將霍夫曼單詞查找樹編碼爲的比特流構造爲霍夫曼查找樹。然後讀取霍夫曼壓縮碼,根據該編碼從根結點向下移動(讀取一個比特,爲0移動到左結點,爲1移動到右結點)。當遇到葉子結點後,輸出該結點的字符並重新回到根結點。

例如壓縮ABRACADABRA!後的編碼爲:0111110010110100011111001010,其單詞查找樹的比特流爲:01010000010010100010001001000011010000110101010010101000010。

首先由樹的比特流構造霍夫曼查找樹(得如下樹),然後解碼編碼。第一個爲0,所以移動到左子結點,輸出A;回到根,然後連續三個1,即向右移動3次,輸出B;回到根,然後兩個1,一個0,即向右移動兩次,向左移動一次,輸出R。如此重複,最後得到ABRACADABRA!

實現代碼


/**
 * 霍夫曼壓縮
 * @author nicholas.tang
 *
 */
public class Huffman 
{
    public static int R = 256;
    public static final int asciiLength = 8;//ascii碼,一個字符等於8個bit
    public static String bitStreamOfTrie = "";//使用前序遍歷將霍夫曼單詞查找樹編碼爲比特流
    public static int lengthOfText = 0;//要壓縮文本的長度

    private static String next = "";//讀取霍夫曼單詞查找樹用到的next字符串,它指向下一個比特流的子字符串,用了遍歷比特流

    /**
     * 霍夫曼單詞查找樹中的結點
     * @author nicholas.tang
     *
     */
    private static class Node implements Comparable<Node>
    {
        private char ch;    //內部結點不會使用該變量
        private 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;
        }

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

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

    /**
     * 解壓
     * @param bitSteam 霍夫曼單詞查找樹編碼爲的比特流
     * @param length 文本長度
     * @param huffmanCode 霍夫曼編碼
     * @return 解壓後的文本
     */
    public static String expand(String bitSteam, int length, String huffmanCode)
    {   
        Node root = null;
        if(bitSteam == "")
        {
            return "";
        }
        else
        {
            root = readTrie(bitSteam);  
        }

        int j = 0;
        String text = "";
        for(int i = 0; i < length; i++)
        {
            Node x = root;
            while(!x.isLeaf())
            {
                if(huffmanCode.substring(j, j+1).equals("1"))
                {
                    x = x.right;
                }
                else
                {
                    x = x.left;
                }
                j++;
            }
            text +=x.ch;
        }   
        return text;
    }

    /**
     * 壓縮
     * @param s 要壓縮的文本
     * @return 壓縮後,反饋的霍夫曼編碼
     */
    public static String compress(String s)
    {
        //讀取輸入
        char[] input = s.toCharArray();

        //統計頻率
        int[] freq = new int[R];
        for(int i = 0; i < input.length; i++)
        {
            freq[input[i]]++;
        }

        //構造霍夫曼編碼樹
        Node root = buildTrie(freq);

        //遞歸地構造編譯表
        String[] st = new String[R];
        buildCode(st, root, "");

        //遞歸地打印解碼用的單詞查找樹,即比特流
        writeTrie(root);

        //打印字符總數
        lengthOfText = input.length;

        //使用霍夫曼編碼處理輸入
        String codeOfHuffman = "";
        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) == '1')
                {
                    codeOfHuffman += '1';
                }
                else
                {
                    codeOfHuffman += '0';
                }
            }
        }
        return codeOfHuffman;//返回霍夫曼編碼
    }

    /**
     * 構建霍夫曼單詞查找樹
     * @param freq 字符在文本出現的頻率
     * @return 霍夫曼單詞查找樹
     */
    private static Node buildTrie(int[] freq)
    {
        MinPQ<Node> pq = new MinPQ<Node>(R);
        for(char c = 0; c < R; c++)
        {
            if(freq[c] > 0)
            {
                pq.insert(new Node(c, freq[c], null, null));
            }
        }
        while(pq.size() > 1)
        {//合併兩顆頻率最小的樹
            Node x = pq.delMin();
            Node y = pq.delMin();
            Node parent = new Node('\0', x.freq + y.freq, x, y);
            pq.insert(parent);
        }
        return pq.delMin();
    }

    /**
     * 構造編譯表
     * @param st 編譯表
     * @param x 霍夫曼單詞查找樹中的結點
     * @param s 編譯表內容
     */
    private static void buildCode(String[] st, Node x, String s)
    {
        if(x.isLeaf())
        {
            st[x.ch] = s;
            return;
        }
        buildCode(st, x.left, s + '0');
        buildCode(st, x.right, s + '1');
    }

    /**
     * 使用前序遍歷將霍夫曼單詞查找樹編碼爲比特流
     * @param x 霍夫曼單詞查找樹
     */
    private static void writeTrie(Node x)
    {//輸出單詞查找樹的比特字符串
        if(x.isLeaf())
        {
            bitStreamOfTrie += '1';
            String temp = Integer.toBinaryString(x.ch);
            int n = asciiLength - temp.length();
            temp = repeatStrings("0", n) + temp;
            bitStreamOfTrie += temp;
            return ;
        }
        bitStreamOfTrie += '0';
        writeTrie(x.left);
        writeTrie(x.right);
    }   

    /**
     * 用比特流構造霍夫曼單詞查找樹
     * @param s 比特流
     * @return 霍夫曼單詞查找樹
     */
    private static Node readTrie(String s)
    {           
        if(s.substring(0, 1).equals("1"))
        {  
            int value = Integer.parseInt(s.substring(1, 1 + asciiLength),2);
            next = s.substring(1 + asciiLength);
            return new Node((char)value, 0, null, null);            
        }
        else
        {           
            next = s.substring(1);
            return new Node('\0', 0, readTrie(next), readTrie(next));
        }       
    }

    /**
     * 重複字符串
     * @param s 需要重複的字符串
     * @param n 重複次數
     * @return 重複後的字符串
     */
    private static String repeatStrings(String s , int n)
    {
          String temp = "";
          for(int i = 0; i < n;i++)
          {
              temp += s;
          }
          return temp;
    }

    public static void main(String[] args) 
    {
        String text = "ABRACADABRA!";
        System.out.println("Input text: " + text);

        String HuffmanCode = Huffman.compress(text);
        int bitsOfText = Huffman.lengthOfText * Huffman.asciiLength;
        String bitStream = Huffman.bitStreamOfTrie;
        double compressionRatio = 1.0 * HuffmanCode.length() / bitsOfText;

        System.out.println("Huffman Code: " + HuffmanCode);     
        System.out.println("BitStream: " + bitStream);
        System.out.println("Huffman Code length(bit): " + HuffmanCode.length());
        System.out.println("Length of text(bit): " + bitsOfText);
        System.out.println("Compression ratio: " + compressionRatio * 100 + "%");

        String expandText = Huffman.expand(bitStream, lengthOfText, HuffmanCode);
        System.out.println("Expand text: " + expandText);
    }
}

輸出:

Input text: ABRACADABRA!
Huffman Code: 0111110010110100011111001010
BitStream: 01010000010010100010001001000011010000110101010010101000010
Huffman Code length(bit): 28
Length of text(bit): 96
Compression ratio: 29.166666666666668%
Expand text: ABRACADABRA!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章