簡介
哈夫曼樹是一種帶權路徑長度最短的二叉樹,也稱為最優二叉樹。
定義:給定 n 個權值作為 n 個葉子節點,構造一顆二叉樹,若樹的帶權路徑長度達到最小,則這棵樹稱為哈夫曼樹。
路徑的長度:從樹中的一個結點到另外一個結點之間的分支構成兩個結點的路徑,路徑上的分支數目稱為路徑長度。
樹的路徑長度:從樹根到每個結點的路徑長度之和,所以我們說完全二叉樹就是這種路徑長度最短的二叉樹。
樹的帶權路徑長度:如果在樹的每個葉子節點上賦一個權值,那么熟的帶權路徑長度就等于根節點到所有葉子節點的路徑長度與葉子節點權值乘積的總和。
如何判斷一棵樹是否是最優二叉樹?
帶權長度分別為:
WPL1:7*2+5*2+2*2+4*2=36
WPL2:7*3+5*3+2*1+4*2=46
WPL3:7*1+5*2+2*3+4*3=35
第三棵樹的帶權路徑最短。所以第三棵就是我們要找的 最優二叉樹(哈夫曼樹)。
哈夫曼樹的構建
依次取權重最小的節點放在樹的底部,將最小的兩個連接構成新的節點(新節點的權值是兩個最小節點權值之和),然后把新節點放回需要構成的樹中繼續進行排序,構成哈夫曼樹,要存放的所有信息都在葉子節點上。
a | b | c | d | e |
---|---|---|---|---|
45 | 42 | 60 | 100 | 75 |
上表中的字母分別帶有權重,將其構成哈弗曼樹。
- 先對表中數據進行排序
d | e | c | a | b |
---|---|---|---|---|
100 | 75 | 60 | 54 | 42 |
取出隊列中的兩個權重最小的節點作為樹的底部,按照左邊的子樹永遠小于右邊的原則進行構造。并將新生成的節點放在隊列中進行重新排序。a=42,b=45,新節點為 87。
d | e | c | |
---|---|---|---|
100 | 87 | 75 | 60 |
然后將 87 加入隊列,形成新的隊列,并進行排序。
d | e | c | |
---|---|---|---|
100 | 87 | 75 | 60 |
再按照之前的方法將隊列中最小的兩個樹作為樹的底部,形成一個新節點。c=60,e=75,新節點為 135。
依次重復以上步驟就可以畫出該哈夫曼樹了。
d | ||
---|---|---|
135 | 100 | 87 |
87 和 d = 100 構成新節點 187。
322 | 135 |
Java 編碼
如上就是構造哈夫曼的過程。接下來用 java 實現上面我畫的那棵哈夫曼樹。
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
public class HuffmanTreeDemo {
public static void main(String[] args) {
List<Node<String>> nodes = new ArrayList<>();
nodes.add(new Node<String>("a", 45));
nodes.add(new Node<String>("b", 42));
nodes.add(new Node<String>("c", 60));
nodes.add(new Node<String>("d", 100));
nodes.add(new Node<String>("e", 75));
Node<String> root = HuffmanTree.createTree(nodes);
HuffmanTree.levelDisplay(root);
}
static class Node<T> implements Comparable<Node<T>> {
public T data;
public int weight;
public Node<T> left;
public Node<T> right;
public Node(T data, int weight) {
super();
this.data = data;
this.weight = weight;
}
@Override
public String toString() {
return "data:" + this.data + ",weight:" + this.weight + "; ";
}
@Override
public int compareTo(Node<T> o) {
if (o.weight > this.weight) {
return 1;
} else if (o.weight < this.weight) {
return -1;
}
return 0;
}
}
static class HuffmanTree<T> {
//參數是多個節點組成的隊列
public static <T> Node<T> createTree(List<Node<T>> nodes) {
while (nodes.size() > 1) {// 只剩一個節點時,退出循環
Collections.sort(nodes);
// 使用sort方法對nodes進行排序,CompareTo方法實現的是降序序列
Node<T> left = nodes.get(nodes.size() - 1);// 取兩個最小的節點
Node<T> right = nodes.get(nodes.size() - 2);
// 生成新節點,新節點的權重 = 兩個權重最小的節點的和
Node<T> parent = new Node<T>(null, left.weight + right.weight);
parent.left = left;
parent.right = right;
nodes.remove(left);
nodes.remove(right);
nodes.add(parent);
}
return nodes.get(0);
}
public static void levelDisplay(Node root) {
List<Node> list = new ArrayList<>();
Queue<Node> queue = new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()) {
Node node = queue.poll();
System.out.println(node.toString());
if (node.left != null) {
queue.add(node.left);
}
if (node.right != null) {
queue.add(node.right);
}
}
}
}
}
運行結果
data:null,weight:322;
data:null,weight:135;
data:null,weight:187;
data:c,weight:60;
data:e,weight:75;
data:null,weight:87;
data:d,weight:100;
data:b,weight:42;
data:a,weight:45;
哈夫曼編碼
哈夫曼編碼 (Huffman Coding) 是一種編碼方法,哈夫曼編碼是可變字長編碼(VLC)的一種。
哈夫曼編碼使用變長編碼表對源符號(如文件中的一個字母)進行編碼,其中變長編碼表是通過一種評估來源符號出現機率的方法得到的,出現機率高的字母使用較短的編碼,反之出現機率低的則使用較長的編碼,這便使編碼之后的字符串的平均長度、期望值降低,從而達到無損壓縮數據的目的。
哈夫曼編碼的具體步驟如下:
1)將信源符號的概率按減小的順序排隊。
2)把兩個最小的概率相加,并繼續這一步驟,始終將較高的概率分支放在右邊,直到最后變成概率 1。
3)畫出由概率 1 處到每個信源符號的路徑,順序記下沿路徑的 0 和 1,所得就是該符號的哈夫曼碼字。
4)將每對組合的左邊一個指定為 0,右邊一個指定為 1(或相反)。
根據哈夫曼樹可以解決報文編碼的問題。假設需要把一個字符串,如 “abcdabcaba” 進行編碼,將它轉換為唯一的二進制碼,但是要求轉換出來的二進制碼的長度最小。
假設每個字符在字符串出現頻率為 w,其編碼長度為 L,編碼字符 n 個,則編碼后二進制的總長度為 W1L1+W2L2+…+WnLn,這恰好是哈夫曼樹的處理原則。因此可以采用哈夫曼的構造原理進行二進制編碼,從而使得電文長度最短。
對于 “abcdabcaba”,共有 a、b、c、d 四個字符,出現次數分別為 4、3、2、1,相當于它們的權值,將 a、b、c、d 以出現次數為權值構造哈夫曼樹,得到下左圖的結果。
從哈夫曼樹根節點開始,對左子樹分配代碼 “0”,對右子樹分配 “1”,一直到達葉子節點。然后,將從樹根沿著每條路徑到達葉子節點的代碼排列起來,便得到每個葉子節點的哈夫曼編碼,如下右圖。
從圖中可以看出,a、b、c、d 對應的編碼分別為 0、10、110、111,然后將字符串 “abcdabcaba” 轉換為對應的二進制碼就是0101101110101100100,長度僅為 19。這也就是最短二進制編碼,也稱為哈夫曼編碼。