赫夫曼樹JAVA實現及分析


一,介紹

1)構造赫夫曼樹的算法是一個貪心算法,貪心的地方在於:總是選取當前頻率(權值)最低的兩個結點來進行合並,構造新結點。

2)使用最小堆來選取頻率最小的節點,有助於提高算法效率,因為要選頻率最低的,要么用排序,要么用堆。用堆的話,出堆的復雜度為O(logN),而向堆中插入一個元素的平均時間復雜度為O(1),在構建赫夫曼樹的過程中,新生成的結點需要插入到原來的隊列中,故用堆來維持這種順序比排序算法要高效地多。

 

二,赫夫曼算法分析

①用到的數據結構分析

首先需要構造一棵赫夫曼樹,因此需要二叉鏈表這種數據結構(類似於二叉樹);其次,假設樹中各個結點出現的頻率保存在一維數組中,初始時,根據該數組構造出每個結點。

算法每次從選取兩個頻率最小的結點,構造出一個新結點,新結點的頻率為這個結點的頻率之和。那么如何選取頻率最小的那兩個結點呢?

一種方式是先將結點的頻率排序,另一種方式是使用優先級隊列(比如最小堆),這里我們使用優先級隊列。

結點之間要能夠比較(比較誰出現的頻率小啊),故結點類需要實現Comparable接口,結點類(內部類)的定義如下:

 1 private class BinaryNode implements Comparable<BinaryNode>{
 2         int frequency;//出現的頻率
 3         BinaryNode left;
 4         BinaryNode right;
 5         BinaryNode parent;
 6         
 7         public BinaryNode(int frequency, BinaryNode left, BinaryNode right, BinaryNode parent) {
 8             this.frequency = frequency;
 9             this.left = left;
10             this.right = right;
11             this.parent = parent;
12         }
13 
14         @Override
15         public int compareTo(BinaryNode o) {
16             return frequency - o.frequency;
17         }
18     }

注意:這里需要一個parent指針。因為,在對各個結點進行編碼的時候,需要根據兒子結點,向上遍歷查找父親結點。

 

對於赫夫曼樹而言,需要知道樹的指針,同時,我們還額外定義了一個屬性,記錄樹中的結點個數

public class HuffmanCode{
    
    private BinaryNode root;//root of huffman tree
    private int nodes;//number of total nodes in huffman tree
    
    private class BinaryNode implements Comparable<BinaryNode>{
            
        //........

 

②構造赫夫曼樹的算法具體實現步驟

1)根據各個結點出現的頻率來構造結點。初始時,根據每個頻率構造一棵單結點的樹

2)將結點放到優先級隊列(最小堆)中保存起來,這樣易於每次選取當前兩個最小頻率的結點

算法正式開始:

3)從優先級隊列中彈出兩個結點,再創建一個新的結點作為這兩個結點的父親,新結點的頻率為這兩個結點的頻率之和,並把新結點插入到優先級隊列中

4)重復步驟 3) 直到優先級隊列中只剩下一個結點為止

最后剩下的這一個結點,就是已經構造好的赫夫曼樹的根結點

注意,第 1) 步中構造的為赫夫曼樹的葉子結點,而在第 3) 步中構造的結點全是非葉子結點。赫夫曼樹中只有兩個類型的結點,一種是葉子結點,另一種是度為2的非葉子結點

赫夫曼樹中總結點的個數為:葉子結點的個數乘以2 再減去1

1) 和 2) 步驟的實現代碼如下:

 1         /**
 2          * 根據各個結點的權值構造 N 棵單根節點的樹
 3          * @param frequency 
 4          * @return 
 5          */
 6         public List<BinaryNode> make_set(Integer[] frequency){
 7             List<BinaryNode> nodeList = new ArrayList<HuffmanCode.BinaryNode>(frequency.length);
 8             for (Integer i : frequency) {
 9                 nodeList.add(new BinaryNode(i, null, null, null));
10             }
11             nodes = frequency.length<<1 -1;//huffman 樹中結點個數等於葉子結點個數乘以2減去1
12             return nodeList;
13         }

 

