哈夫曼树的引入
哈夫曼树是一种树的最优结构,以哈夫曼博士命名。那么为什么要用哈夫曼树,下面以一个例子引入。
编程:将学生的百分制成绩转换成五分制成绩。
<
60
:
E
        
60
−
69
:
D
        
70
−
79
:
C
        
80
−
89
:
B
        
90
−
100
:
A
<60:E\;\;\;\;60-69:D\;\;\;\;70-79:C\;\;\;\;80-89:B\;\;\;\;90-100:A
<60:E60−69:D70−79:C80−89:B90−100:A
显然,可以用if else分支语句或switch开关语句来实现。如果用if else语句,则实现代码如下:
if (score < 60)
grade = 'E';
else if (score < 70)
grade = 'D';
else if (score < 80)
grade = 'C';
else if (score < 90)
grade = 'B';
else
grade = 'A';
上述的判断过程,其实就是一棵二叉树,
下面,具体化一些参数。如果每次的输入量很大,可以设置学生的成绩共有10000个,且每个分数端段的占比满足下图:
则考虑到程序的操作时间,这10000个数据的比较次数为:
10000
(
1
∗
5
%
+
2
∗
15
%
+
3
∗
40
%
+
4
∗
10
%
)
=
31500
次
10000(1*5\%+2*15\%+3*40\%+4*10\%)=31500次
10000(1∗5%+2∗15%+3∗40%+4∗10%)=31500次。
那么,如果把比较的次序换一下,比如下面这棵树,
则这10000个数据要比较的总次数为:
10000
(
3
∗
20
%
+
2
∗
80
%
)
=
22000
次
10000(3*20\%+2*80\%)=22000次
10000(3∗20%+2∗80%)=22000次。
显然,这两棵树(判别树)的效率是不一样的。
那么能不能找到一种效率最高的判别树呢?
下面的哈夫曼树就是一种最优二叉树。
哈夫曼树的相关概念
-
路径:从树的一个结点到另一个结点之间的分支构成这两个结点间的路径。
-
结点的路径长度:两结点间路径上的分支数。
-
例子:
从A到B,C,D,E,F,G,H,I的路径长度分别为1,1,2,2,3,3,4,4。
从A到B,C,D,E,F,G,H,I的路径长度分别为1,1,2,2,2,2,3,3。 -
树的路径长度:从树根到每一个结点的路径长度之和。记作:TL。
以上例(a)和(b)为例,
T L ( a ) = 0 + 1 + 1 + 2 + 2 + 3 + 3 + 4 + 4 = 20 T L ( b ) = 0 + 1 + 1 + 2 + 2 + 2 + 2 + 3 + 3 = 16 TL(a)=0+1+1+2+2+3+3+4+4=20 \\ TL(b)=0+1+1+2+2+2+2+3+3=16 TL(a)=0+1+1+2+2+3+3+4+4=20TL(b)=0+1+1+2+2+2+2+3+3=16
结点数相同的二叉树中,完全二叉树是路径长度最短的二叉树。但路径长度最短的二叉树不一定是完全二叉树。
-
权:将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。例如,前面不同分数段的学生数占比就是权值。
-
结点的带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积。
-
树的带权路径长度:树中所有叶子结点的带权路径长度之和。记作: W P L = ∑ k = 1 n w k l k WPL = \sum\limits_{k = 1}^n {{w_k}{l_k}} WPL=k=1∑nwklk。
-
例子: 有4个结点a,b,c,d,权值分别为7,5,2,4,构造以此4个结点为叶子结点的二叉树。
- 构造树(a)
W P L = 7 ∗ 2 + 5 ∗ 2 + 2 ∗ 2 + 4 ∗ 2 = 36 WPL=7*2+5*2+2*2+4*2=36 WPL=7∗2+5∗2+2∗2+4∗2=36 - 构造树(b)
W P L = 7 ∗ 3 + 5 ∗ 3 + 2 ∗ 1 + 4 ∗ 2 = 46 WPL=7*3+5*3+2*1+4*2=46 WPL=7∗3+5∗3+2∗1+4∗2=46
- 构造树(a)
以上两棵树,其WPL值不同,那么能不能找到使WPL值最小的树结构呢?这就是哈夫曼树。
-
哈夫曼树:最优树,带权路径长度(WPL)最短的树。
“带权路径最短”是在**“度相同”**的树中比较而得的结果,因此有最优二叉树,最优三叉树之称等等。 -
哈夫曼树:最优二叉树,带权路径长度(WPL)最短的二叉树。
-
例子: 延续上一个例子,我们也可以构造如下图的两个二叉树,
W P L = 35 WPL=35 WPL=35,这两棵树都是哈夫曼树。
满二叉树不一定是哈夫曼树。哈夫曼树中权值越大的叶子离根越近。具有相同带权结点的哈夫曼树不唯一。
哈夫曼树的构造算法
-
哈夫曼树中权越大的叶子离根越近。根据这一点,在构造哈夫曼树时首先选择权值小的叶子结点(贪心算法)。
-
哈夫曼算法(构造哈夫曼树的方法)
1.根据n个给定的权值 { w 1 , w 2 , ⋯   , w n } \{w_1,w_2,\cdots,w_n\} {w1,w2,⋯,wn}构成n棵二叉树森林 F = { T 1 , T 2 , ⋯   , T n } F=\{T_1,T_2,\cdots,T_n\} F={T1,T2,⋯,Tn},其中 T i T_i Ti只有一个带权为 w i w_i wi的根结点。
构造森林全是根
2.在F中选取两棵根结点的权值最小的树作为左右子树,构造一棵新的二叉树,且设置新的二叉树的根结点的权值为其左右子树上根结点的权值之和。
选用两小造新树
3.在F中删除这两棵树,同时将新得到的二叉树加入到森林中。
删除两小添新人
4.重复步骤2和3,直到森林中只有一棵树为止,这棵树即为哈夫曼树。
重复2、3剩单根 -
例子:
例子1.有4个结点a,b,c,d,权值分别为7,5,4,2,构造哈夫曼树。
例子2.有5个结点a,b,c,d,e,权值分别为7,5,5,2,4,构造哈夫曼树。
-
哈夫曼树的特点:
- 在哈夫曼算法中,初始时有n棵树,要经过 n − 1 n-1 n−1次合并最终形成哈夫曼树。
- 经过 n − 1 n-1 n−1次合并产生 n − 1 n-1 n−1个新结点,且这 n − 1 n-1 n−1个新结点都是具有两个孩子的分支结点(度为2)。
- 哈夫曼树中包含n个叶子结点(度为0)。
- 哈夫曼树中共有 n + n − 1 = 2 n − 1 n+n-1=2n-1 n+n−1=2n−1个结点,且其所有的结点的度均不为1。
哈夫曼算法的实现
- 采用顺序存储结构——一维结构数组
- 结点类型定义:
typedef struct
{
int weight; //结点权重
int parent, lch, rch; //定义三个指针,分别指向双亲、左孩子、右孩子
}HTNode,*HuffmanTree;
-
哈夫曼树中共有 2 n − 1 2n-1 2n−1个结点,不使用0下标的情况下,数组大小为 2 n 2n 2n。
例如,第一个结点的权值为5,即可表示为HuffmanTree H;H[i].weight=5;
-
例子: 有n=8,权值为 W = { 7 , 19 , 2 , 6 , 32 , 3 , 21 , 10 } W=\{7,19,2,6,32,3,21,10\} W={7,19,2,6,32,3,21,10},构造哈夫曼树。
1.构造森林全是根,初始化数组如下
2.选用两小造新树,选择两个权值较小的结点(结点3和结点6),构造一个新树(新树的根结点为结点9,权值为5),
3.删除两小添新人,删除结点3和结点6的方法就是在其parent的域里写上新添加的结点(结点9),同时将结点9的左右孩子域写上3和6,表示该结点的左右孩子是结点3和结点6。
4.重复2、3剩单根,重复步骤2,3,直到数组里parent域只剩一个值为0的结点的时候(单根的情况),结束。
-
算法过程:
1.初始化 H T [ 1 , 2 , ⋯   , 2 n − 1 ] HT [1,2,\cdots,2n-1] HT[1,2,⋯,2n−1]:lch=rch=parent=0;
2.输入初始的n个叶子结点:置 H T [ 1 , 2 , ⋯   , n ] HT [1,2,\cdots,n] HT[1,2,⋯,n]的weight值;
3.进行以下 n − 1 n-1 n−1次合并,依次产生 n − 1 n-1 n−1个结点 H T [ i ] , i = n + 1 , ⋯   , 2 n − 1 HT[i],i=n+1,\cdots,2n-1 HT[i],i=n+1,⋯,2n−1:
(1)在 H T [ 1 , 2 , ⋯   , i − 1 ] HT[1,2,\cdots,i-1] HT[1,2,⋯,i−1]中选两个未被选过(从parent==0的结点中选)的weight值最小的两个结点 H T [ s 1 ] HT[s1] HT[s1]和 H T [ s 2 ] HT[s2] HT[s2], s 1 s1 s1和 s 2 s2 s2为两个最小结点的下标;
(2)修改 H T [ s 1 ] HT[s1] HT[s1]和 H T [ s 2 ] HT[s2] HT[s2]的parent值: H T [ s 1 ] . p a r e n t = i ; H T [ s 2 ] . p a r e n t = i HT[s1].parent=i;HT[s2].parent=i HT[s1].parent=i;HT[s2].parent=i;
(3)修改新产生的 H T [ i ] HT[i] HT[i]:
H T [ i ] . w e i g h t = H T [ s 1 ] . w e i g h t + H T [ s 2 ] . w e i g h t ; HT[i].weight=HT[s1].weight+HT[s2].weight; HT[i].weight=HT[s1].weight+HT[s2].weight;
H T [ i ] . l c h = s 1 ; H T [ i ] . r c h = s 2 ; HT[i].lch=s1;HT[i].rch=s2; HT[i].lch=s1;HT[i].rch=s2; -
算法描述;
void CreateHuffmanTree(HuffmanTree HT, int n)
{
if (n <= 1)
return;
m = 2 * n - 1; //数组共2n-1个元素
HT = new HTNode[m + 1]; //0号单元未用,HT[m]表示根结点
for (i = 1; i <= m; ++i) //将2n-1个元素的lch、rch、parent置为0
{
HT[i].lch = 0;
HT[i].rch = 0;
HT[i].parent = 0;
}
for (i = 1; i <= n; ++i) //输入前n个元素的weight值
cin >> HT[i].weight;
//初始化结束,下面开始建立哈夫曼树
for (i = n + 1; i <= m; i++)
{
Select(HT, i - 1, s1, s2); //在HT[k](1<=k<=i-1)中选择两个其双亲域为0,且权值最小的结点,并返回它们在HT中的序号s1和s2
HT[s1].parent = i; //表示从F中删除s1,s2
HT[s2].parent = i;
HT[i].lch = s1; //s1,s2分别作为i的左右孩子
HT[i].rch = s2;
HT[i].weight = HT[s1].weight + HT[s2].weight; //i的权值为左右孩子权值之和
}
}