1. 程式人生 > >一本正經的聊資料結構(6):最優二叉樹 —— 哈夫曼樹

一本正經的聊資料結構(6):最優二叉樹 —— 哈夫曼樹

![](https://cdn.geekdigging.com/DataStructure/head.png) 前文傳送門: [「一本正經的聊資料結構(1):時間複雜度」](https://www.geekdigging.com/2020/03/28/6072951828/) [「一本正經的聊資料結構(2):陣列與向量」](https://www.geekdigging.com/2020/04/14/3143023839/) [「一本正經的聊資料結構(3):棧和佇列」](https://www.geekdigging.com/2020/04/25/5497775819/) [「一本正經的聊資料結構(4):樹」](https://www.geekdigging.com/2020/05/03/1553615465/) [「一本正經的聊資料結構(5):二叉樹的儲存結構與遍歷」](https://www.geekdigging.com/2020/05/07/7201023288/) ## 基礎知識 感謝某位在後臺留言的同學,讓我想起來我還有這個沒寫完的系列。 在最開始,先了解幾個基礎概念: * 路徑:在一棵樹中,一個結點到另一個結點之間的通路,稱為路徑。 ![](https://cdn.geekdigging.com/DataStructure/hafuman_1.png) 上面這個二叉樹中,根節點 A 到葉子結點 I 的路徑,就是A,B,D,I。 * 路徑長度:在一條路徑中,每經過一個結點,路徑長度都要加 1 。例如在一棵樹中,規定根結點所在層數為1層,那麼從根結點到第 i 層結點的路徑長度為 i - 1 。 ![](https://cdn.geekdigging.com/DataStructure/hafuman_2.png) 在這個二叉樹中,根節點 A 到葉子結點 H 的路徑長度就是 3 。 * 結點的權:給每一個結點賦予一個數值,被稱為這個結點的權。 * 結點的帶權路徑長度:指的是從根結點到該結點之間的路徑長度與該結點的權的乘積。 ![](https://cdn.geekdigging.com/DataStructure/hafuman_3.png) 我們假設節點 H 的權是 5 ,從根結點到結點 H 的路徑長度是 3 ,那麼結點 H 的帶權路徑長度是 3 X 5 = 15。 * 樹的帶權路徑長度:在一棵樹中,所有葉子結點的帶權路徑長度之和,被稱為樹的帶權路徑長度,也被簡稱為 「WPL」 。 ![](https://cdn.geekdigging.com/DataStructure/hafuman_4.png) 還是這顆樹,它的帶權路徑長度是 1 X 2 + 2 X 1 + 2 X 3 + 2 X 4 + 3 X 5 + 3 X 6 = 51 。 ## 哈夫曼樹 哈弗曼樹就是在用 n 個結點(都做葉子結點且都有各自的權值)試圖構建一棵樹時,如果構建的這棵樹的帶權路徑長度最小,稱這棵樹為「最優二叉樹」,有時也叫「哈夫曼樹」或者「赫夫曼樹」。 在構建哈弗曼樹時,要使樹的帶權路徑長度最小,只需要遵循一個原則,那就是:權重越大的結點離樹根越近。 需要注意的是,同樣葉子結點所構成的哈夫曼樹可能不止一顆,在同一層,左右葉子節點交換位置,樹的帶權路徑長度是一樣的,就比如下面這兩個哈夫曼樹: ![](https://cdn.geekdigging.com/DataStructure/hafuman_5.png) ## 哈弗曼樹的構建過程 那麼如何構建一個哈夫曼樹呢?我們這裡舉個例子,比如我們有這麼幾個葉子節點:3,4,7,9,13,15,17: ![](https://cdn.geekdigging.com/DataStructure/goujianhanfuman_1.png) 第一步:選出兩個最小的權值,對應的兩個結點組成一個新的二叉樹,且新二叉樹的根結點的權值為左右孩子權值的和: ![](https://cdn.geekdigging.com/DataStructure/goujianhanfuman_2.png) 第二步:從佇列中移除上一步選擇的兩個最小結點,把新的父節點加入佇列,也就是從佇列中刪除 3 和 4 ,插入 7 ,並且仍然保持佇列的升序: ![](https://cdn.geekdigging.com/DataStructure/goujianhanfuman_3.png) 第三步:選擇當前權值最小的兩個結點,生成新的父結點。 這一步其實是在重複上一步操作,當前佇列中最小的節點是 7 和 7 ,生成新的父結點權值是 7 + 7 = 14 : ![](https://cdn.geekdigging.com/DataStructure/goujianhanfuman_4.png) 第四步:從佇列中移除上一步選擇的兩個最小結點,把新的父節點加入佇列。 這一步依然是在重複,從佇列中刪除 7 和 7 ,加入 14 ,並且仍然保持佇列的升序: ![](https://cdn.geekdigging.com/DataStructure/goujianhanfuman_5.png) 第五步:選擇當前權值最小的兩個結點,生成新的父結點。 這一步還是重複操作。當前佇列中權值最小的結點是 9 和 14 ,生成新的父結點權值是 9 + 14 = 23 : ![](https://cdn.geekdigging.com/DataStructure/goujianhanfuman_6.png) 第六步:從佇列中移除上一步選擇的兩個最小結點,把新的父節點加入佇列。 這一步依然是在重複,從佇列中刪除 9 和 14 ,加入 23 ,並且仍然保持佇列的升序: ![](https://cdn.geekdigging.com/DataStructure/goujianhanfuman_7.png) 第七步:選擇當前權值最小的兩個結點,生成新的父結點。 這一步從佇列中選擇權值最小的結點是 13 和 15 ,生成新的父結點權值是 13 + 15 = 28 : ![](https://cdn.geekdigging.com/DataStructure/goujianhanfuman_8.png) 第八步:從佇列中移除上一步選擇的兩個最小結點,把新的父節點加入佇列。 從佇列中刪除 13 和 15 ,加入 28 ,並且仍然保持佇列的升序: ![](https://cdn.geekdigging.com/DataStructure/goujianhanfuman_9.png) 第九步:選擇當前權值最小的兩個結點,生成新的父結點。 這一步從佇列中選擇權值最小的結點是 17 和 23 ,生成新的父結點權值是 17 + 23 = 40 : ![](https://cdn.geekdigging.com/DataStructure/goujianhanfuman_10.png) 第十步:從佇列中移除上一步選擇的兩個最小結點,把新的父節點加入佇列。 從佇列中刪除 17 和 23 ,加入 40 ,並且仍然保持佇列的升序: ![](https://cdn.geekdigging.com/DataStructure/goujianhanfuman_11.png) 第十一步:選擇當前權值最小的兩個結點,生成新的父結點,移除佇列中的最後兩個節點(懶得畫了,最後兩步並一步)。 這一步從佇列中選擇權值最小的結點是 28 和 40 ,生成新的父結點權值是 28 + 40 = 68 : ![](https://cdn.geekdigging.com/DataStructure/goujianhanfuman_12.png) 此時,我們得到的這棵樹就是哈弗曼樹。 哈夫曼樹就介紹到這裡,下一節,將會介紹哈夫曼樹的用途:哈夫曼編碼。 ## 參考 http://c.biancheng.net/view/3398.html https://baijiahao.baidu.com/s?id=1663514710675419737&wfr=spide