3) 和 4) 步驟的實現代碼如下:

 1         /**
 2          * 
 3          * @param roots initial root of each tree
 4          * @return root of huffman tree
 5          */
 6         public BinaryNode buildHuffmanTree(List<BinaryNode> roots){
 7             if(roots.size() == 1)//一共只有一個結點
 8                 return roots.remove(0);
 9             PriorityQueue<BinaryNode> pq = new PriorityQueue<BinaryNode>(roots);
10             while(pq.size() != 1){
11                 BinaryNode left = pq.remove();
12                 BinaryNode right = pq.remove();
13                 BinaryNode parent = new BinaryNode(left.frequency+right.frequency, left, right,null);
14                 left.parent = parent;
15                 right.parent = parent;
16                 pq.add(parent);
17             }
18             return (root = pq.remove());//最后剩下的這個結點就是構造好的赫夫曼樹的樹根
19         }

 

三,赫夫曼編碼

①如何編碼?

編碼的實現思路:

從赫夫曼樹的葉子結點開始,依次沿着父結點遍歷直到樹根,如果該葉子結點是其父親的左孩子,則編碼為0,否則編碼為1

對所有的葉子結點執行上面操作,就可以把各個結點編碼了。

編碼的思路類似於求解赫夫曼樹中所有葉子結點的頻率之和,也就是整個赫夫曼樹的代價。求解赫夫曼樹的代價的代碼如下:

 1         /**
 2          * 
 3          * @param root huffman樹的根結點
 4          * @param nodeList huffman樹中的所有葉子結點列表
 5          * @return 
 6          */
 7         public int huffman_cost(List<BinaryNode> nodeList){
 8             int cost = 0;
 9             int level;
10             BinaryNode currentNode;
11             for (BinaryNode binaryNode : nodeList) {
12                 level = 0;
13                 currentNode = binaryNode;
14                 while(currentNode != root){
15                     currentNode = currentNode.parent;
16                     level++;
17                 }
18                 cost += level*binaryNode.frequency;
19             }
20             return cost;
21         }

 

②如何解碼?

解碼就是根據0 ,1組成的'字符串' 去查找結點。步驟是:對於每個葉子結點,都從赫夫曼的根結點開始,取出每一位,如果是0,往左走;如果是1,往右走。直到遇到一個葉子結點, 此時取出的 0,1 位(二進制位)就是該結點的 編碼了。

 1     public Map<BinaryNode, String> huffmanDecoding(String encodeString) {
 2         BinaryNode currentNode = root;
 3         //存儲每個葉子結點對應的二進制編碼
 4         Map<BinaryNode, String> node_Code = new HashMap<HuffmanCode.BinaryNode, String>();
 5         StringBuilder sb = new StringBuilder();//臨時保存每個結點的二進制編碼
 6         for (int i = 0; i < encodeString.length(); i++) {
 7             
 8             char codeChar = encodeString.charAt(i);
 9             sb.append(codeChar);
10             if (codeChar == '0')
11                 currentNode = currentNode.left;
12             else//codeChar=='1'
13                 currentNode = currentNode.right;
14             if (currentNode.left == null && currentNode.right == null)// 說明是葉子結點
15             {
16                 node_Code.put(currentNode, sb.toString());
17                 sb.delete(0, sb.length());//清空當前結點,為存儲下一個結點的二進制編碼做准備
18                 currentNode = root;//下一個葉子結點的解碼,又從根開始
19             }
20         }
21         return node_Code;
22     }

第18行,當解碼完一個葉子結點后,又從根結點開始解碼下一個葉子結點。

 

四,赫夫曼編碼的應用

①文件壓縮

假設有8個字符如下:a,e,i,s,t,空格(space),換行(newline)。'a'出現的頻率為10,'e'出現的頻率為15……

