哈夫曼樹及哈夫曼編碼
-
哈夫曼樹是判定過程最優的決策樹,又稱最優二叉樹。
-
哈夫曼樹的每個結點有權值,一個結點的權值實際上就是這個結點子樹在整個樹中所占的比例,通常指字符對應的二進制編碼出現的概率。權值大的結點距離根結點近。
-
樹的帶權路徑長度(WPL):如果樹中每個葉子上都帶有一個權值,則把樹中所有葉子的帶權路徑長度之和稱為樹的帶權路徑長度。哈弗曼樹就是帶權路徑長度最小的二叉樹。設某二叉樹有n個帶權值的葉子結點,則該二叉樹的帶權路徑長度記為(Wk為第k個葉子結點的權值;Lk為該結點的路徑長度):
-
哈弗曼樹是自底向上構建的,二叉樹的左子樹編碼為0,右子樹編碼為1,然后從根到每個葉結點依次寫出葉結點的編碼,即哈夫曼編碼。
-
哈夫曼編碼是已知的最佳無損壓縮算法,並且滿足前綴碼的性質,可以隨時解碼。
-
具體過程:
- 給定字符集設為S={a,b,c,d,e,f,g,h},計算各個字符出現的概率,即權值。
- 將字符轉換為樹結點並根據權值大小將字符由小到大排序
- 依次將權值最小的兩個字符的權值相加,得到新的權值,為那兩個字符的父結點,刪除那兩個字符,並將形成的樹添加到排序列表中,父結點的權值按大小排序,在序列中找到合適的位置,然后再將最小的兩個字符進行以上操作...
- 最終只剩一個結點,並形成了一棵樹,即哈弗曼樹。
-
編碼:在哈夫曼樹中規定左分支表示符號0,右分支表示符號1。對於每個葉子結點,從根結點到此葉子結點形成的編碼就是這個結點表示字符的哈夫曼編碼。
-
解碼:譯碼過程是分解、識別各個字符,還原數據的過程。從字符串頭開始掃描到尾,依次去匹配。
-
圖示過程
- 首先是帶權值的數
- 第二步將最小的1和2組合相加形成父結點,並刪除1和2
- 依次進行
- 這是只剩下一個結點,同時也形成了一棵樹,這就是哈夫曼樹。
- 首先是帶權值的數
代碼實現
-
首先構造結點類,每個結點需要有哈夫曼編碼,字符本身,權值,以及左結點和右結點。
public class TreeNode { public String code = "";// 節點的哈夫曼編碼 public String data = "";// 節點的字符 public int count;// 節點的權值 public TreeNode lChild;//左結點 public TreeNode rChild;//右結點 public TreeNode(String data, int count) { this.data = data; this.count = count; } public TreeNode(int count, TreeNode lChild, TreeNode rChild) { this.count = count; this.lChild = lChild; this.rChild = rChild; } }
-
計算每個字符的權重,flag用來判定取出的字符串是否存在,若不存在為true,如果存在那么就在出現次數統計上加一,即權重加一,如果沒有出現過,就將它加入到存儲字符的鏈表中。
// 統計出現的字符及出現次數 private void getCharNum(String str) { for (int i = 0; i < str.length(); i++) { char ch = str.charAt(i); // 從字符串中取出字符 flag = true; for (int j = 0; j < charList.size(); j++) { CharData data = charList.get(j); // 遍歷字符鏈表,若有相同字符則將個數加1 if(ch == data.chardata){ data.num++; flag = false; totalcount++; break; } } // 字符對象鏈表中沒有相同字符則將其加到鏈表中 if(flag){ charList.add(new CharData(ch)); totalcount++; } } }
-
將字符鏈表中的字符構建為結點,利用循環將字符鏈表中的字符和其權值創建為TreeNode結點,並加入到結點鏈表中。
//將字符創建為結點 private void creatNodes() { for (int i = 0; i < charList.size(); i++) { String data = charList.get(i).chardata + ""; int count = charList.get(i).num;//權值 TreeNode node = new TreeNode(data, count); // 創建節點對象 NodeList.add(node); // 加入到節點鏈表 } }
-
構建哈弗曼樹,根據上文提到的哈夫曼編碼的規則,將最小的兩個節點分別設置為左右子結點,左邊編碼為0,右邊為1,然后依據父結點創建新的TreeNode結點,將新的結點放到鏈表首位並重新排序。一次循環進行,直到結點鏈表中的節點數不大於1,這時樹就形成了。
//構建哈夫曼樹 private void creatTree() { // 當節點數目大於一時,將權值最小的兩個節點生成一棵新樹,父結點為兩者之和,並刪除兩個結點將新樹放到列表中 while (NodeList.size() > 1) { TreeNode left = NodeList.poll(); TreeNode right = NodeList.poll(); // 設置各個結點的哈夫曼編碼 left.code = "0"; right.code = "1"; setCode(left); setCode(right); int parentWeight = left.count + right.count;// 父節點權值等於子節點權值之和 TreeNode parent = new TreeNode(parentWeight, left, right); NodeList.addFirst(parent); // 暫時將父節點置於首位 Sort(NodeList); // 重新排序將父結點放到合適位置 } }
-
在構建哈夫曼樹的過程中每次將最小的兩個相加構造新結點時,需要對新的鏈表進行排序,所以用到升序排序法,將鏈表從左開始依次與其右邊的所有字符比較權值,將權值小的字符放到左邊,這樣就形成升序鏈表。
//升序排序 private void Sort(LinkedList<TreeNode> nodelist) { for (int i = 0; i < nodelist.size() - 1; i++) { for (int j = i + 1; j < nodelist.size(); j++) { TreeNode temp; if (nodelist.get(i).count > nodelist.get(j).count) { temp = nodelist.get(i); nodelist.set(i, nodelist.get(j)); nodelist.set(j, temp); } } } }
-
設置每個結點的哈夫曼編碼,從根結點開始,分別對做孩子和右孩子的編碼添加0/1,左結點為0,右結點為1。這樣每個葉子節點都有獨一無二的01編碼。
//設置結點的哈夫曼編碼 private void setCode(TreeNode root) { if (root.lChild != null) { root.lChild.code = root.code + "0"; setCode(root.lChild); } if (root.rChild != null) { root.rChild.code = root.code + "1"; setCode(root.rChild); } }
-
利用以上方法,根據哈夫曼編碼的規則構建哈夫曼樹
//構建哈夫曼樹 public void creatHuffmanTree(String str) { this.str = str; NodeList = new LinkedList<TreeNode>(); charList = new LinkedList<CharData>(); // 統計字符串中字符以及字符的出現次數 getCharNum(str); // 創建節點 creatNodes(); // 對節點升序排序 Sort(NodeList); // 將權值最小的兩個節點相加生成一個新的父節點並刪除權值最小的兩個節點,將父節點存放到列表中,形成樹 creatTree(); // 將最后的一個節點賦給根節點 root = NodeList.get(0); }
-
遍歷節點輸出字符的編碼,通過判斷左右孩子是否為空的情況,找到葉子結點,即字符,然后輸出其編碼和出現次數。
//遍歷結點 private void output(TreeNode node) { if (node.lChild == null && node.rChild == null) { System.out.println(node.data + " 的編碼為:" + node.code+ " 出現次數為:"+ node.count + " 出現概率為:"+ (node.count)/totalcount); } if (node.lChild != null) { output(node.lChild); } if (node.rChild != null) { output(node.rChild); } }
-
編碼,定義字符串hfmCodeStr,將葉子結點的編碼依次添加到編碼字符串中,返回hfmCodeStr字符串。
//編碼 public String creHufmCode(String str) { for (int i = 0; i < str.length(); i++) { String string = str.charAt(i) + ""; search(root, string); } return hfmCodeStr; } //找到葉子結點,添加其編碼 private void search(TreeNode root, String c) { if (root.lChild == null && root.rChild == null) { if (c.equals(root.data)) { hfmCodeStr += root.code; } } if (root.lChild != null) { search(root.lChild, c); } if (root.rChild != null) { search(root.rChild, c); } }
-
解碼,通過遍歷葉子結點找到相匹配的字符編碼,定義編碼后的字符串從第一個數字開始,用substring方法截取字符串,如果找到相匹配的葉子結點編碼,就進行解碼,當解碼失敗時,說明編碼不匹配,end向后移,當解碼成功時,first向后移,對下一段字符進行解碼,最后輸出解碼后的字符串。
//解碼 public String decode(String codeStr) { int first = 0; int end = 1; while(end <= codeStr.length()){ target = false; String s = codeStr.substring(first, end); matchCode(root, s); // 解碼 // 每解碼一個字符,first向后移 if(target){ first = end; } end++; } return result; } //匹配字符哈夫曼編碼,找到對應的字符 private void matchCode(TreeNode root, String code){ if (root.lChild == null && root.rChild == null) { if (code.equals(root.code)) { result += root.data; // 找到對應的字符,拼接到解碼字符串后 target = true; // 標志為true } } if (root.lChild != null) { matchCode(root.lChild, code); } if (root.rChild != null) { matchCode(root.rChild, code); } }
-
讀取文件中的字符串,並構造哈夫曼樹
BufferedReader a = new BufferedReader(new FileReader("wen.txt")); String data = a.readLine(); huff.creatHuffmanTree(data);// 構造樹
-
將編碼解碼后的內容寫入文件
//寫入文件 File file = new File("wen2"); Writer write = new FileWriter(file); write.write(hufmCode); write.flush(); write.write(huff.decode(hufmCode)); write.close(
-
運行結果
-
英文文件
-
將編碼解碼后的結果存入文件
碼雲鏈接
https://gitee.com/CS-IMIS-23/20172314/blob/master/src/新第十四周/Huffman.java