第五章 二叉树及应用
description
Transcript of 第五章 二叉树及应用
第五章 二叉树及应用
一种重要的非线性结构
学习要点: 二叉树的递归概念,这与二叉树各种基本运算具有密切关联。 满二叉树和完全二叉树概念,二叉树和完全二叉树基本性质。 二叉树的顺序存储与二叉链表存储结构。 二叉树遍历的基本思想和基于递归与非递归实现算法。 线索二叉树概念,二叉树的线索化和遍历。 Huffman 树概念与基本算法; Huffman 编码和实现算法。
2
§5.1 二叉树及其基本性质
5.1.1 二叉树基本概念“ 二叉树”是一个满足下述条件的由结点组成的有限集合 E :① 当 E 为空集时,定义其为空二叉树;② 当 E 非空时,分为两种情形。● 如果 E 为单元素集合,定义其为一棵根二叉树;● 如果 E 为多于一个结点的集合,则 E 中应当具有唯一一个结点
r 称其为根结点,而集合 E’=E \{r} 也是一棵二叉树,称为 r 的子二叉树。此时,结点 r 至多只能有两棵不相交的子二叉树,并且相应子二叉树有左右之分,分别称为 r 的左子树和右子树。
3
其它一些相关概念:结点的度:结点拥有的子树棵数 结点的深度(层次):根结点看做第 0 层,其余结点的层次值为
其父结点所在层值加 1
树的度:树中所有结点度的最大值树的深度:树中最大层次数
4
根结点、叶结点、内部结点
子结点、父结点、兄弟结点、堂兄弟结点、子孙结点、祖先结点;
5.1.1 二叉树基本概念 2
1 、二叉树的特征 二叉树可以没有任何结点,即是一个空二叉树。 二叉树中每个结点至多只有两棵子树,而这两棵子树作为结点
集合互不相交; 二叉树中结点的两棵子树有左、右之分,次序不能颠倒。
2 、二叉树基本类型
5
(a)空二叉树
Φ
(b)根二叉树 (c)无右子树 (d)无左子树 (e)具左右子树
5.1.2 满二叉树和完全二叉树
1 、满二叉树 如果一棵二叉树满足下述条件,就称其为满二叉树( full
binary tree ):① 每个结点或是度数为 2 (具有两个非空子树)的结点,或是度
数为 0 的(叶)结点;② 所有叶结点都在同一层。
6
A
I
F
B C
G D E
H M J K L N O
5.1.2 满二叉树和完全二叉树 2
2 、完全二叉树若一棵二叉树满足下述条件,就称其为完全二叉树( complete
binary tree ):① 至多只有最下两层中结点的度数小于 2 ;② 最下一层的叶结点都依次排列在该层最左边位置。
7
A
I
F
B C
G D E
H J K
B
A
C
L
D
5.1.2 满二叉树和完全二叉树 2
2 、完全二叉树 2
重点理解: 满二叉树是完全二叉树,但完全二叉树却不一定是满二叉树。 空二叉树和根二叉树既是满二叉树,也是完全二叉树。 完全二叉树可以看作是在满二叉树的最下一层,从右向左连续去掉若
干个结点后得到二叉树。 完全二叉树中的一个结点如果没有左子结点,就一定没有右子结点。
8
练习:
1 、完全二叉树某结点若无右子树则定无左子树。 2 、完全二叉树某结点若无左子树则定无右子树。
9
×√
5.1.3 二叉树基本性质
性质 5-1 一棵二叉树第 i( i≥0 )层上至多只能有 个结点。
10
2i
证明:应用数学归纳法。
二叉树第 0 层有一个结点,即当 i=0 时, 2i=20=1 成立。
假设结论对第 i 层成立,即第 i 层至多只能有 2i 个结点。注意到二叉树每个结点的度最多为 2 ,第 i+1 层结点个数至多只能是第 i 层结点个数的 2 倍,即 2*2i = 2i+1 ,归纳完成,命题得证。
5.1.3 二叉树基本性质 2
11
性质 5-2 树高为 k( k≥0 )的二叉树,最多有 个结点。2k+1-1
证明: 由性质 5-1 可知在树高为 k的二叉树当中,第 0 层有 20 个结点,第 1 层有 21 个结点,第 2 层有 22 个结点,……,第 k层有 2k 个结点。由此可知,树高为 k的二叉树结点个数总数为 20 + 21 + 22
+…+ 2k 。这是一个公比为 p=2 的等比数列,前 k+1 项和 Sk+1 为:
Sk+1 =( a0 – akp) /( 1−p) = (20−2k2)/(1−2) =
(1−2k+1)/(1−2) = 2k+1−1
5.1.3 二叉树基本性质 3
性质 5-3 如果二叉树中度为 0 结点数为 n0 ,度为 2 结点数为 n2 , 则 成立。
12
n0=n2+1
证明:设结点总数 n = n0+ n1+ n2
又因为:结点数 n = 边数 +1
边数 = n1+ 2*n2
即 n0 + n1 + n2 = n1 + 2n2 + 1
所以: n0 = n2 + 1
结点数 n = 边数 +1
练习:
一棵树的度为 4, n4=2 , n3=3 , n2=4 ,求 n0 ?
13
解: 结点数 = 边数 + 1
n0+ n1+n2 +n3+n4 = n1+ 2*n2 + 3*n3 + 4*n4 + 1
n0 + 2 + 3 + 4 = 8 + 9 + 8 + 1
n0=17
练习:
设完全二叉树有 1000 个结点,求叶子结点个数?有多少个度为 1 的结点?度为 2 的结点呢?
14
解:设二叉树中叶子结点、度为 1 、度为 2 的结点数目
分别 n0 、 n1、 n2 ,
其中完全二叉树中 n1 只能为 1或 0 ,则:
n0+ n1+ n2 = 1000
n0 = n2+ 1
n1= 0 或 n1=1
n0 = 500
n1 = 1
n2 =499
复习两个概念:
( 1 )满二叉树:深度为 k 的满二叉树有 个结点。
15
2k+1-1
对满二叉树按层次从上到下,从左到右,不留一个空位进行编号,号码 1~n 。
( 2 )完全二叉树:结点数为 n 的完全二叉树上各个结点与同深度的满二叉树前 n 个相应位置结点编号一一对应。
A
I
F
B C
G D E
H J K L
1
2 3
4
9 10 11 12
5 7 6
8
5.1.3 二叉树基本性质 4
16
性质 5-4 设 BT 为具 n 个结点的完全二叉树,将 BT 所有结点按照从上到下、从左到右的顺序(二叉树的层序)进行编号。则 BT 中任意结点 N 的序号 i( 1≤i≤n )具有下述性质。( 1 )若 i=1 ,则 N为 BT 根结点, N 无父结点;( 2 )若 i>1 ,则 N 父结点序号为 (即 i 除以 2 后向下取整);( 3 )若 2i>n ,则 N 无左子树,否则其左子结点(即左子树的根结点)序号为 2i ;( 4 )若 2i+1>n ,则 N 无右子树,否则其右子结点(即右子树的根结点)序号为 2i+1 。
2/i
练习:
1、 1000 个结点的完全二叉树最大的分支结点编号为 。2、 n 个结点的完全二叉树深度为 。
17
500int( log2n)
§5.2 二叉树存储5.2.1 二叉树顺序存储 预留最大空间 深度为 k 的二叉树预留 2k+1-1 个存储单元,按编号顺序
存储,遇空结点留空位。
18
A
E
B C
D
G F
A
E
B C
D
G F
A B C ^ D E ^
1 2 3 4 5 6 7
^
8
^
9
F ^ ^ G ^ ^
10 11 12 13 14 15
5.2.1 二叉树顺序存储 2
适合满(完全)二叉树,求双亲、孩子方便 不适合深度较大、结点不多的二叉树
19
5.2.2 二叉树链式存储
1 、二叉链表存储 让一个存储结点只包含与其子树的邻接关系,那么就是
二叉树的二叉链表存储结构。
20
左子结点 右子结点
结点数据域
结点右指针域:Rchild 结点左指针域:Lchild
5.2.2 二叉树链式存储1 、二叉链表存储 2
21
A
∧
B C
D ∧
∧
E ∧
∧
F ∧
∧
G ∧
A
E
B C
F D
G
用 C语言定义二叉链表的结构类型如下: struct node {DataType data; /* 定义数据域, DataType代表实际需要的类型 */ struct node *lch; /* 定义左孩子域,指向左孩子地址 */ struct node *rch; /* 定义右孩子域,指向右孩子地址 */ }; typedef struct node bitree; /* 定义二叉树结点类型为 bitree */
1 、二叉链表存储 3
算法 5-1 创建一棵只有根结点的二叉树算法。 创建只有以 x 为根结点的二叉树 Bt, x 的数据类型为 DataType ,相
应结点的 Lchild和 Rchild域均取值 NULL ,返回指向根结点的指针。
22
00 Create_Bt(DataType x)01 {02 bitree *Bt,*ptr;03 ptr = (bitree *) malloc (sizeof(bitree)); /* 申请存储结点 */04 Bt=ptr;05 ptr->data = x;06 ptr->lch = NULL;07 ptr->rch = NULL;08 return (Bt);09 }
∧ X ∧
Bt
根结点右指针为空 根结点左指针为空
1 、二叉链表存储 4
算法 5-2 在指定左子结点处插入一个新结点。 已知二叉链表 Bt ,在指针 Parent 所指结点左子结点处插入一个数据元
素值为 x 的新结点,使之成为 Parnet 所指结点新的左子树根结点。
23
bitree *Inl_Bt(bitree *Bt, bitree *Parent, DataType x) {if (Parent == NULL) { printf (" 位置错! "); return (NULL); } ptr = (bitree *) malloc (sizeof(bitree)); /* 申请存储结点空间 */ ptr->data = x; ptr->lch = NULL; ptr->rch = NULL; if (Parent->lch == NULL) /* Parent 所指结点左子树为空 */ Parent->lch = ptr; else /* Parent 所指结点左子树非空 */
{ ptr->lch = Parent->lch; Parent->lch = ptr; } return(Bt) }
5.2.2 二叉树链式存储 2
2 、三叉链表存储 同时反映当前结点与其左子树的根结点、右子树的根结
点和与其父结点关联。
24
Lchild
数据域 左子结点指针域
Info Parent Rchild
左子结点指针域 父结点指针域
5.2.2 二叉树链式存储 2
2 、三叉链表存储 2
25
A
E
B C
F D
G
A
∧
B C
D ∧
∧
E ∧
∧
F ∧
∧
G ∧
∧
§5.3 二叉树的遍历
按照某种确定方式对二叉树进行访问,但要求二叉树中每个结点被访问一次且只被访问一次。
1 、先序、中序和后序遍历对左、右子树,限定“先访左后访右”,那么访问结点的
顺序有三种不同的组合形式: TLR、 LTR、 LRT 。通常,称 TLR 为二叉树的先序(先根)遍历, LTR 为中
序(中根)遍历, LRT 为后序(后根)遍历。
26
例子:以三种遍历方式访问如图所示的二叉树。
27
A
H
F
B C
G D E
I J K
解:先序遍历序列 A-B-D-H-E-C-F-I-G-J-K
中序遍历序列 D-H-B-E-A-I-F-C-J-G-K
后序遍历序列 H-D-E-B-I-F-J-K-G-C-A
例子:已知二叉树先序遍历序列是 A-B-C-D-E-F-G ,中序遍历序
列是 C-B-D-A-E-G-F 。由这两个序列可唯一确定一棵二叉树。
28
解:从先序遍历序列第一个结点可知二叉树根结点是 A 。由结点 A
在中序遍历序列里位置可知该根结点左子树包含结点 C-B-D ,右子树包含结点 E-G-F ,如图 5-22 所示。由中序序列片段 C-B-D 可知,B是 A 左子树根结点,再结合先序序列片段 B-C-D 可知, C和 D
分别是 B 的左右子结点。由先序序列片段 E-F-G 可知, E是 A 的右子结点,再由先序序列片段 F-G 和中序序列片段 G-F 可知, F 不能是 E 的左子结点,故只能是 E 的右子结点,并且 G是 F 的左子结点。
29
∧ E
A
先序序列
C B D A E G F
A B C D E F G
后序序列
B
C ∧
∧
D ∧
∧
F ∧
G ∧
∧
练习:
1 、已知二叉树先序遍历序列为 ABCDEFGH ,中序遍历序列为 CDBAFEHG ,试画出此二叉树。
2 、已知二叉树后序遍历序列为 DCBFHGEA ,中序遍历序列为 CDBAFEHG ,求先序遍历序列。
30
§5.3 二叉树的遍历 2
2 、基于递归遍历算法递归步骤(先序遍历):① 访问根结点;② 先序遍历访问左子二叉树;③ 先序遍历访问右子二叉树。
31
E
A
B C
D F
2 、基于递归遍历算法 2
算法 5-4 二叉树先序遍历递归算法。 已知二叉树 Bt ,对其进行先序遍历,若二叉树为空,则为空操作;否则进行如下操作:访问二叉树根结点;先序遍历二叉树的左子树;先序遍历二叉树的右子树。
32
00 Pret_Bt(bitree *Bt)01 {02 if (Bt != NULL)03 {04 printf ("%c", Bt->data); /* 访问根结点 */05 Pret_Bt(Bt->lch); /* 先序遍历左子树 */06 Pret_Bt(Bt->rch); /* 先序遍历右子树 */07 }08 }
2 、基于递归遍历算法 2
基于递归调用先序遍历 :
33
访问根结点 A
遍历左子树 遍历右子树
访问根结点 B 访问根结点 C
访问根结点 D 访问根结点 E 访问根结点 F 空,return
空,return 空,return 空,return 空,return 空,return 空,return
遍历左子树 遍历右子树 遍历右子树
遍历右子树 遍历右子树 遍历右子树 遍历左子树 遍历左子树 遍历左子树
第 2步
第 3步 第 4步
第 5步 第 6步
第 7步
第 8步
第 9步
第 11步
第 10步 第 12步 第 13步
遍历左子树
第 1步
2 、基于递归遍历算法 2
先序递归算法应用实例 : 先序建立二叉树bitree *creat()
{ bitree *t; int x;
scanf(“%d”,&x);
if (x==0) t=NULL;
else
{ t=(bitree *) malloc (sizeof(bitree));
t->data=x;
t->lch=creat(); t->rch=creat();}
return t;
}
2 、基于递归遍历算法 2
先序递归算法应用实例 : 先序建立二叉树 (续 )
主程序调用 :
main()
{ bitree *root;
root=creat();
}
例:建立如图二叉树应该如何输入?练习: 测试用例: 1 2 3 0 0 4 0 5 0 0 6 0 0
问:画出建立的二叉树?
2 、基于递归遍历算法 3
算法 5-5 二叉树中序遍历递归算法。 已知二叉树 Bt ,对其进行中序遍历,若二叉树为空,则为空操作;否则进行如下操作:中序遍历二叉树的左子树;访问二叉树根结点;中序遍历二叉树的右子树。
36
00 Indt_Bt(bitree *Bt)01 {02 if (Bt != NULL)03 {04 Indt_Bt(Bt->lch); /* 中序遍历左子树 */05 printf ("%c", Bt->data); /* 访问根结点 */06 Indt_Bt(Bt->rch); /* 中序遍历右子树 */07 }08 }
2 、基于递归遍历算法 3
基于递归调用中序遍历 :
37
访问结点 A
遍历左子树 遍历右子树
访问结点 B 访问结点 C
访问结点 D 访问结点 E 访问结点 F 空,return
空,return 空,return 空,return 空,return 空,return 空,return
遍历左子树 遍历右子树 遍历右子树
遍历右子树 遍历右子树 遍历右子树 遍历左子树 遍历左子树 遍历左子树
第 2步
第 1步 第 4步
第 3步 第 5步
第 10步
第 8步
第 7步
第 12步
第 9步 第 11步 第 13步
遍历左子树
第 6步
2 、基于递归遍历算法 4
算法 5-6 二叉树后序遍历递归算法。 已知二叉树 Bt ,对其进行后序遍历,若二叉树为空,则为空操作;否则进行如下操作:后序遍历二叉树的左子树;后序遍历二叉树的右子树;访问二叉树根结点。
38
00 Postv_Bt(bitree *Bt)01 {02 if (Bt != NULL)03 {04 Postv_Bt(Bt->lch); /* 后序遍历左子树 */05 Postv_Bt(Bt->rch); /* 后序遍历右子树 */06 printf ("%c", Bt->data); /* 访问根结点 */07 }08 }
2 、基于递归遍历算法 4
基于递归调用后序遍历 :
39
访问结点 A
遍历左子树 遍历右子树
访问结点 B 访问结点 C
访问结点 D 访问结点 E 访问结点 F 空,return
空,return 空,return 空,return 空,return 空,return 空,return
遍历左子树 遍历右子树 遍历右子树
遍历右子树 遍历右子树 遍历右子树 遍历左子树 遍历左子树 遍历左子树
第 5步
第 1步 第 4步
第 2步 第 3步
第 12步
第 8步
第 6步
第 11步
第 7步 第 9步 第 10步
遍历左子树
第 13步
§5.3 二叉树的遍历 3
3 、基于非递归遍历算法非递归遍历算法中,需要做出三条假设:① 设置一个一维数组 Ss 作为顺序栈以临时保存遍历时遇到的结点信息,其栈顶指针为 Ss-top ,初始时为 0 。
② 采用二叉链表结构保存需要遍历的二叉树,起始指针为 Bt ,每个结点包含 Data、 Lchild和 Rchild 等三个域。
③ 对结点进行的“访问”理解为将该结点的 Data域的值打印出来。
40
A
E
B C
F D
G
3 、基于非递归遍历算法 2
算法 5-7 先序遍历二叉树的非递归算法。已知二叉树 Bt ,顺序栈 Ss ,要求打印出该二叉树的先序遍历序列。
41
00 Pre_Bt(bitree *Bt)01 {02 bitree *p;03 bitree *stack[10]; /* 定义栈数组 */04 int top=-1; /* 定义栈顶下标 top 并赋初值 -1 */05 printf("\nOutput preorder:");06 p= Bt;07 while(p!=NULL || top!=-1)08 if (p!=NULL)09 {10 printf("%d ",p->data); /* 访问该结点 */11 top=top+1;stack[top]=p; /* 访问后入栈 */12 p=p->lch; /* 继续深入左孩子 */13 }14 else15 {16 p=stack[top];top=top-1; /* 遇空出栈,栈顶给 p */17 p=p->rch; /* 转向右孩子 */18 }19 }
3 、基于非递归遍历算法 2
基于非递归二叉树先序遍历:
42
步骤 指针 p 访问结点 栈 S 说明 先序序列
A→ rch=C - ∧ 访问二叉树根结点 A A 第 1次
A→ l ch=B - C A右子结点 C进栈 ∧
Ptr=B - C B为新根结点 ∧
B→rch =D B ∧ 访问 A左子树根结点 B A,B
第 2次
B→lch =nul l - C,D B右子结点 D进栈 ∧
Ptr=D - C 栈顶元素 D出栈,新根结点 ∧ 第 3次
D→ rch =nul l D C 访问 B右子树根结点 D A,B,D
D→ lch =G - C G为新根结点 ∧ 第 4次
G→rch =nul l G C 访问 D左子树根结点G A,B,D,G
G→lch =C - ∧ 栈顶结点 C出栈,新根结点 ∧
C→rch =F C C 访问 A左子树根结点 C A,B,D,G,C
第 5次
C→lch =E - F C右子结点 F进栈 ∧
Ptr=E - F E作为新根结点 ∧ 第 6次
E→rch =nul l E F 访问 C左子树根结点 E A,B,D,G,C,E
E→lch =nul l - ∧ 栈顶结点 F出栈,新根结点 ∧ 第 7次
F→rch =nul l F ∧ 访问 C右子树根结点 F A,B,D,G,C,E,F
F→lch =nul l - ∧ 栈空,do结束 ∧
3 、基于非递归遍历算法 3
中序非递归遍历算法流程 : bitree *p; 定义及初始化栈 ; p=root; while(p!=NULL || 栈不空 ) if (p!=NULL) {p 进栈; p=p->lch;} else {栈顶给 p 并出栈;输出 p; p=p-
>rch;}
3 、基于非递归遍历算法 3
算法 5-8 中序遍历二叉树的非递归算法。已知二叉树 Bt ,顺序栈 Ss ,要求打印出该二叉树的中序遍历序列。
44
00 In_Bt(bitree *Bt)01 {02 bitree *stack[10]; /* 定义栈数组 */03 int top=-1; /* 定义栈顶下标 top 并赋初值 -1 */04 bitree *ptr;05 ptr = Bt; /* ptr 是工作指针 */06 do07 {08 while (ptr != NULL) /* 一直朝左子树深入下去 */09 {10 top++; /* 调整栈顶指针 */11 stack[top] = ptr; /* ptr 所指结点进栈 */ 12 ptr = ptr->lch;13 }14 if (Ss_top !=-1 )15 {16 ptr = stack[top] ; /* 栈顶元素赋值给 ptr */17 top -- ; /* 出栈 */18 printf ("%d ", ptr->data); /* 访问该结点 */19 ptr = ptr->rch; /* 进入右子树访问 */20 }21 }while((ptr !=NULL)||(top!=-1));22 }
3 、基于非递归遍历算法 3
基于非递归二叉树中序遍历( 1-4趟):
45
步骤 工作指针 ptr 访问结点 栈 Ss 说明 中序序列
初始 A - ∧ 初始化 ∧
A→ lch=B - A 根结点 A进栈 ∧
B→ lch =null - A,B A左子结点 B进栈 ∧
- A Ptr=nul,栈顶非空,B出栈 ∧
第 1趟
B→rch=D
访问 A左子树根结点 B B
D→ lch =G - A,D B右子树根结点 D进栈 ∧
G→ lch =null - A,D,G D左子结点 G进栈 ∧
- A,D Ptr=null,栈顶非空,G出栈 ∧
第 2趟
G→rch = null
G A,D 访问 D左子树根结点 G B,G
- A Ptr=null,栈顶非空,D出栈 ∧ 第 3趟 D→ rch = null
D A 访问 B右子树根结点 D B,G,D
- ∧ Ptr=null,栈顶非空,A出栈 ∧
A ∧ 访问根结点 A B,G,D,A
第 4趟 A→ rch =C
F ∧ 访问 C右子树根结点 F B,G,D,A,E.C,F
3 、基于非递归遍历算法 3
基于非递归二叉树中序遍历( 1-4趟):
46
步骤 工作指针 ptr 访问结点 栈 Ss 说明 中序序列
C→ lch=E - C A右子结点 C进栈 ∧
E→ lch =null - C,E A左子结点 E进栈 ∧
- ∧ Ptr=null,栈顶非空,E出栈 ∧
第 5趟
E→rch = null
E C 访问 C左子树根结点 E B,G,D,A,E
- ∧ Ptr=null,栈顶非空,C出栈 ∧ 第 6趟 C→ rch =F
C ∧ 访问 A右子树根结点 C B,G,D,A,E.C
F→ lch =null - F C右子结点 F进栈 ∧
- ∧ Ptr=null,栈顶非空,F出栈 ∧
第 7趟
∧
F ∧ 访问 C右子树根结点 F B,G,D,A,E.C,F
第 8趟 ∧ - ∧ 栈空,do结束 ∧
3 、基于非递归遍历算法 4
算法 5-9 后序遍历二叉树的非递归算法(略)。已知二叉树 Bt ,顺序栈 Ss ,要求打印出该二叉树的后序遍历序列。
47
算法要点:由于后序遍历是“左、右、根”,因此在后序遍历过程中搜索到某个结点时,不是马上访问它,而是将其作为相应子树根结点保存在工作栈中,然后沿着其左子树继续深入直到“最左下”结点。完成对其左子树访问后,从工作栈顶元素中获得相应根结点信息,但仍然不能马上进行访问,而是在工作栈中对其进行第二次保存,同时对其右子树进行遍历。在访问完右子树后,从工作栈中得到根结点信息,由此实现对相应根结点访问。
3 、基于非递归遍历算法 4
基于非递归二叉树后序遍历第 1-3趟运算变化:
48
步骤 指针 ptr 访问结点 栈 stack1 栈 stack2 说明 后序序列
初始 A - ∧ ∧ 初始化 ∧
A? lch=B - A 0 根结点 A进栈 ∧
B? lch =null - A,B 0,0 A左子树根结点 B进
栈
∧
B - A 0 Ptr=null,栈非空,B出
栈
∧
B? rch=D A,B 0,1 flag=0,B二次进栈 ∧
D? lch =G - A,B,D 0,1,0 B右子结点 D进栈 ∧
G? lch =null A,B,D,G 0,1,0,0 D左子结点G进栈 ∧
- A,B,D 0, 1,0 D左子结点G出栈 ∧ G? rch =null
- A,B,D,G 0,1,0,1 Gflag=0, G二次进栈 ∧
- Ptr=null,G出栈 ∧
第 1趟
Null
G
A,B,D 0,1,0
Gflag=1,访问G G
- A,B 0, 1 Ptr=null,D出栈 ∧ D? rch =null
- A,B,D 0, 1,1 Dflag=0,D二次进栈 ∧
- Prt=null, D二次出栈 ∧
第 2趟
null
D
A,B
0,1
Dflag=1,访问 D G.D
3 、基于非递归遍历算法 4
基于非递归二叉树后序遍历第 4趟运算变化:
49
步骤 指针 ptr 访问结点 栈 stack1 栈 stack2 说明 后序序列
- ∧ 0 Ptr=null,A出栈 ∧
D? rch=C - A 1 Aflag=0,A二次进栈 ∧
C? lch=E A,C 1,0 C进栈 ∧
E? lch=null A,C,E 1,0,0 E进栈 ∧
A,C, 1,0 Ptr=null,E出栈
- A,C 1,0,1 Eflag=0,E二次进栈 ∧
第 4趟
F? rch=null
B AC 1.0 Ptr=null,栈不空,E出
栈
∧
3 、基于非递归遍历算法 4
基于非递归二叉树后序遍历第 5-7趟运算变化:
50
步骤 指针 ptr 访问结点 栈 stack1 栈 stack2 说明 后序序列
- A 1,0 Ptr=null,C出栈 ∧ C? rch=F
- A,C 1,1 Cflag=0,C二次进栈 ∧
- A,C ,F 1,1,0 F进栈 ∧ F? lch=null
- A,C 1,1 Ptr=null,F出栈 ∧
- A,C,F 1,1,1 Fflag=0,F二次进栈 ∧ null
- A,C, 1,1 Ptr=null, F二次出栈 ∧
第 5趟
F A,C, 1,1 Fflag=1,访问 F G,D,B,E,F
- A 1 Ptr=null,C出栈 G,D,B,E,F 第 6趟 null
C Cflag=1,访问 C G,D,B,E,F,C
null - ∧ ∧ Ptr=null,A出栈 ∧ 第 7趟
null A Aflag=1,访问 A G,D,B,E,F,C,A
第 8趟 null - ∧ ∧ Do结束 ∧
上机:
1 、用先序递归方法建立一棵二叉树;2 、用中序非递归方法遍历该二叉树。
51
§5.4 线索二叉树5.4.1 线索与线索二叉树 结合遍历方式特点使用这些空链域来存放相应前驱或后继信息即前驱或后继的地址。
① 当前结点左孩子域非空时,保留原指针不变;当左孩子域为空时,添加该结点在相应历序列中前驱结点地址。
② 当前结点右孩子域非空时,保留原指针不变;当右孩子域为空时,添加该结点在相应遍历序列中后继结点地址。
52
结点数据域:info
结点右指针域:rlink 结点左指针域:llink
结点左标记域:ltag 结点右标记域:rtag
5.4.1 线索与线索二叉树 2
一个中序线索二叉树的例子:
53
D 1
0
∧
A 0
0
B 0
1
∧
C 0
0
G 1
1
∧
∧
E 1
1
∧
∧
F 1
1
∧
∧
1
0
Root
5.4.2 创建线索二叉树
1 、创建线索二叉树结点
54
算法 5-10 (线索二叉树结点结构)
01 struct ThreadNode /* 线索二叉树结点的定义 */
02 {
03 DataType info;
04 struct ThreadNode llink, rlink;
05 int ltag, rtag;
06 };
07 typedef struct ThreadNode ThreadTree; /* 线索二叉树类型的定义 */
5.4.2 创建线索二叉树 2
2 、二叉树的线索化
55
在中序遍历过程中修改结点的左、右指针域,以保存当前访问结点的“前驱”和“后继”信息。遍历过程中,附设指针 pre ,并始终保持指针pre指向当前访问的、指针 p 所指结点的前驱。将给定二叉树扩充为线索二叉树的过程称为二叉树的线索化,也就是线索二叉树的创建。
线索化实际上就是在遍历过程中在当前结点的空链域中添加前驱或后继指针。为了保留遍历过程中访问结点的前驱与后继关系,需要设置一个工作指针 pre始终指向刚访问过的结点,也就是说,当指针 p指向当前访问结点时, pre 就指向 p 的前驱结点。
5.4.2 创建线索二叉树 3
3 、线索二叉树遍历
56
以下以对中序线索化链表为例讨论基于线索的二叉树中序遍历算法。此时,算法关键点有二:
① 怎样获取中序遍历的首结点?从根结点沿着左指针不断向左下搜寻,直到给定二叉树左子树的
处于“最左下”的结点,结点是“最左下”的含义是该结点再无左子结点,亦即该结点的左指针域为空。
② 怎样获取在中序线索化链表中当前结点的后继结点?如果当前结点没有无右子树,则其后继结点即为其右指针域中后
继线索所指结点;如果当前结点存在右子树,则从该右子树根结点开始沿左指针行进,直到右子树“最左下”结点,此即为当前结点的后继。
5.4.2 创建线索二叉树 3
3 、线索二叉树遍历 2
57
算法 5-12 基于中序线索二叉树中序遍历算法00 void thrInOrder(ThreadTree *root ) 01 {02 ThreadTree *p;03 p = root->llink;04 if (p==NULL)05 return ;06 while ( p->llink!=NULL && p->ltag==0 )07 p = p->llink; /* 一直到“最左下” */08 while (p!=root) 09 {10 printf("%d ",p->info);11 if ( p->rlink!=NULL && p->rtag==0 ) /* 右子树不是线索时
*/12 {13 p = p->rlink;14 while (p->llink!=NULL&&p->ltag==0)15 p = p->llink; /* 顺右子树的左子树一直向下 */16 }17 else18 p = p->rlink;19 }20 }
§5.5 Huffman 编码5.5.1 等长与非等长编码 所谓编码( code ),就是用一组不同的代码表示一个数据
对象集合中的每个元素的过程。
1 、两种基本编码类型编码三要素:① 唯一性:发送方传输编码字段,接收方解码后必须具有唯一性,
解码结果与发送原文保持相同;② 简洁性:发送的编码应该尽可能做到简洁短小,减少存储代价和提高传输效率。
③ 前缀性:两个编码字节不使用特殊标记如标点符号进行分隔,一个编码字节不能是另一个编码字节的前缀,应具有“前缀性”( Prefix Property ); 58
5.5.1 等长与非等长编码 2
2 、最优二叉树与 Huffman 树二叉树路径长度:一棵二叉树的所有从根结点到每个叶结点路径长
度之和称为该二叉树的“路径长度”。叶结点的权:赋给二叉树叶结点一个具有某种意义的实数,则称此
数为该叶结点的“权”。二叉树带权路径长度:设二叉树具有 n 个带权的叶结点,则从根结
点到各叶结点的路径长度与相应结点权值乘积之和称为该二叉树的“带权路径长度”,记为:
59
n
k 1
WPL= wk×lk
( wk 是第 k个叶结点权值, lk 是第 k个叶结点路径长度)
“Huffman 树”:由带有权值的一组相同叶结点所构成二叉树当中,称其中带权路径长度最小的二叉树为是“最优二叉树”。
5.5.2 Huffman 树构建思想设有 n 个权值 w1、 w2、 w3、…、 wn ,则可按照下述
步骤构造 Huffman 树:Step 1. 构造 n 棵只有一个根结点的二叉树森林 HT = {T1, T2,
T3,…, Tn} ,它们分别以 w1、 w2、 w3、…、 wn 作为权值。Step 2. 在 HT 集合中,选取权值最小和次小的两个根结点作为
一棵新二叉树的左子树和右子树。新二叉树根结点权值是其左、右子树根结点权值之和;
Step 3. 在 HT 集合中,删除已经取为左、右子树的原两棵二叉(根)树,并将新构成二叉树添加到 HT 中;
Step 4. 重复 Step 2和 Step 2至 HT 只剩下一棵二叉树,此即是所求 Huffman 树。
60
5.5.2 Huffman 树构建思想 2
构造具有四个分别带有权值 1、 3、 5、 7的 Huffman 树如下图所示:
61
7
1 3
9 7
1 3
5
(a) (b)
7 1 3 5
HT
4 5
HT
4
HT
9 7
1 3
5 4
HT
(c) (d)
练习:
已知有 6 个叶子,值分别为 A、 B、 C、 D、
E、 F ,它们的权值分别为 4, 9, 1, 3, 2, 1 ,试构造哈夫曼树(左权≤右权)。
62
5.5.3 基于顺序存储 Huffman 树构造
1、 Huffman 树结点结构 问题:若哈夫曼树有 n0 个叶子结点,则所有结点 个。
63
2n0-1
解:因为树中无度为 1 的结点,
由二叉树的性质 3 知: n0=n2+1
所以: n=n0+n2=n0+n0-1
=2n0-1
由于最终结果中结点个数已经确定,因此可采用顺序结构存储存放所建 Huffman 树
5.5.3 基于顺序存储 Huffman 树构造
1、 Huffman 树结点结构 2
问题:结点结构如何构造?
64
00 struct node
01 {
02 int weight;
03 int parent,lch,rch;
04 };
05 struct node Htree[MAX];
value weight lch rch parent
结点数据域
结点权值
左子结点
右子结点
父结点
5.5.3 基于顺序存储 Huffman 树构造 2
2、 Huffman 树构造算法 问题:算法如何构造 Huffman 树?
65
原理:
找两无双亲且权值最小结点合并,生成新结点,填写相关信息(两结点双亲,新结点权值,新结点左、右孩子)。
算法 5-13 基于顺序存储 Huffman 树构造算法 00 creat_huff(struct node Htree[])
01 {
02 int i,j,min1,min11,min2,min22;
03 printf("\nInput the count of leaves(<10):");
04 scanf("%d",&n); /* 读取叶子结点个数 n */
05 for (i=0;i<2*n-1;i++) /* 数组 Htree初始化 */
06 {
07 Htree[i].parent=-1;
08 Htree[i].lch=-1;
09 Htree[i].rch=-1;
10 }
11 printf("\nInput the leaves's weight:");
12 for (i=0;i<n;i++) /* 读取各个叶子的权值并赋值到 Htree 数组 */
13 scanf("%d",&Htree[i].weight); 66
算法 5-13 基于顺序存储 Huffman 树构造算法 2
14 for (i=n;i<2*n-1;i++) /* 控制 n-1 次二叉树的合并 */15 {16 min2=min1=MAX; /* min1、min2记录当前最小、次小权值 */17 min11=min22=0; /* min11、min22记录当前最小、次小权值
结点位置 */18 for (j=0;j<i;j++) /* 在 j 的范围内,找最小、次小结点 */19 {20 if (Htree[j].parent==-1)21 if (Htree[j].weight<min1) /* 如当前权值比最小结点还小 */22 {23 min22=min11;24 min2=min1; /* 最小结点信息赋给次小 */25 min11=j;26 min1=Htree[j].weight; /* 当前权值信息赋给最小 */27 }28 else
67
算法 5-13 基于顺序存储 Huffman 树构造算法 3
29 if (Htree[j].weight<min2) /* 如当前权值比次小结点小 */30 {31 min22=j;32 min2=Htree[j].weight; /* 当前权值信息赋给次小 */33 }34 }35 Htree[min11].parent=i; /* 设置最小权值结点的 parent域 */36 Htree[min22].parent=i; /* 设置次小权值结点的 parent域 */37 Htree[i].weight=Htree[min11].weight+Htree[min22].weight; /* 设置新结点的权值域 */38 Htree[i].lch=min11; /* 设置新结点的 lch域为最小结点 */39 Htree[i].rch=min22; /* 设置新结点的 rch域为次小结点 */40 }41 Htree[2*n-2].parent=-1; /* 最后一个结点为根结点, parent域为 -1
*/42 };
68
5.5.3 基于顺序存储 Huffman 树构造 3
3、 Huffman 树算法分析
69
设有叶结点相应权值分别为 1, 3, 5, 7 。按照算法构建Huffman 树过程如下图所示:
weight lch rch parent
Htree[0] 1 -1 -1 -1
Htree[1] 3 -1 -1 -1
Htree[2] 5 -1 -1 -1
Htree[3] 7 -1 -1 -1
Htree[4] -1 -1 -1
Htree[5] -1 -1 -1
Htree[6] -1 -1 -1
(a)初始时 (b)合并一次后
weight lch rch parent
Htree[0] 1 -1 -1 4
Htree[1] 3 -1 -1 4
Htree[2] 5 -1 -1 -1
Htree[3] 7 -1 -1 -1
Htree[4] 4 0 1 -1
Htree[5] -1 -1 -1
Htree[6] -1 -1 -1
5.5.3 基于顺序存储 Huffman 树构造 3
3、 Huffman 树算法分析
70
设有叶结点相应权值分别为 1, 3, 5, 7 。按照算法构建 Huffman树过程如下图所示(续):
weight lch rch parent
Htree[0] 1 -1 -1 4
Htree[1] 3 -1 -1 4
Htree[2] 5 -1 -1 5
Htree[3] 7 -1 -1 6
Htree[4] 4 0 1 5
Htree[5] 9 4 2 6
Htree[6] 16 3 5 -1
(c) 最终 (d)树形结构
9 7
1 3
5 4
16
3#
2#
1# 0#
4#
6#
5#
5.5.4 Huffman 编码
1、 Huffman 编码概念基于具体 Huffman 树的不等长编码就称为 Huffman 编码。
首先得到需要编码的字符 c1、 c2 、…、 cn 在相应数据信息中出现的频率为 p1、 p2 、…、 pn ;再以 c1、 c2 、…、 cn 作为叶结点, p1、 p2
、…、 pn 作为相应权值,按照 Huffman 算法构造一棵 Huffman 树。
其次,由构造的 Huffman 树的根结点开始,在每个左分支上标注 0 ,右分支上标注 1 。这样,从根结点到每个叶结点的路径上由 0、 1 组成的序列,就是该结点对应字符的编码。
71
5.5.4 Huffman 编码1、 Huffman 编码概念 2
编码步骤 1 :先建 Huffman 树
72
(a) (b) (c)
0.1 0. 3 0. 2 0. 3 0. 1
A B C D E
0.1 0. 1
0. 3 0. 2 0. 3
A E
B C D
0. 2
0.1 0. 1
0. 3
0. 2
0. 3
A E
B
C
D 0. 2
0. 4
5.5.4 Huffman 编码1、 Huffman 编码概念 2
编码步骤 1 :先建 Huffman 树(续)
73
(d)
0.1 0. 1
0. 3 0. 2 0. 3
A E
B C D
0. 4
0. 2
0. 6
0.1 0. 1
0. 3 0. 2 0. 3
A E
B C D
0. 4
0. 2
0. 6
1
(e)
5.5.4 Huffman 编码1、 Huffman 编码概念 2
编码步骤 2 :再编码
74
A:0 1 0
B:1 0
C:0 0
D:1 1
E:0 1 1 0.1 0. 1
0. 3 0. 2 0. 3
A E
B C D
0. 4
0. 2
0. 6
1
1
1 1
1 0
0 0
0
5.5.4 Huffman 编码 2
2、 Huffman 编码算法 问题:如何编码?
原理:从叶子结点到根结点顺着路径分支为其编码。
75
算法 5-14 Huffman 编码算法00 code_huff(struct node Htree[])01 {02 int i,j,k,pr,start;03 for (i=0;i<n;i++)04 {05 j=n-2; 06 k=i; /* k记录当前编码结点,初始为叶结点位置 */07 while(Htree[k].parent!=-1)08 {09 pr=Htree[k].parent; /* 记录 k 的父结点 */10 if(k==Htree[pr].lch)11 code[i][j]=0; /* 左分支编码为 0 */12 else13 code[i][j]=1; /* 右分支编码为 1 */14 j=j-1;15 k=pr; /* 进到路径的上一个结点,直至根结点 */16 }17 code[i][n-1]=j+1; /* 存放编码的起始位置是 j+1 */18 }
76
算法 5-14 Huffman 编码算法 2
19 printf("\n The code are:\n");
20 for (i=0;i<n;i++)
21 {
22 start=code[i][n-1]; /* 取出编码的起始位置 */
23 for (j=start;j<n-1;j++) /* 逐个输出编码 */
24 printf(" %-3d",code[i][j]);
25 printf("\n");
26 }
27 }
77
5.5.4 Huffman 编码 3
3、 Huffman 编码算法分析Huffman 编码过程如下图所示:
78
weight lch rch parent
Htree[0] 0.05 -1 -1 5
Htree[1] 0.3 -1 -1 7
Htree[2] 0.15 -1 -1 6
Htree[3] 0.4 -1 -1 7
Htree[4] 0.1 -1 -1 5
Htree[5] 0.15 0 4 6
Htree[6] 0.3 2 5 8
Htree[7] 0.7 1 3 8
Htree[8] 1 6 7 -1
0.05 0. 1
0. 3 0. 15 0. 4
A E
B C D
0. 3
0. 15
0. 7
1
3# 2# 1#
0# 4#
6#
5#
7#
8#
0 1 2 3 4 5(start)
code [0] 0 1 0 2
code [1] 1 0 3
code [2] 0 0 3
code [3] 1 1 3
code [4] 0 1 1 2
练习:
设有 7 个带权结点 A, B, C, D, E, F, G ,其权值分别为 3, 7, 8, 2, 5, 8, 4 ,试以这 7带权结点为叶子结点,构造一棵哈夫曼树(左权≤右权)。
79
练习:
在一份报文中有五种字符, A, B, C, D, E ,它们在报文中出现的频率比例为 4, 7, 5, 2, 9 ,试通过构造最优二叉树来确定每种字符的哈夫曼编码(左权≤右权)。
80
上 机:
81
理解建立 huffman 树的过程及编码过程。 1. 结点定义(需要至少 4 个域:
weight, parent, lch, rch ) 2. 定义数组 3. 输入叶子的个数 n 4. 数组初始化 5. 输入叶子的权值 6. 循环 n- 1 次做: ( 1 )找两个最小权值及其下标 ( 2 )合并,填写相关信息 7. 输出数组 8. 编码并输出
本章小结
本章基本内容
二叉树
二叉树
存储结构
满和完全二叉
树 基本性质
二叉树的
遍历
Huffman树
Huffman编码
一般二叉树
二叉链表存储
完全二叉树
顺序存储
先序、中序和
后序遍历
线索
二叉树
递归遍历算法
非递归遍历算法