课件下载

Download Report

Transcript 课件下载

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