哈夫曼樹通常用於壓縮, 先看下哈夫曼樹的由來
看上面這段代碼, 結合右圖中各個分數段的比例。 現在假設一共有100個學生, 那么一共要執行多少次判斷的邏輯呢?
顯然 5 + 15*2 + 40*3 + 30*4 +10*5 = 325次
那么是否可以優化呢?----當然也是可以的, 我們如果把分數占比大的判斷往前放, 總的判斷次數就可以減小
如 if (a<80){
}else if(a<90){
}else if (a<70){
}else if (a<100){
}else{
}
那么總的判斷執行次數就變成了 40 + 30*2 + 15*3 + 10*4 + 5*5 = 210次, 當然,這是一段偽代碼, 只是體現性能上的提升
現在我們把上面兩段代碼分別轉換成樹
這里引入一個概念叫做樹的路徑
哈夫曼大叔說,從樹中一個節點到另一個節點之間的分支,構成兩個節點之間的路徑,路徑上的分支數目稱為路徑長度。
樹的路徑長度就是樹根到每一個節點的路徑長度之和
如圖, 二叉樹a的路徑長度為20, 二叉樹b的路徑長度為16
上圖中5,15,40...這些稱為節點的權值
節點的帶權路徑長度 為節點的路徑長度與該節點權值的乘積, 如二叉樹a中, 節點D的帶權路徑長度為4*30 = 120
樹的帶權路徑長度 為樹中所有葉子節點的帶權路徑之和, 如二叉樹a的帶權路徑長度為: 1*5 + 2*15 + 3*40 +4*30 +4*10 = 315
二叉樹b的帶權路徑長度為:3*5+3*15+2*40+2*30+2*10 = 220
對於一棵有n個葉子節點的二叉樹, 我們可以隨意組合出很多種, 其中帶權路徑長度最小的二叉樹稱為哈夫曼樹
那么下一步,如何構造一棵哈夫曼樹呢?
1)先把有權值的葉子節點按照權值從小到大的順序排成一個有序序列
A5 E10 B15 D30 C40
2)取最小的兩個葉子作為節點N1的子節點, 注意較小的為左兒子,新節點的權值為兩個子節點的權值之和
3)用N1替換A和E,插入有序序列中, 保持從小到大排序,即N15 B15 D30 C40
4) 重復步驟2), 將N1和B作為新節點N2的子節點,較小的為左兒子,新節點的權值為兩個子節點的權值之和
5)用N2替換N1和B,插入有序序列中, 保持從小到大排序,即N30 D30 C40
6)重復步驟2),將N2和D作為新節點N3的子節點
7)用N3替換N2和D, 插入有序序列中, 保持從小到大排序,即C40 N60
8)因為只剩最后兩個元素, 因此C40和N60成為新節點T的子節點,T即為根節點
上圖即構造了一棵哈夫曼樹,
它的帶權路徑長度為 1*40 + 2*30 + 3*15 +4*5 +4*10 = 205
小結: 構建哈夫曼樹的一般過程:
首先把葉子節點排成排成有序序列,
然后取最小的兩個構建樹葉, 用他們的和值構建他們的父節點,
把父節點放到序列中,再重復上面的步驟
接下來我們看看代碼實現:
首先定義節點內部類:
1 /** 2 * 定義節點 3 */ 4 public static class TreeNode implements Comparable<TreeNode>{ 5 //數據域 6 String data; 7 //權值 8 int balance; 9 //父節點 10 TreeNode parent; 11 //左兒子 12 TreeNode leftChild; 13 //右兒子 14 TreeNode rightChild; 15 16 //比較權值 17 @Override 18 public int compareTo(TreeNode treeNode) { 19 if(balance < treeNode.balance){ 20 return 1; 21 }else { 22 return -1; 23 } 24 } 25 26 public TreeNode(String data, int balance){ 27 this.data = data; 28 this.balance = balance; 29 this.parent = null; 30 this.leftChild = null; 31 this.rightChild = null; 32 } 33 }
然后構造方法:
1 //根節點 2 private TreeNode root; 3 4 public HuffmanTree(TreeNode root){ 5 this.root = root; 6 }
創建樹:
1 /** 2 * 構建哈夫曼樹 3 * @param list 4 * @return 5 */ 6 public static HuffmanTree createHuffmanTree(ArrayList<TreeNode> list){ 7 while(list.size() > 1){ 8 //首先對list進行排序 9 //因為我們給TreeNode實現了compareTo, 所以會按照權值來排序 10 Collections.sort(list); 11 //取出最小的兩個節點 12 TreeNode firstNode = list.get(list.size()-1); 13 TreeNode secondNode = list.get(list.size()-2); 14 //創建一個新節點, 權值為前兩個節點的權值之和 15 TreeNode newNode = new TreeNode("N", firstNode.balance + secondNode.balance); 16 newNode.leftChild = firstNode; 17 newNode.rightChild = secondNode; 18 firstNode.parent = newNode; 19 secondNode.parent = newNode; 20 //用新節點替換剛取出的最小節點 21 list.remove(firstNode); 22 list.remove(secondNode); 23 list.add(newNode); 24 } 25 //while循環執行完之后, list中只剩下最后一個節點, 這個節點就是根節點 26 return new HuffmanTree(list.get(0)); 27 }
樹的遍歷
1 /** 2 * 哈夫曼樹的遍歷 3 */ 4 public void showHuffmanTree(){ 5 //這里利用了隊列 先進先出 的特點 6 LinkedList<TreeNode> list = new LinkedList<>(); 7 //根節點入隊 8 list.offer(root); 9 while(list.size()>0){ 10 //取出來一個 11 TreeNode node = list.pop(); 12 //打印出來 13 System.out.println(node.data+"("+node.balance+")"); 14 //子節點入隊 15 if (node.leftChild != null){ 16 list.offer(node.leftChild); 17 } 18 if (node.rightChild != null){ 19 list.offer(node.rightChild); 20 } 21 } 22 }
測試代碼:
1 @Test 2 fun testHuffmanTree(){ 3 val list = arrayListOf( 4 HuffmanTree.TreeNode("A", 5), 5 HuffmanTree.TreeNode("B", 15), 6 HuffmanTree.TreeNode("C", 40), 7 HuffmanTree.TreeNode("D", 30), 8 HuffmanTree.TreeNode("E", 10) 9 ) 10 11 val tree = HuffmanTree.createHuffmanTree(list) 12 tree.showHuffmanTree() 13 }
測試結果
哈夫曼編碼:
哈夫曼編碼的目的是數據壓縮
比如ABCDEF的二進制表示如下
此時假設我們要存儲ABCDE, 則計算機存儲的是000001010011100 共15個字符
那么我們在哈夫曼樹中,將節點的左路徑權值用0替換,右路徑用1替換,則得到下圖
那么A=1000 B=101 C=0 D=11 E=1001
此時存儲ABCDE,則計算機存儲10001010111001共14個字符
好像沒什么大變化是吧?😄
這是因為我們沒有引入權值,我們知道哈夫曼樹是基於權值來構建的, 回到本文最開始的例子
假設一共100個數字, A出現5次,B出現15次,C出現40次,D出現30次, E出現10次, 那么按照正常編碼, 計算機存儲這100個ABCDE共計需要100*3 = 300個字符
但是如果用哈夫曼編碼, 則只需要存儲 4*5 + 3*15 +1*40 + 2*30 + 4*10 = 205 個字符
節約了31.6%的存儲空間