一、基本术语
1、路径和路径长度
若在一棵二叉树中存在着一个结点序列k1、k2......kj,使得ki是ki+1的双亲(1<=i<=j),则称此结点序列是从k1到kj的路径,因树中每个结点只有一个双亲结点,所以它也是这两个结点之间唯一的路径。从k1到kj所经过的分支数称为这两点之间的路径长度,它等于路径上的结点数减1。
2、结点的权和带权路径长度
在许多应用中,常常将树中的结点赋上一个有着某种意义的实数,称此实数为该结点的权。结点的带权路径长度规定为从树根结点到该结点之间的路径长度与该结点上权的乘积。(结点的带权路径长度=路径长度*结点的权)
3、树的带权路经长度
树的带权路径长度定义为树中所有叶子结点的带权路径长度之和。
4、哈夫曼树
哈夫曼树又称为最优二叉树。它是n个带权叶子结点构成的所有二叉树中带权路径长度(WPL)最小的二叉树。
例如,有4个叶子结点a,b,c,d,分别带权为9、4、5、2,由他们构成的三棵不同的二叉树(当然还有其他许多种)分别如图7-6(a)~(c)所示。
每一棵二叉树的带权路径长度WPL分别为:
(a)WPL=9*2+4*2+5*2+2*2=40
(b)WPL=4*1+2*2+5*3+9*3=50
(c)WPL=9*1+5*2+4*3+2*3=37
图7-6(c)中的带权二叉树的WPL值最小,为37。稍后便知,此树就是由上述4个叶子结点构成的所有二叉树中的一棵哈夫曼树。
由上面可以大致看出,由n个带权叶子结点所构成的二叉树中, 满二叉树或完全二叉树不一定是最优二叉树。权值越大的结点离树根越近的二叉树才是最优二叉树。
二、构造哈夫曼树
构造最优二叉树的算法是由哈夫曼提出的,所以称为哈夫曼算法,具体叙述如下:
(1)根据与n个权值{ w1,w2。。。,wn}对应的n个结点构成具有n棵二叉树的森林F={ T1,T2,T3。。。Tn },其中,每棵二叉树Ti(1<=i<=n)都只有一个权值为wi的根结点,其左、右子树均为空。
(2)在森林F中选出两棵根结点的权值最小的树作为一棵新树的左、右子树,且置新树的根结点的权值为其左,右子树上根结点的权值之和。
(3)从F中删除构成新树的那两棵树,同时把新树加入F中。
(4)重复(2)和(3)步,每次使F中减少一棵树,直到F中只含有一棵树为止,此树就是所求的哈夫曼树。
假定仍采用图7-6中的4个带权叶子结点来构造一棵哈夫曼树,按照上述算法,则构造过程如图7-7所示,其中,图7-7(d)就是最后生成的哈夫曼树,它的带权路径长度为37,由此可知,图7-6(c)是一棵哈夫曼树。
在构造哈夫曼树的过程中,每次由两棵权值最小的树生成一棵新树时,新树的左子树和右子树可以任意安排,这样将会得到具有不同结构的多个哈夫曼树,但它们都具有相同的带权路径长度。为了使得到的哈夫曼树的结构尽量唯一,通常规定生成的哈夫曼树中每个结点的左子树根结点的权小于或等于右子树根结点的权。上述哈夫曼树的构造过程就是遵照这一规定进行的。
根据上述构造哈夫曼树的方法可以写出相应的java语言描述的算法:
public static BTreeNode createHuffmanTree(Integer []a)
{
//以数组a的长度赋给n,并创建一个长度为n的结点指针数字b
int n=a.length;
BTreeNode[] b=new BTreeNode[n];
//初始化b指针数组,使每个指针元素指向a数组中对应元素所建立的结点
for(int i=0;i<n;i++)
{
b[i]=new BTreeNode(a[i]);
}
//用k1表示当前森林中具有最小权值的树根结点的下标
//用k2表示当前森林中具有次最小权值的树根结点的下标
int k1=0,k2=0;
//进行n-1次循环,建立哈夫曼树
for(int i=1;i<n;i++)
{
int k=0;
//每次循环开始让k1指向第一棵非空树,k2指向第二棵非空树
while(b[k]==null)
{
k++;
}
k1=k;
k++;
while(b[k]==null)
{
k++;
}
k2=k;
//从当前森林中求出最小权值树和次最小权值树
for(int j=k2;j<n;j++)
{
if(b[j]!=null)
{
Integer x=(Integer)b[j].element; //用x保存b[j]结点的权值(即k2的权值)
if(x.compareTo((Integer)b[k1].element)<0)
{
k2=k1; //当k1大于k2时,交换位置,小的在前,大的在后
k1=j;
}
else if(x.compareTo((Integer)b[k2].element)<0)
{
k2=j;
}
}
}
//根据最小权值树b[k1]和次最小权值树b[k2]建立一棵新树,并由b[k1]所指向
Integer x=(Integer)b[k1].element+(Integer)b[k2].element;
b[k1]=new BTreeNode(x,b[k1],b[k2]);
//将次最小权值树b[k2]置为空
b[k2]=null;
}
//返回整个哈夫曼树的树根指针
return b[k1];
}
在一棵哈夫曼树的生成过程中,每次都由两棵子树构成一棵树,对于n个叶子结点共需要构成n-1棵子树。所以,在一棵哈夫曼树中只存在双支结点和叶子结点,若叶子结点为n个,则双支结点必为n-1个。
下面给出根据哈夫曼树求出带权路径长度的算法。
//求出带权路径长度的算法
public static int weighthLength(BTreeNode ft,int len)
{
//根据ft指针所指向的哈夫曼树求出带权路径长度,len初值为0
if(ft==null)
{
return 0; //空树则返回0
}
else{
//访问到叶子结点时返回该结点的带权路径长度,其中,值参len
//保存当前被访问结点的路径长度
if(ft.left==null&&ft.right==null)
{
return ((Integer)ft.element)*len;
}
//访问到非叶子节点时,进行递归调用,返回左、右子树的带权
//路径长度之和,向下深入一层时len值增1
else{
return weighthLength(ft.left,len+1)+weighthLength(ft.right,len+1);
}
}
}