一、 一些基本概念
1、 路径长度
树中一个结点到另一个结点之间的分支构成这两个结点之间的路径,路径上的分支数目为路径长度
上图中,从1到4的路径长度为2,从7 到3的路径长度为4
2、 树的路径长度
从树根到每一个结点的路径长度之和,这种路径长度最短的树是完全二叉树
上图中,树的路径长度为:1 + 1 + 2 + 2 + 3 + 3 = 12
3、 权
若将树中结点赋一个有某种含义的数值,则这个数值称为该结点的权
4、 结点的带权路径长度
从根结点到该结点间的路径长度与该结点的权的乘积
2、 树的路径长度
从树根到每一个结点的路径长度之和,这种路径长度最短的树是完全二叉树
上图中,树的路径长度为:1 + 1 + 2 + 2 + 3 + 3 = 12
3、 权
若将树中结点赋一个有某种含义的数值,则这个数值称为该结点的权
4、 结点的带权路径长度
从根结点到该结点间的路径长度与该结点的权的乘积
2、 树的路径长度
从树根到每一个结点的路径长度之和,这种路径长度最短的树是完全二叉树
上图中,树的路径长度为:1 + 1 + 2 + 2 + 3 + 3 = 12
3、 权
若将树中结点赋一个有某种含义的数值,则这个数值称为该结点的权
4、 结点的带权路径长度
从根结点到该结点间的路径长度与该结点的权的乘积
上图中,结点d的带权路径长度为:4 X 3 = 12
5、 树的带权路径长度WPL
树中所有带权结点的路径长度之和
上图中,树的带权路径长度WPL为:7 X 1 + 5 X 2 + 2 X 3 + 4 X 3 = 35
6、 哈夫曼树(最优二叉树)
有n个叶子结点的二叉树,每个叶子结点均带权,带权路径长度WPL最小的二叉树即为哈夫曼树
二、 哈夫曼树的构造
1、 给定n个权值{w1, w2, w3 …… wn}构成 n棵二叉树的集合F = {T1, T2, T3 …… Tn}, 其中每棵树只有权值为Wi的根节点,其左右子树均为空。
例如,给定结点的权值为7 5 2 4,构造出树如下:
2、 在二叉树集合F中选取两棵根结点的权值最小的树作为左右子树,构造一棵新的二叉树,且新的二叉树的根节点的权值为其左右子树结点的权值之和。
3、 在F中删除这两棵树,并将新的二叉树加入到F中
4、 重复2、3,直到F只含一棵树为止,这棵树便是哈夫曼树
三、 哈夫曼树的应用--得到最佳判定算法
例如,编制一个将百分制转换成五分制的程序,判定过程可用判定树来表示
如果分数小于60,我们做1次判断即可;如果分数为60~70,需要判断2次;如果分数为70~80,需要判断3次;如果分数为80~90,需要判断4次;如果分数为90~100,需要判断5次。
在实际中,学生成绩在五个等级上的分布上不均匀的,假设分布规律如下:
如果有100个数据,判断次数为:100 X 5% X 1 + 100 X 15% X 2 + 100 X 40% X 3 + 100 X 30% X 4 + 100 X 10% X 5 = 325
如果每次输入的量很大,就需要考虑尽量减少判断次数,降低程序的运行时间。
从上面的分布图上可看出,80%以上的数据需要进行三次或更多次的比较才能得到结果,但如果以5,15,40,30,10作为权,构造一棵有5个叶子结点的哈夫曼树,可得到:
这样对100个数据进行判断,次数为:100 X 5% X 4 + 100 X 15% X 3 + 100 X 40% X 1 + 100 X 30% X 2 + 100 X 10% X 4 = 205
这样可以使大部分数据经过教少的比较次数得出结果。把每个判定框内的两次比较分开,得到最终的判定树,如下:
四、哈夫曼编码
1、 定长编码与变长编码
在处理字符串序列时,如果对每个字符串采用相同的二进制位来表示,则称这种编码方式为定长编码。假如传送的电文为“A B A C C D A”,编码分别为00、01、10、11,则上述电文便为“00010010101100”,译码时按二位一分进行译码。但传送电文时,希望电文长度尽可能短,因此可采用变长编码方式,即对不同的字符采用不等长的二进制位进行表示。可变长编码的特点就是,对使用频率高的字符采用短编码,而对使用频率低的字符采用长编码的方式。这样我们就可以减少数据的存储空间,从而起到压缩数据的效果。
采用变长编码方式需要解决两个问题
A、 编码尽可能短。这样可以提高压缩率,节省空间,提高运输和通信速度
B、 不能有二义性。如果设计A、B、C、D的编码为0、00、1、01,如果传送的字串为“0000”,译码时可以译为AAAA、ABA、BB等,不唯一,所以必须采用前缀编码,也就是说任一个字符的编码都不是另一个字符的编码的前缀。如0,101和100是前缀编码。由前缀码形成的序列可以被唯一的组成一个字符串序列。如00101100可以被唯一的分析为0,0,101和100。
哈夫曼编码就很好的解决了上面两个问题。
2、 设计前缀编码
可以利用二叉树来设计前缀编码,如下图所示二叉树,叶子结点为A、B、C、D,且左分支表示0,有分支表示1,则以从根结点到叶子结点的路径上的分支字符组成的字符串作为该叶子结点字符的编码,这个编码即为二进制前缀编码,可得A、B、C、D的编码0、10、110、111。
3、 哈夫曼编码
哈夫曼编码的基本思想是以字符的使用频率作为权,构造一棵哈夫曼树,然后利用哈夫曼树对字符进行编码。这棵哈夫曼树,是将所要编码的字符作为叶子结点,该字符在文件中的使用频率作为叶子结点的权,以自底向上的方式,通过n-1次合并运算后构造出一棵树,权值越大的叶子离根越近。
4、 哈夫曼树的数据结构
哈夫曼树中没有度为1的结点,则一棵有n个叶子结点的哈夫曼树共有2n – 1个结点(n – 1次合并,每次产生一个新结点),对每个结点而言,需要知道双亲和孩子的信息,因此设定的存储结构如下
typedef struct
{
double weight; //权值
int parent; //双亲
int lchild; //左孩子
int rchild; //右孩子
char value; //该节点表示的字符
} HNodeType;
5、 构造哈夫曼树
int i, j, x1, x2; //x1、x2为两个最小权值结点的序号。
double m1,m2; //m1、m2为两个最小权值结点的权值。
for (i=0; i<n-1; i++) {
m1=m2=MAXVALUE; //初始化为最大值
x1=x2=-1; //初始化为-1
//找出所有结点中权值最小、无双亲结点的两个结点
for (j=0; j<n+i; j++) {
if (HuffNode[j].weight < m1 && HuffNode[j].parent==-1) {
m2 = m1;
x2 = x1;
m1 = HuffNode[j].weight;
x1 = j;
} else if (HuffNode[j].weight < m2 && HuffNode[j].parent==-1) {
m2=HuffNode[j].weight;
x2=j;
}
}
/* 更新新树信息 */
HuffNode[x1].parent = n+i; //x1的父亲为新结点编号n+i
HuffNode[x2].parent = n+i; //x2的父亲为新结点编号n+i
HuffNode[n+i].weight =m1+m2; //新结点权值为两个最小权值之和m1+m2
HuffNode[n+i].lchild = x1; //新结点n+i的左孩子为x1
HuffNode[n+i].rchild = x2; //新结点n+i的右孩子为x2
}
举例:
有一些字符和他们的使用频率,如下图:
初始化哈夫曼树
对应的树
i=0时,j=0;j<6;找双亲为−1,权值最小的两个数:
x1=0 x2=3;//x1、x2为两个最小权值结点的序号
m1=5 m2=7;//m1、m2为两个最小权值结点的权值
HuffNode[0].parent = 6; //x1的父亲为新结点编号n+i
HuffNode[3].parent = 6; //x2的父亲为新结点编号n+i
HuffNode[6].weight =12; //新结点权值为两个最小权值之和m1+m2
HuffNode[6].lchild = 0; //新结点n+i的左孩子为x1
HuffNode[6].rchild = 3; //新结点n+i的右孩子为x2
第一次循环后
对应的哈夫曼树
全部循环完成后
对应的哈夫曼树
五、 哈夫曼树的应用
1、 数据的压缩:在通信中我们可以先对发送的数据进行哈夫曼编码压缩数据提高传输速度。
2、 查询优化:在查询的时候我们把查询频率最高的数据建立索引提高查询速度