哈夫曼編碼


一、哈夫曼編號:

又稱霍夫曼編碼,是一種編碼方式,哈夫曼編碼是可變字長編碼(VLC)的一種。Huffman於1952年提出一種編碼方法,該方法完全依據字符出現概率來構造異字頭的平均長度最短的碼字,有時稱之為最佳編碼,一般就叫做Huffman編碼(有時也稱為霍夫曼編碼)。是一種高效的編碼方式,在信息存儲和傳輸過程中用於對信息進行壓縮。

二、計算機系統如何存儲信息

計算機不是人,它不認識中文和英文,更不認識圖片和視頻,它唯一“認識”的就是0(低電平)和1(高電平)。

因此,我們在計算機上看到的一切文字、圖像、音頻、視頻,底層都是用二進制來存儲和傳輸的。

從狹義上來講,把人類能看懂的各種信息,轉換成計算機能夠識別的二進制形式,被稱為編碼

編碼的方式可以有很多種,我們大家最熟悉的編碼方式就屬ASCII碼了。

在ASCII碼當中,把每一個字符表示成特定的8位二進制數,比如:

 

 

 顯然,ASCII碼是一種等長編碼,也就是任何字符的編碼長度都相等。

等長編碼

優點:因為每個字符對應的二進制編碼長度相等,容易設計,也很方便讀寫。

缺點:計算機的存儲空間以及網絡傳輸的寬帶是有限的,等長編碼最大的缺點就是編碼結果太長,會占用過多的資源。

為什么這么說呢?讓我們來看一個例子:

假如一段信息當中,只有A,B,C,D,E,F這6個字符,如果使用等長編碼,我們可以把每一個字符都設計成長度為3的二進制編碼:

 

 

 如此一來,給定一段信息 “ABEFCDAED”,就可以編碼成二進制的 “000 001 100 101 010 011 000 100 011”,編碼總長度是27。

 

 

 但是,這樣的編碼方式是最優的設計嗎?如果我們讓不同的字符對應不同長度的編碼,結果會怎樣呢?比如:

 

 

 如此一來,給定的信息 “ABEFCDAED”,就可以編碼成二進制的 “0 00 10 11 01 1 0 10 1”,編碼的總長度只有14。

這樣的編碼設計可一使總長度大大縮短,但是這樣設計會帶來歧義,如A的編碼是0,B的編碼是00,那么000既可能代表AB也可能代表AAA,所有這種不定長的代碼是不能隨意設計的。

三、哈夫曼編碼設計

哈夫曼編碼(Huffman Coding),同樣是由麻省理工學院的哈夫曼博所發明,這種編碼方式實現了兩個重要目標:

1.任何一個字符編碼,都不是其他字符編碼的前綴。

2.信息編碼的總長度最小。

哈夫曼編碼並不是 一套固定的編碼,而是根據給的信息中各個字符出現的頻次動態生成最優的編碼。

這里引入一個哈夫曼樹。

哈夫曼編碼的生成過程是什么樣子呢?讓我們看看下面的例子:

假如一段信息里只有A,B,C,D,E,F這6個字符,他們出現的次數依次是2次,3次,7次,9次,18次,25次,如何設計對應的編碼呢? 

我們不妨把這6個字符當做6個葉子結點,把字符出現次數當做結點的權重,以此來生成一顆哈夫曼樹:

第一步:構建森林

 

 

 

在上圖當中,右側是葉子結點的森林,左側是一個輔助隊列,按照權值從小到大存儲了所有葉子結點。至於輔助隊列的作用,我們后續將會看到。

第二步:選擇當前權值最小的兩個結點,生成新的父結點

借助輔助隊列,我們可以找到權值最小的結點2和3,並根據這兩個結點生成一個新的父結點,父節點的權值是這兩個結點權值之和:

 

 

 

第三步:從隊列中移除上一步選擇的兩個最小結點,把新的父節點加入隊列

也就是從隊列中刪除2和3,插入5,並且仍然保持隊列的升序:

 

 

 

第四步:選擇當前權值最小的兩個結點,生成新的父結點

這是對第二步的重復操作。當前隊列中權值最小的結點是5和7,生成新的父結點權值是5+7=12:

 

 

 

第五步:從隊列中移除上一步選擇的兩個最小結點,把新的父節點加入隊列

這是對第三步的重復操作,也就是從隊列中刪除5和7,插入12,並且仍然保持隊列的升序:

第六步:選擇當前權值最小的兩個結點,生成新的父結點

這是對第二步的重復操作。當前隊列中權值最小的結點是9和12,生成新的父結點權值是9+12=21:

 

 

 

第七步:從隊列中移除上一步選擇的兩個最小結點,把新的父節點加入隊列

這是對第三步的重復操作,也就是從隊列中刪除9和12,插入21,並且仍然保持隊列的升序:

