一、基本概念
1、路径:从树的一个结点到另一个结点之间的分支构成这两个结点之间的路径
2、路径长度:路径上的分支的数目
3、树的路径长度:从树根到每一个结点的路径长度之和
4、结点的带权路径长度:从该结点到树根之间的路径长度与结点上权值的乘积
5、树的带权路径长度:树中所有叶子结点的带权路径长度之和,通常记作WPL
哈夫曼树:假如一颗二叉树有n个叶子结点,且n个叶子结点都有对应的权值,那么带权路径长度最小的二叉树称为哈夫曼树
叶子结点为a,b,c,d,权值对应为7,5,2,4的二叉树中为哈夫曼树的二叉树如下图:
二、哈夫曼树的构造
思路就是让权值大的叶子结点离根近,权值小的叶子结点离根远。
(1)根据给定的n个权值{w1,w2,...,wn},构造n颗只有根节点的二叉树,这n颗二叉树构成森林F。
(2)在森林F中选取两颗根节点的权值最小的树作为左右子树构造一颗新的二叉树,且置新的二叉树的根节点的权值为其左、右子树的权值之和。
(3)在森林F中删除这两颗树,同时将得到的新二叉树加入到F中。
(4)重复(2)和(3),直到F只含一颗树为止。这棵树便是哈夫曼树。
下图是叶子结点权值为7,5,2,4的构造过程
三、算法实现
一颗有n个叶子结点的哈夫曼树有2n-1个结点,可以将所有结点存储在数组中。树的结点要包含双亲和孩子结点的信息。可以用数组的1~n来存储叶子结点,n+1~2*n-1存储非叶子结点。
package trees;
import java.util.*;
//哈夫曼树及哈夫曼编码,使用数组存储
public class HuffmanTree {
static final int MAX = 200000000; //最大权值
static class HTNode{
int left; //左孩子下标
int right; //右孩子下标
int parent; //双亲结点下标
int weight ; //权值,初值为0
public HTNode(){
left = 0;
right = 0;
parent = 0;
weight = 0;
}
public HTNode(int weight){
this.weight = weight;
}
public HTNode(int weight,int parent,int left,int right){
this.weight = weight;
this.parent = parent;
this.left = left;
this.right = right;
}
public String toString(){
return weight+" "+parent+" "+left+" "+right+"\n";
}
}
//存储结构,假设有n个叶子结点,1~n存叶子结点,n+1~2n-1存非叶子结点
private ArrayList<HTNode> HT;
private ArrayList<String> HC; //存储哈夫曼编码
public HuffmanTree(){
HT = null;
}
public HuffmanTree(List<Integer> ht){
init(ht);
createHuffman(ht.size());
createHuffmanCode(ht.size());
}
private void init(List<Integer> ht){ //初始化,ht中是叶子结点的权值
HT = new ArrayList<>();
int len = ht.size();
HT.add(new HTNode()); //为了方便,HT[0]不使用
for(int i=0;i<len;i++){ //n个叶子结点
HT.add(new HTNode(ht.get(i),0,0,0));
}
for(int i=0;i<len-1;i++){//n-1个非叶子结点
HT.add(new HTNode());
}
//System.out.println(HT);
}
//构造哈弗曼树
private void createHuffman(int len){
int m = 2*len;
for(int i=len+1;i<m;i++){ //n+1~2*n-1保存非叶子结点
int w1 = MAX,s1=0;
int s2=0,w2=w1;
for(int j=1;j<i;j++){ //找最小的
if(HT.get(j).weight<w1 && HT.get(j).parent == 0){
w1 = HT.get(j).weight;
s1 = j;
}
}
for(int j=1;j<i;j++){ //找第二小的
if(HT.get(j).weight<w2 && j!=s1 && HT.get(j).parent == 0){
w2 = HT.get(j).weight;
s2 = j;
}
}
HT.get(s1).parent = i; //s1,s2的双亲设置为HT[i]
HT.get(s2).parent = i;
HT.get(i).left = s1; //HT[i]的左右孩子设置成s1,s2
HT.get(i).right = s2;
HT.get(i).weight = HT.get(s1).weight + HT.get(s2).weight; //HT[i]的权重
}
}
public String toString(){
return HT.toString()+HC.toString();
}
//哈夫曼编码,从叶子结点向上回溯,如果回溯时走左分支,则生成代码0,走右分支则生成代码1,最后
//反转一下
public void createHuffmanCode(int len){
HC = new ArrayList<String>();
for(int i=1;i<=len;i++){ //对叶子结点i进行编码
StringBuilder sb = new StringBuilder("");
int c = i; //
int f = HT.get(i).parent; //找到i的父节点
while(f != 0){ //从叶子结点向上回溯直到根结点
if(HT.get(f).left == c)
sb.append("0");
else
sb.append("1");
c = f; //向上回溯
f = HT.get(f).parent;
}
System.out.println(sb.toString());
HC.add(sb.reverse().toString()); //反转一下并存入
}
}
public static void main(String[] args){
Integer[] a = new Integer[]{5,29,7,8,14,23,3,11};
ArrayList<Integer> t = new ArrayList<>(Arrays.asList(a));
HuffmanTree ht = new HuffmanTree(t);
System.out.print(ht);
}
}