基本介紹
赫夫曼樹(Huffman tree):
- 給定 n 個 權值 作為 n 個 葉子節點,構造一顆二叉樹,若該樹的 帶權路徑長度(WPL)達到最小,稱這樣的二叉樹為 最優二叉樹,也稱為 哈夫曼樹(Huffman Tree),還有的叫 霍夫曼樹
- 赫夫曼樹是帶權路徑長度最短的樹,權值較大的節點離根節點較近
詳情請看百度百科——哈夫曼樹
重要概念
-
路徑 和 路徑長度:
在一顆樹中,從一個節點往下可以到達的孩子或孫子節點之間的通路,稱為 路徑。
通路中分支的數目稱為路徑長度。若規定根節點的層數為 1,則從根節點到第 L 層節點的路徑長度為 L-1
-
節點的權 和 帶權路徑長度
若將樹中節點賦給一個有著某種含義的數值,則這個數值稱為該節點的 權。
節點的帶權路徑長度為:從根結點到該結點之間的路徑長度與該結點的權的乘積。
-
樹的帶權路徑長度
所有葉子節點的帶權路徑長度之和,記為 WPL(weighted path length),權值越大的節點離根節點越近的二叉樹才是最優二叉樹
-
下圖
WPL
最小的就是赫夫曼樹
如上圖:
權
:元素的值路徑長度
:一個節點到另一個節點的一段路,就叫路徑長度帶權路徑長度
:例如從根節點到 13 有3條路徑長度,則它的帶權路徑長度為 13*2=26樹的帶權路徑長度
:如圖上的WPL,所有葉子節點的帶權路徑長度之和
建立思路
以數列 [ 13,7,8,3,29,6,1 ]
進行講解。
-
首先將它進行從小到大進行排序,排序後是:
1,3,6,7,8,13,29
其中,每一個元素都是一個節點,每個節點可以看成是一顆最簡單的二叉樹
-
取出根節點權值最小的兩棵樹:
1 和 3
-
組成一棵新的二叉樹,該二叉樹的根節點權值是:這兩顆樹的權值之和,如下圖:
-
再將這顆新的二叉樹,以 根節點的權值大小,再次排序,並不斷重複上述步驟
如圖所示:將剩餘未處理的節點,與新的根節點權值進行排序,那麼再次取最小的兩棵樹 4 和 6
,組成新的根節點 10
一般來說,可以將左節點指向權值較大的,右節點指向權值較小的,但是這個不做特別規定。重複以上過程,直到組成如下圖這顆赫夫曼樹
單單看文字圖解,就有點索然無味了,下面進行程式碼實現。
程式碼實現
首先進行推導實現,方便理解。
推導實現
/**
* 赫夫曼樹實現
*/
public class HuffmanTreeTest {
/**
* 首先推導實現
*/
@Test
public void processDemo() {
int[] arr = {13, 7, 8, 3, 29, 6, 1};
// 1. 為了實現方便,先將每個元素轉成 Node 物件,並裝入 arrayList 中
List<Node> nodes = new ArrayList<>();
for (int i : arr) {
nodes.add(new Node(i));
}
// 2. 從小到大排序
Collections.sort(nodes);
// 3. 取出兩個較小的樹
// 因為從小到大排序了
Node left = nodes.get(0);
Node right = nodes.get(1);
// 4. 構成成新的二叉樹
Node parent = new Node(left.value + right.value);
parent.left = left;
parent.right = right;
// 5. 從 list 中刪除已經處理過的二叉樹
nodes.remove(left);
nodes.remove(right);
// 6. 將新的二叉樹新增到 list 中,為下一輪構建做準備
nodes.add(parent);
// 最後來看一下結果
System.out.println("原始陣列:" + Arrays.toString(arr));
System.out.println("新的節點:" + nodes);
}
}
/**
* 節點
*
* 為了讓Node 物件進行排序,用Collections工具類集合排序
* 讓Node 實現Comparable介面,因為會用到 Collections.sort(nodes); 進行排序,所以一定要實現Comparable介面
*/
class Node implements Comparable<Node> {
int value; // 權
Node left;
Node right;
public Node(int value) {
this.value = value;
}
/**
* 為了列印方便
*
* @return
*/
@Override
public String toString() {
return value + "";
}
/**
* 從小到大排序
*
* @param o
* @return
*/
@Override
public int compareTo(Node o) {
return this.value - o.value;
}
}
執行結果輸出
原始陣列:[13, 7, 8, 3, 29, 6, 1]
新的節點:[6, 7, 8, 13, 29, 4]
可以看到,第一輪的處理之後,的確如我們的建立思路解說一致。
那麼建立一顆完整的赫夫曼樹的核心程式碼就在上面,只要對上述步驟進行重複執行,就可以了。
完整實現
@Test
public void createHuffmanTreeTest() {
int[] arr = {13, 7, 8, 3, 29, 6, 1};
//構建赫夫曼樹
Node huffmanTree = createHuffmanTree(arr);
// 前序遍歷
huffmanTree.list();
}
private Node createHuffmanTree(int[] arr) {
// 1. 為了實現方便,先將每個元素轉成 Node 物件,並裝入 arrayList 中
List<Node> nodes = new ArrayList<>();
for (int i : arr) {
nodes.add(new Node(i));
}
//核心程式碼重複執行
while (nodes.size() > 1) {
// 2. 從小到大排序
Collections.sort(nodes);
// 3. 取出兩個較小的樹
Node left = nodes.get(0);
Node right = nodes.get(1);
// 4. 構成成新的二叉樹
Node parent = new Node(left.value + right.value);
parent.left = left;
parent.right = right;
// 5. 從 list 中刪除已經處理過的二叉樹
nodes.remove(left);
nodes.remove(right);
// 6. 將新的二叉樹新增到 list 中,為下一輪構建做準備
nodes.add(parent);
}
// 返回赫夫曼樹的 root 節點
// 因為前面從小到大排序的,最後一個就是最大節點
return nodes.get(0);
}
測試輸出,輸出的是前序遍歷的順序。
67
29
38
15
7
8
23
10
4
1
3
6
13
結果和下圖的前序遍歷是一致的,如果不懂前序遍歷 請看 資料結構與演算法——二叉樹
是不是有一個疑問?給定的陣列是
13,7,8,3,29,6,1
,變成樹之後,怎麼找回原來的資料?一定要記得赫夫曼樹的特點:它的資料都在葉子節點,父節點是通過葉子節點相加得到的