第六章 树和二叉树2
TRANSCRIPT
6.8 哈 夫 曼 树 与 哈 夫 曼 编 码• 最优树 ( 哈夫曼树 ) 的定义• 如何构造最优树• 前缀编码• 哈夫曼编码
一、最优树的定义
树的路径长度定义为: 树中每个结点的路径长度之和。
结点的路径长度定义为: 从根结点到该结点的路径上 分支的数目。
结点的带权路径长度定义为: 从根结点到该结点的路径长度与 结点上权的乘积。
树的带权路径长度定义为: 树中所有叶子结点的带权路径长度之和 WPL(T) = wklk ( 对所有叶子结点 ) 。
在所有含 n 个叶子结点、并带相同权值的 m 叉树中,必存在一棵其带权路径长度取最小值的树,称为“最优树”。
例如:
WPL(T)=
72+52
+22+42 =36
WPL(T)=
71+52
+23+43
=35
WPL(T)=
73+53
+42+21 =46
根据给定的 n 个权值 {w1, w2,
…, wn} ,构造 n 棵二叉树的集合 F = {T1, T2, … , Tn} ,其中每棵二叉树 Ti 中均只含一个带权值 为 wi 的根结点,其左、右子树为空树;
二、如何构造最优树
(1)( 哈夫曼算法 ) 以二叉树为例:
在 F 中选取其根结点的权值为最 小的两棵二叉树,分别作为左、 右子树构造一棵新的二叉树,并 置这棵新的二叉树根结点的权值 为其左、右子树根结点的权值之 和;
(2)
从 F 中删去这两棵树,同时加入 刚生成的新树;
重复 (2) 和 (3) 两步,直至 F 中只含一棵树为止。这棵树便是哈夫曼树
(3)
(4)
9
例如 : 已知权值 W={ 5, 6, 2, 9, 7 }
5 6 2 7
5 2
76 9 7
6 7
139
5 2
7
6 7
139
5 2
7
9
5 2
7
16
6 7
13
29
注意:• ① 初始森林中的n棵二叉树,每棵树有一个孤立的结点,它们既是根,又是叶子 ② n个叶子的哈夫曼树要经过n-1次合并,产生n-1个新结点。最终求得的哈夫曼树中共有2n-1个结点。 ③ 哈夫曼树是严格的二叉树,没有度数为1的分支结点。
前缀编码 在电文传输中,需要将电文中出现的每个字符进行二进制编码。在设计编码时需要遵守两个原则:
( 1)发送方传输的二进制编码,到接收方解码后必须具有唯一性,即解码结果与发送方发送的电文完全一样;
( 2)发送的二进制编码尽可能地短。下面我们介绍两种编码的方式。
1. 等长编码 这种编码方式的特点 : 每个字符的编码长度相同。 设字符集只含有 4个字符 A, B, C, D,用两位二进制表示的编码分别为 00, 01 , 10 ,11 。若现在电文为: ABACCDA ,则应发送二进制序列: 00010010101100 ,总长度为 14 位。当接收方接收到这段电文后,将按两位一段进行译码。这种编码的特点 :译码简单且具有唯一性,但编码长度并不是最短的。
2. 不等长编码 在传送电文时,为了使其二进制位数尽可能地少,可以将每个字符的编码设计为不等长的,使用频度较高的字符分配一个相对比较短的编码,使用频度较低的字符分配一个比较长的编码。例如,可以为 A, B, C, D四个字符分别分配0, 00 , 1, 01 ,并可将上述电文用二进制序列:000011010 发送,其长度只有 9个二进制位,但随之带来了一个问题,接收方接到这段电文后无法进行译码,因为无法断定前面 4个 0是 4个 A, 1个 B、2个 A,还是 2个 B,即译码不唯一,因此这种编码方法不可使用。
利用哈夫曼树可以构造一种不等长的二进制编码,并且构造所得的哈夫曼编码是一种最优前缀编码,即使所传电文的总长度最短。称为哈夫曼编码
哈夫曼编码
( 1)利用字符集中每个字符的使用频率作为权值构造一个哈夫曼树;( 2)从根结点开始,为到每个叶子结点路径上的左分支赋予 0,右分支赋予 1,并从根到叶子方向形成该叶子
结点的编码。
哈夫曼编码的构造方法 :
例如:
假设有一个电文字符集中有 8个字符,每个字符
的使用频率分别为 {0.05,0.29,0.07,0.08,0.14,0.23,
0.03,0.11},现以此为例设计哈夫曼编码。 为方便计算,将所有字符的频度乘以 100 ,使
其转换成整型数值集合,得到 {5,29,7,8,14,23,3,11} ; 哈夫曼编码设计如下图:
115 29 7 8 14 23 3
1000 1
15
58
29
0
0
0
1
1
18
42
19
0
0
0
1
1
1
00
010
0110 0111 1110 1111
110
10
哈夫曼树的存储结构 用一个大小为 2n-1 的向量来存储哈夫曼树中的结点,其存储结构为: typedef struct { //结点类型 int weight; //权值,不妨设权值均大于零 int lchild, rchild, parent;
//左右孩子及双亲指针 }HTNode, *HuffmanTree;
Typedef char **Huffmancode;
哈夫曼编码的存储结构
Weight parent lchild rchild
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
5 29 7 8 14 23 3 11
0 1 2 3 4 5 6 7
W 529
87
0 0 00 0 00 0 00 0 00 0 00 0 00 0 00 0 00 0 00 0 00 0 00 0 00 0 00 0 00 0 0
1423311
9
9
9
1 78
1010
3 415
111112
819
14
14
1213
13
1515
5294258
1002
10116
1412
130
HC
0cd 0 1 2 3 4 5 6 7
1
2
3
4
5
6
7
8
00 11
↑start
0 01 11
1 111
11
11 1
11
10
00
000
00
1
求哈夫曼编码过程的实例 : HT数组
求每个字符的 Huffman 编码是从叶子到根逆向处理 , 即从叶子出发 , 沿着双亲线索 , 回溯到根节点 , 求得各个叶子节点所表示的字符的编码 .
0
HT(‘5’).parent = ‘8’
HT(‘8’).lchild = ‘5’
HT(‘8’).parent = ‘19’
HT(‘19’).rchild = ‘8’
1HT(‘19’).parent = ‘42’
HT(‘42’).rchild = ‘19’
HT(‘42’).parent = ‘100’
HT(‘100’).ichild = ‘42’
1
0
HT(‘100’).parent = 0
‘5’: 0110
if(n<=1) return; m=2*n-1; HT=(HuffmanTree)malloc((m+1)*sizeof(HTNode));
求哈夫曼编码的算法 6.12:
for(p=HT+1,i=1;i<=n;++i,++p,++w) *p={*w,0,0,0} for(;i<=m;++i,++p) *p={0,0,0,0}
for(i=n+1 ; i<=m ; i++){ SelectMin(HT , i-1 , s1 , s2) ; HT[s1].parent=i; HT[s2].parent=i ; HTIi].1child=s1 ; HT[i].rchild=s2 ; HT[i].weight=T[s1].weight+T[s2].weight ; } // end for 创建哈夫曼树
void HuffmanCoding(HuffmanTree &HT,HuffmanCode &HC, int *w,int n){
//从叶子到根逆向求每个字符的赫夫曼编码
HC=(Huffmancode)malloc((n+1)*sizeof(char *));cd=(char *)malloc(n*sizeof(char *));cd[n-1]=“\0” ; //cd 临时存储每个叶子节点的哈夫曼编码
for(i=1;i<=n;i++){
... // 对每个叶子节点逆向求其 Haffman 编码
}
free(cd); }//Huffmancoding
//从叶子到根逆向求每个字符的赫夫曼编码 for(i=1;i<=n;i++){
start=n-1 ; for( c=i,f=HT[i].parent; f!=0; c=f,f=HT[f].parent) //i 为当前的树叶 , f 为 i 的双亲 . 每次循环沿着双亲信息逆向搜寻 , 直至根节点 if(HT[f].lchild==c cd[--start]=“0”; else cd[--start]=“1”; HC[i]=(char * )malloc((n-start)*sizeof(char)); strcpy(Hc[i] , &cd[start]) ;}
求每个字符的 Huffman 编码是从叶子到根逆向处理 , 也可以从根出发 , 遍历整棵树 , 求得各个叶子节点所表示的字符的编码 .
0
0
HT(‘100’).lchild =’42’
Cd[0] = “0”
HT(‘42’).lchild =’23’
Cd[1] = “0”
HT(‘23’).lchild =0
HT(‘23’).rchild =0
‘23’: 00
// 无栈非递归遍历 Haffman 树 , 求 Haffman 编码...
HC=(HuffmanCode)malloc(n+1)*sizeof(char *));
p = m; cdlen = 0; // 指向根节点for (i = 1; i<=m; ++i)
HT[i].weight = 0; // 遍历时用作节点的状态标志while(p) {
... // 从根开始遍历整棵树 , 求取每个节点的编码}
// 无栈非递归遍历 Haffman 树 , 求 Haffman 编码 if (HT[p].weight == 0) { // 向左 HT[p].weight = 1;
if (HT[p].lchild != 0 )
{ p = HT[p].lchild; cd[cdlen++] = “0”;}
else if (HT[p].rchild ==0 ) { // 表明该节点为叶子 ,记录编码
HC[p] = (char *)malloc((cdlen+1)*sizeof(char)); cd[cdlen] = “\0”; strcpy(HC[p],cd); } //end else if
}
// 无栈非递归遍历 Haffman 树 , 求 Haffman 编码 else if (HT[p].weight ==1 ) { // 向右 HT[p].weight = 2;
if(HT[p].rchild != 0){ p = HT[p].rchild; cd[cdlen++]=“1”;}
else {
HT[p].weight = 0;
p = HT[p].parent;
--cdlen; // 退到父亲节点 , 编码长度减 1
} //end else
// HT[p].weight = 2 时 ,
HT[p].weight = 0;
p = HT[p].parent;
--cdlen; // 退到父亲节点 , 编码长度减 1
0
0
HT(‘23’).lchild =0
HT(‘23’).rchild =0
cd: 00p = HT[’23’].parent = ’42’
cdlen = 2-1 = 2
cd: 0
再开始遍历’ 42’ 的右边
...
上机实习 2-1