一,介紹
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 }
六,參考資料
《數據結構與算法分析》Mark Allen Wiess著