由於有8個字符,故需要3bit才能完成表示這8個字符(2^3=8),比如 000 表示 'a';101 表示 空格字符(space)....

由於 'a' 出現了10次,故一共需要 3*10=30bit來存儲所有的'a'

 

經過計算,一共需要174個bit來存儲上面的字符。

而若使用赫夫曼編碼,則只需要146個bit來存儲上面的編碼,原因是:對於那些出現頻率高的字符,赫夫曼編碼使用較短的位來存儲,而對於那些出現頻率低的字符,可能需要較長的位來存儲。

程序運行后得到下面的結果:

可以看出,只出現了三次的 's' 字符,它的赫夫曼編碼為11011,長度為5bit。而出現了15次的字符 'e' 的赫夫曼編碼為 10,長度為2bit

 

②最優二叉查找樹

 假設有這樣一種情況,某些東西經常出現,比如英語中的:  is 、a、hello、you、.....這樣的單詞經常看到,而有些單詞很冷門。

我們把那些經常需要查詢的單詞(用到的單詞)放到赫夫曼樹的頂部(即給它們以較短的赫夫曼編碼),那在查找它們時,只需要經過少量的幾次比較就可以找到了。這就是優化了的二叉查找樹。

即把查找頻率高的單詞放到離樹根近的地方,這樣就不需要每次都查找到葉子結點后,才能找到要想的單詞。

 

五,完整代碼

  1 import java.util.ArrayList;
  2 import java.util.HashMap;
  3 import java.util.List;
  4 import java.util.Map;
  5 import java.util.PriorityQueue;
  6 import java.util.Set;
  7 
  8 public class HuffmanCode {
  9 
 10     private BinaryNode root;// root of huffman tree
 11     private int nodes;// number of total nodes in huffman tree
 12 
 13     private class BinaryNode implements Comparable<BinaryNode> {
 14         int frequency;// 出現的頻率
 15         BinaryNode left;
 16         BinaryNode right;
 17         BinaryNode parent;
 18 
 19         public BinaryNode(int frequency, BinaryNode left, BinaryNode right,
 20                 BinaryNode parent) {
 21             this.frequency = frequency;
 22             this.left = left;
 23             this.right = right;
 24             this.parent = parent;
 25         }
 26 
 27         @Override
 28         public int compareTo(BinaryNode o) {
 29             return frequency - o.frequency;
 30         }
 31 
 32         public boolean isLeftChild() {
 33             return parent != null && parent.left == this;
 34         }
 35 
 36         public boolean isRightChild() {
 37             return parent != null && parent.right == this;
 38         }
 39     }
 40 
 41     /**
 42      * 
 43      * @param roots
 44      *            initial root of each tree
 45      * @return root of huffman tree
 46      */
 47     public BinaryNode buildHuffmanTree(List<BinaryNode> roots) {
 48         if (roots.size() == 1)// 只有一個結點
 49             return roots.remove(0);
 50         PriorityQueue<BinaryNode> pq = new PriorityQueue<BinaryNode>(roots);//優先級隊列保存所有葉子結點
 51         while (pq.size() != 1) {
 52             BinaryNode left = pq.remove();//頻率最小的先出隊列
 53             BinaryNode right = pq.remove();
 54             BinaryNode parent = new BinaryNode(
 55                     left.frequency + right.frequency, left, right, null);//構造父結點
 56             left.parent = parent;
 57             right.parent = parent;
 58             pq.add(parent);//新構造好的根結點插入到優先級隊列中
 59         }
 60         return (root = pq.remove());
 61     }
 62 
 63     /**
 64      * 根據各個結點的權值構造 N 棵單根節點的樹
 65      * 
 66      * @param frequency
 67      * @return
 68      */
 69     public List<BinaryNode> make_set(Integer[] frequency) {
 70         List<BinaryNode> nodeList = new ArrayList<HuffmanCode.BinaryNode>(
 71                 frequency.length);
 72         for (Integer i : frequency) {
 73             nodeList.add(new BinaryNode(i, null, null, null));
 74         }
 75         nodes = frequency.length << 1 - 1;// huffman 樹中結點個數等於葉子結點個數乘以2減去1
 76         return nodeList;
 77     }
 78 
 79     /**
 80      * 
 81      * @param root
 82      *            huffman樹的根結點
 83      * @param nodeList
 84      *            huffman樹中的所有葉子結點列表
 85      * @return
 86      */
 87     public int huffman_cost(List<BinaryNode> nodeList) {
 88         int cost = 0;
 89         int level;
 90         BinaryNode currentNode;
 91         for (BinaryNode binaryNode : nodeList) {
 92             level = 0;
 93             currentNode = binaryNode;
 94             while (currentNode != root) {
 95                 currentNode = currentNode.parent;
 96                 level++;
 97             }
 98             cost += level * binaryNode.frequency;
 99         }
100         return cost;
101     }
102 
103     public String huffmanEncoding(List<BinaryNode> nodeList) {
104         StringBuilder sb = new StringBuilder();
105         BinaryNode currentNode;
106         for (BinaryNode binaryNode : nodeList) {
107             currentNode = binaryNode;
108             while (currentNode != root) {
109                 if (currentNode.isLeftChild())
110                     sb.append("0");// 左孩子編碼為0
111                 else if (currentNode.isRightChild())
112                     sb.append("1");// 右孩子編碼為1
113                 currentNode = currentNode.parent;
114             }
115         }
116         return sb.toString();
117     }
118 
119     public Map<BinaryNode, String> huffmanDecoding(String encodeString) {
120         BinaryNode currentNode = root;
121         //存儲每個葉子結點對應的二進制編碼
122         Map<BinaryNode, String> node_Code = new HashMap<HuffmanCode.BinaryNode, String>();
123         StringBuilder sb = new StringBuilder();//臨時保存每個結點的二進制編碼
124         for (int i = 0; i < encodeString.length(); i++) {
125             
126             char codeChar = encodeString.charAt(i);
127             sb.append(codeChar);
128             if (codeChar == '0')
129                 currentNode = currentNode.left;
130             else
131                 currentNode = currentNode.right;
132             if (currentNode.left == null && currentNode.right == null)// 說明是葉子結點
133             {
134                 node_Code.put(currentNode, sb.toString());
135                 sb.delete(0, sb.length());//清空當前結點,為存儲下一個結點的二進制編碼做准備
136                 currentNode = root;//下一個葉子結點的解碼,又從根開始
137             }
138         }
139         return node_Code;
140     }
141 
142     // for test purpose
143     public static void main(String[] args) {
144         Integer[] frequency = { 10, 15, 12, 3, 4, 13, 1 };//各個結點的初始頻率
145         HuffmanCode hc = new HuffmanCode();
146         List<BinaryNode> nodeList = hc.make_set(frequency);//構造各個單節點樹
147         hc.buildHuffmanTree(nodeList);//構建huffman tree
148         int totalCost = hc.huffman_cost(nodeList);//計算huffman tree的代價
149         System.out.println(totalCost);
150         String encodeStr = hc.huffmanEncoding(nodeList);//將各個葉子結點進行huffman 編碼
151         System.out.println("編碼后的字符串" + encodeStr);
152         
153         //根據編碼字符串解碼
154         Map<BinaryNode, String> decodeMap = hc.huffmanDecoding(encodeStr);
155         Set<Map.Entry<BinaryNode, String>> entrys = decodeMap.entrySet();
156         for (Map.Entry<BinaryNode, String> entry : entrys) {
157             BinaryNode node = entry.getKey();
158             String code = entry.getValue();
159             System.out.println("Node's frequency=" + node.frequency + " : " + code);
160         }
161     }
162 }

 

六,參考資料

Huffman編碼算法之Java實現

《數據結構與算法分析》Mark Allen Wiess著


免責聲明!

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



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