1. 概念
哈夫曼樹是一個特殊的二叉樹,它的特殊在於:
- 葉子節點帶有權值:對葉子結點賦予的一個有意義的數值量
- 二叉樹的帶權路徑長度:設二叉樹具有n個帶權值的葉子結點,從根結點到各個葉子結點的路徑長度與相應葉子結點權值的乘積之和。記為 WPL=Wklk,這里的WPL即帶權路徑長度(Weight Path Length)。
- 權值越大的葉子結點越靠近根結點,而權值越小的葉子結點越遠離根結點
- 只有度為0的葉子結點和度為2的分支節點,不存在度為1的結點
綜上,總結哈夫曼樹的概念為:
哈夫曼樹:給定一組具有確定權值的葉子節點,帶權路徑長度最小的二叉樹
舉例:給定4個葉子結點,其權值分別為{2,3,4,7},可以構造出形狀不同的多個二叉樹。
如上圖所示,可以根據給定的葉子結點構建出不同的二叉樹結構,按照計算以上三個二叉樹帶權路徑長度計算公式得出的WPL分別為32、41、30,根據哈夫曼樹定義可知,最右側WPL=30的這個二叉樹就是一個哈夫曼樹。
2. 基本思想
2.1 初始化
由給定的n個權值 {w1,w2,…,wn} 構造n顆只有一個根結點的二叉樹,從而得到一個二叉樹集合F= {T1,T2,…,Tn}
2.2 選取與合並
在集合F中選取根結點的權值最小的兩顆二叉樹分別作為左、右子樹構造一顆新的二叉樹,這顆二叉樹的根結點的權值為其左、右子樹根結點的權值之和
2.3 刪除與加入
在集合F中刪除作為作為左、右子樹的兩顆二叉樹,並將新建立的二叉樹加入到集合F中
2.4 遞歸
重復以上選取與合並、刪除與加入兩步,當集合F中只剩下一顆二叉樹時,這顆二叉樹便是哈夫曼樹
2.5 實例
我們按照上面的例子,初始化集合{2,3,4,7}來模擬下哈夫曼樹的推演過程,記錄如下:
3. 數據結構
設置一個數組 huffTree 來存儲哈夫曼樹中各結點信息,數組元素的數據結構如下圖:
- weight 權值
- leftChild 左子節點,存儲數組下標
- rightChild 右子節點,存儲數組下標
- parent 父節點,存儲數組下標
數組大小可以通過2n-1來計算獲得,為何是2n-1大小,可以參考二叉樹性質來計算。
根據上面的初始化、選取與合並、刪除與加入、遞歸的步驟方法推演,進行數組操作過程如下:
4. 代碼實現
參考的資料中是采用朴素的數組進行存儲的,數組存儲的困難在於不利於動態擴容和調整,這里采用List集合進行存儲,實際上底層也是數組存儲,只是通過封裝好的容器進行調用即可。這里還是延用上面圖例中的權值數組 {2,3,4,7},除此之外,還可以使用 {2,3,5,7,5} 這種新構建結點等於已存在權值結點、多個權值結點權值相等各種特殊情況來進行驗證。
/** * 哈夫曼樹 * created by guanjian on 2020/12/30 15:43 */ public class HuffmanTree { //權值結點 private Integer[] weightArray; //哈夫曼樹結點總數 2*weights - 1 private int huffmanTreeNodeLength; //哈夫曼樹結點數組 private List<HuffmanTreeNode> huffmanTreeNodeList; //每次獲取最小權值數量 private final static int minNodeLength = 2; public HuffmanTree(Integer[] weightArray) { Assert.notEmpty(weightArray, "weightArray can not be empty"); this.weightArray = weightArray; } /** * 初始化哈夫曼樹結點數組 */ public void initHuffmanTreeNodeArray() { //哈夫曼樹結點總數 2*weights - 1 this.huffmanTreeNodeLength = 2 * this.weightArray.length - 1; this.huffmanTreeNodeList = Lists.newArrayListWithCapacity(huffmanTreeNodeLength); //初始化填充哈夫曼樹結點 IntStream.range(0, weightArray.length).forEach(i -> { //這里將權值灌入到node節點中 huffmanTreeNodeList.add( HuffmanTreeNode.Builder.aHuffmanTreeNode() .weight(weightArray[i]) .build() ); }); System.out.format("初始化完成 huffmanTreeNodeList=%s \n", Arrays.toString(huffmanTreeNodeList.toArray())); } /** * 查找最小權值方法 */ public List<HuffmanTreeNode> findMinNodes() { //按權值從小到大升序進行排序 List<HuffmanTreeNode> sortedList = huffmanTreeNodeList.stream() .filter(x -> !x.isSorted()) .sorted(Comparator.comparing(HuffmanTreeNode::getWeight)) .collect(Collectors.toList()); sortedList.forEach(x -> System.out.format("排序 %s 處理 %s \n", x.getWeight(), x.isSorted())); //取最小權值的兩個node List<HuffmanTreeNode> list = sortedList.subList(0, sortedList.size() >= minNodeLength ? minNodeLength : sortedList.size()); if (CollectionUtils.isEmpty(list)) return list; list.forEach(x -> { x.setSorted(true); });