第八步:選擇當前權值最小的兩個結點,生成新的父結點

這是對第二步的重復操作。當前隊列中權值最小的結點是18和21,生成新的父結點權值是18+21=39:

 

 

 

第九步:從隊列中移除上一步選擇的兩個最小結點,把新的父節點加入隊列

這是對第三步的重復操作,也就是從隊列中刪除18和21,插入39,並且仍然保持隊列的升序:

第十步:選擇當前權值最小的兩個結點,生成新的父結點

這是對第二步的重復操作。當前隊列中權值最小的結點是25和39,生成新的父結點權值是25+39=64:

 

 

 最終形成的哈夫曼數:

 

 

 

這樣做的意義是什么呢?

哈夫曼樹的每一個結點包括左、右兩個分支,二進制的每一位有0、1兩種狀態,我們可以把這兩者對應起來,結點的左分支當做0,結點的右分支當做1,會產生什么樣的結果?

 

 

 

 

這樣一來,從哈夫曼樹的根結點到每一個葉子結點的路徑,都可以等價為一段二進制編碼:

 

 

 

上述過程借助哈夫曼樹所生成的二進制編碼,就是哈夫曼編碼

 

現在,我們面臨兩個關鍵的問題:

 

首先,這樣生成的編碼有沒有前綴問題帶來的歧義呢?答案是沒有歧義。

因為每一個字符對應的都是哈夫曼樹的葉子結點,從根結點到這些葉子結點的路徑並沒有包含關系,最終得到的二進制編碼自然也不會是彼此的前綴。

其次,這樣生成的編碼能保證總長度最小嗎?答案是可以保證。

哈夫曼樹的重要特性,就是所有葉子結點的(權重 X 路徑長度)之和最小。

放在信息編碼的場景下,葉子結點的權重對應字符出現的頻次,結點的路徑長度對應字符的編碼長度。

所有字符的(頻次 X 編碼長度)之和最小,自然就說明總的編碼長度最小

 

 

 對比可以看出哈夫曼總長度要比定長編碼短了超過20%

 

哈夫曼編碼代碼

    private Node root;
    private Node[] nodes;
     
    //構建哈夫曼樹
    public void createHuffmanTree(int[] weights)
    {
        //優先隊列,用於輔助構建哈夫曼樹
        Queue<Node> nodeQueue = new PriorityQueue<>();
        nodes =new Node[weights.length];
     
         
    //構建森林,初始化nodes數組
         
    for(int i=0; i<weights.length; i++){
            nodes[i]=new Node(weights[i]);
            nodeQueue.add(nodes[i]);
         
    }
     
         
    //主循環,當結點隊列只剩一個結點時結束
         
    while(nodeQueue.size()>1)
    {
             
    //從結點隊列選擇權值最小的兩個結點
             
    Node left = nodeQueue.poll();
             
    Node right = nodeQueue.poll();
             
    //創建新結點作為兩結點的父節點
            Node parent =new Node(left.weight +right.weight,left,right);
            nodeQueue.add(parent);
         
    }
        root =nodeQueue.poll();
    }
     
    //輸入字符下表,輸出對應的哈夫曼編碼
    public String convertHuffmanCode(int index)
      
    {
            return nodes[index].code;}
     
    //用遞歸的方式,填充各個結點的二進制編碼
    public void encode(Node node, String code){
        if(node ==null){
            return;
        }
        node.code =code;
        encode(node.lChild, node.code+"0");
        encode(node.rChild, node.code+"1");}
     
    public static class     Node implements Comparable<Node>{
        int weight;
        //結點對應的二進制編碼
             
        String code;
             
        Node lChild;
             
        Node rChild;
     
         
    public Node(int weight){
            this.weight = weight;
        }
     
         
    public Node(int weight, Node lChild, Node rChild) {    
        this.weight =weight;
             
        this.lChild = lChild;
}
    
    @Override 
    public int compareTo ( Node o ) {
        return new Integer ( this.weight ). compareTo ( new Integer ( o.weight )); 
    } 
}

  public static void main ( String [] args ) {
    char [] chars = { 'A' , 'B' , 'C' , 'D' , 'E' , 'F' };
    int [] weights = { 2 , 3 , 7 , 9 , 18 , 25 };
    HuffmanCode huffmanCode = new HuffmanCode ();
    huffmanCode.createHuffmanTree ( weights );
    huffmanCode.encode ( huffmanCode.root , "" );
    for (int i = 0; i < chars.length; i++) {
      System.out.println(chars[i]+":"+ huffmanCode.convertHuffmanCode(i) );

    }
  }


}

 
         

 


這段代碼中,Node類增加了一個新字段code,用於記錄結點所對應的二進制編碼。

 

當哈夫曼樹構建之后,就可以通過遞歸的方式,從根結點向下,填充每一個結點的code值。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM