2007.9
第六章 树与二叉树
6.1 树的定义和基本概念6.2 二叉树 6.2.1 二叉树的定义和基本术语 6.2.2 二叉树的性质 6.2.3 二叉树的存储结构6.3 遍历二叉树 6.3.1 遍历二叉树 6.3.2 线索二叉树6.4 树和森林 6.4.1 树的存储结构 6.4.2 森林与二叉树的转换 6.4.3 树和森林的遍历6.5 哈夫曼树及哈夫曼编码
6.1 树的定义和基本概念
树的定义 树 (Tree) 是 n(n>=0) 个结点的有限集 T , T 为空时称为空树,否则它满足如下两个条件: (1) 有且仅有一个特定的称为根 (Root) 的结点; (2) 其余的结点可分为 m(m>=0) 个互不相交的子集
T1,T2,T3…Tm ,其中每个子集又是一棵树,并称其为子树(Subtree) 。
JI
A
CB D
HGFE
K L M
树的基本概念结点( node )根结点( root )、叶子 / 叶结点( leaf )孩子结点( child )、双亲结点( parent )、兄弟( brother )度( degree )
结点的度树的度: max( 结点的度 )
树的深度 / 高度( depth ): max (结点的层次)有序树 / 无序树森林( forest )
6.2 二叉树
6.2.1 二叉树的定义和基本术语二叉树的定义
是一颗空树或是由根及两颗不相交的左子树、右子树构成,并且左、右子树本身也是二叉树。
(a)
空二叉树
A A
B
A
B
A
CB
(b)根和空的左右子树 (c)根和左子树 (d)根和右子树
(e)根和左右子树
说明1 )二叉树中每个结点最多有两颗子树;二叉树每个结点度小于等于 2;
2 )左、右子树不能颠倒——有序树 ;
3 )二叉树的定义是递归结构,在二叉树的定义中又用到了二叉树的概念 ;
二叉树的逻辑结构 A
F
G
E D
C B
A
G
E D
B C F
φ二叉树的五种基本形态
两种特殊的二叉树1) 满二叉树:如果深度为 k 的二叉树,有 2k-1 个结点则称为满二叉树;
A
G
F E D
C B A
C B
K=3 的满二叉树K=2 的满二叉树
5.2.1 二叉树的概念
2) 完全二叉树:如果一颗二叉树只有最下一层结点数可能未达到最大,并且最下层结点都集中在该层的最左端,则称为完全二叉树; A
E D C B
G
A
E D C B
(a)
(c)
(b)
(a)、 (b) 完全二叉树(c) 不是完全二叉树
A
F E D
C B
5.2.1 二叉树的概念
性质 1 在二叉树的第 i 层上最多有 2i-1 个结点性质 2 深度为 k 的二叉树最多有 2k-1 个结点性质 3 设二叉树叶子结点数为 n0 ,度为 2 的结点 n2 ,则n0 = n2 +1
A
F
G
E D
C B
6.2.2 二叉树的性质
下面是两个关于完全二叉树的性质性质 4 具有 n 个结点的完全二叉树的深度为: log2 n +1.
对完全二叉树的结点编号:从上到下,每一层从左到右 A
F E D
C B1
2 34 5 6
性质 5 :在完全二叉树中编号为 i 的结点1 )若有左孩子,则左孩编号为 2i2 )若有右孩子,则右孩子结点编号为2i+13 )若有双亲,则双亲结点编号为 i/2
6.2.3 二叉树的存储结构 1 二叉树的顺序结构 适于满二叉树或完全二叉树的存储。
A
F E D
C B
0 1 2 3 4 5 6 n-1A B C D E F
逻辑结构逻辑结构
存储结构存储结构
A
F G
E D C B
1
6 7
24 5
3
8 10
A
F
G
E D
C B
9
0 1 2 3 4 5 6 7 8 9 10 m-1
A B C D E 0 F 0 0 G
二叉树的链式存储结构 A
F E D
C B
typedef struct bnode{ char data; struct node *lchild,*rchild;}btree;
二叉链表的类型定义( C 语言描述)lchild
data rchild
逻辑结构逻辑结构 存储结构存储结构 A
B C
D
F
E
A
F E D
C B
3 三叉链表 三叉链表中每个结点包含四个域:数据域、双亲指针域、左指针域、右指针域
Struct node{ int data; struct node *lch,*rch,*parent;};
A
B
D
F
E
C
lchild data rchild
parent
6.3 遍历( Traversal )与线索化
6.3.1 二叉树的遍历
本节内容: 遍历的基本概念 各种遍历方法的思想 各种遍历方法的递归算法和非递归算法 以先序输入方式建立二叉树的算法。
重点、难点 各种遍历方法的思想 各种遍历方法的非递归算法
一 . 遍历的基本概念遍历是各种数据结构最基本的操作,许多其他的操作可以在遍历基础上实现。二叉树的遍历:沿某条路径周游二叉树,对树中的每个结点访问一次且仅访问一次。
“ 访问”的含义很广,可以是对结点的各种处理,如修改结点数据、输出结点数据。
两种基本策略:广度遍历深度遍历
如何访问二叉树的每个结点, 而且每个结点仅被访问一次?
A
F E D
C B
广度遍历策略层次遍历方法:从上到下、从左到右访问各结点
适用于顺序存储结构 A
F E D
C B
存储结构0 1 2 3 4 5 6 7 8A B C D φ E φ φ F
遍历结果: A B C D E F
深度遍历策略二叉树由根、左子树、右子树三部分组成二叉树的遍历可以分解为:
访问根( D )遍历左子树( L )遍历右子树( R )
有六种遍历方法:D L R , L D R , L R D ,D R L , R D L , R L D约定先左后右 , 有三种遍历方法,分别称为先序遍历、中序遍历、后序遍历
A
F G
E D C B
二、各种遍历的思想1 . 先序遍历( DLR )2 . 中序遍历( LDR )3. 后序遍历( RDL )
例:先序遍历右图所示的二叉树,所得先序遍历序列: A, B, D, E, G, C, F
1 . 先序遍历( DLR )先序遍历( DLR )思想若二叉树非空,则依次进行以下操作 ( 1 )访问根结点; ( 2 )先序遍历左子树; ( 3 )先序遍历右子树;
A
F G
E D C B
Flash 演示
例:中序遍历右图所示的二叉树,所得中序遍历序列: D,B,G,E,A,C,F
2. 中序遍历( LDR )中序遍历( LDR )思想若二叉树非空,则依次进行以下操作 ( 1 )中序遍历左子树; ( 2 )访问根结点; ( 3 )中序遍历右子树;
A
F G
E D C B
Flash 演示
3. 后序遍历( LRD )
例:后序遍历右图所示的二叉树,所得后序遍历序列: D, G, E, B, F, C,A
后序遍历( LRD )思想若二叉树非空,则依次进行以下操作 ( 1 )后序遍历左子树; ( 2 )后序遍历右子树; ( 3 )访问根结点; A
F G
E D C B
Flash 演示
软件水平考试有关试题
2007-1
2002假设一棵二叉树的后序遍历序列为D G J H E B I F C A ,中序遍历序列为D B G E H J A C I F ,则其前序遍历序列为 。 A ) A B C D E F G H I J B ) A B D E G H J C F I C ) A B D E G H J F I C D ) A B D E G J H C F I
A
B
D E
G H
J
C
F
I
2006-2 若某二叉树的先序遍历序列和中序遍历序列分别为 PBECD 、 BEPCD ,则该二叉树的后序遍历序列为 ( 38 ) 。 A. PBCDE B. DECBP C. EBDCP D. EBPDC
1999 试题 2二叉树的查找有深度优先和广度优先二类,深度优先包括
_C_ 。当一棵二叉树的前序序列和中序序列分别是 H G E D B F C A 和 E G B D H F A C 时,其后序序列必是 _D_, 层次序列为 _E_.
C: (1) 前序遍历 后序遍历 中序遍历 (2) 前序遍历 后序遍历 层次遍历 (3) 前序遍历 中序遍历 层次遍历 (4) 中序遍历 后序遍历 层次遍历D: (1) B D E A G F H C (2) E B D G A C F H (3) H G F E D C B A (4) H F G D E A B C
H
G
E D
B
F
C
AE: (1) B D E A C G F H (2) E B D G A C F H (3) H G F E D C B A (4) H F G C D E A B
软件设计师 2004 上半年设结点 x 和 y 是二叉树中任意的两个结点,在该二叉树的先根遍历序列中 x 在 y 之前,而在其后根遍历序列中 x 在 y 之后,则 x 和 y 的关系是
__(10)__ 。A . x 是 y 的左兄弟 B . x 是 y 的右兄弟C . x 是 y 的祖先 D . x 是 y 的后裔
三、遍历的递归算法1 . 先序遍历( DLR )2 . 中序遍历( LDR )3. 后序遍历( RDL )
先序遍历( DLR )的递归算法void preorder ( btree *t ){
若二叉树 t 非空,则:访问根结点 t前序遍历 t 的左子树前序遍历 t 的右子树
if (t) { putchar(t->data); preorder(t->lchild);preorder(t->rchild);
}}
中序遍历、后序遍历的递归算法中序遍历void inorder ( bitree *t){ if ( t ) { inorder(t->lchild); putchar(t->data); inorder(t->rchild); }}
后序遍历void postorder ( bitree *t){ if ( t ) { postorder(t->lchild); postorder(t->rchild); putchar(t->data); }}
四、遍历的非递归算法提问:
二叉树遍历算法的递归版本简洁而又好懂,既然有这么好的算法,为何还要去改成非递归版本呢?回答:
因为在有些场和,出于性能和条件的考虑,无法使用递归机制,这就必须借助非递归来实现相应的递归算法。
实现递归与非递归的换转原理分析1.函数的调用、返回机制
一个函数在调用另一个函数之前,要作三件事:a)将实在参数,返回地址等信息传递给被调用函数保存 ; b) 为被调用函数的局部变量分配存储区 ;c)将控制转移到被调函数的入口。
从被调用函数返回调用函数之前,要做三件事:a)保存被调函数的计算结果 ;b)释放被调函数的数据区 ;c) 依照被调函数保存的返回地址将控制转移到调用函数。
两个问题:数据保存在哪里?(不论是变量还是地址,本质上来说都是”数据” )如何实现“控制转移”?
结论 1 :递归调用时数据都是保存在栈中的,有多少个数据需要保存就要设置多少个栈,而且最重要的一点是:控制所有这些栈的栈顶指针都是相同的,否则无法实现同步。 因此:在非递归算法中定义相应的栈来做为辅助的存储空间。
结论 2 :递归调用时是通过函数的入口地址实现“控制转移”的。因此:在非递归中,程序需要知道到底要转移到哪个部分继续执行,即找到控制变换的因素或条件。
例如:二叉树的遍历二叉树的三种遍历方式,抽象出来只有三种操作:
访问当前结点访 问左子树访问右子树。
这三种操作的顺序不同,遍历方式也不同。如果我们再抽象一点,对这三种操作再进行一个概括,可以得到:
a) 访问当前结点:对目前的 数据进行一些处理 ;b) 访问左子树:变换当前的数据以进行下一次处理 ;c) 访问右子树:再次变换当前的数据以进行下一次处理 (与访问左子树所不同的方 式 )。
先序遍历的非递归算法void preorder_norecursive (bitree
*t) { bitree *s[20], *p=t; int top=0; while (p || top) if (p) { visit(p);
++top; s[top]=p; p=p->lchild; } else{ p=s[top];
--top; p=p->rchild; }
}
当(当前根 p不为空) /* 访问根 */ 访问当前结点 p; /* 访问左子树:变换当前的数据以进行下一次处理 */ Push(S,p); 找 P的左儿子;否则, /* 访问右子树:再次变换当前的数据以进行下一次处理 (与访问左子树所不同的方 式 )。 */ P= Pop(S); 找 P的右儿子;
中序遍历的非递归算法void inorder_norecursive
(bitree *t) { bitree * s[MAX], *p; int top=0; p=t; while (p || top!=0){ if(p) { ++top; s[top]=p;
p=p->lchild; }
else { p=s[top]; --top; visit(p); p=p->rchild; }
} }
当(当前根 p不为空)/* 访问左子树: */ Push(S,p); 找 P的左儿子;否则,
/* 访问根 */ P= Pop(S); 访问当前结点 p; /* 访问右子树 */ 找 P的右儿子;
五 . 二叉树的建立按先序遍历的方式输入结点采用先序递归的方式建立的算法思想若要求建立二叉树,则依次进行以下操作 ( 1 )生成根结点; ( 2 )先序建立左子树; ( 3 )先序建立右子树;
bitree *creat_bintree(bitree *t){ char ch;
printf("\n enter ch in preorder, 1 for no child:");
ch=getchar(); getchar();
if ( ch=='1') t=NULL; else { t = (bitree *)malloc(NodeLen);
t->data=ch; t->lchild = creat_bintree( t->lchild ); t->rchild = creat_bintree( t->rchild ); }
return(t);}
Threaded Binary Tree
6.3.2 线索二叉树
问题的提出: 当以二叉链表作为存储结构时 , 只能找到结点的左右孩子的信息 , 而不能在结点的任一序列的前驱与后继信息 , 这种信息只有在遍历的动态过程中才能得到。
解决办法: 利用二叉链表中的空指针 增加标志域
效果: 为二叉树建立了一个双向线索链表
一 . 线索二叉树的结构结点结构
按遍历序列分为:先序线索二叉树中序线索二叉树后序线索二叉树
ltag data rtaglchild rchild
指向结点的左儿子
指向结点的前驱
lchild
lchildltag
,0
,1
指向结点的右儿子
指向结点的后继
rchild
rchildrtag
,0
,1
先序线索二叉树 A
F E D
C B
0 A 0
0 B 1 0 C 1
1 D 0 1 E 1
1 F 1
先序遍历序列: ABDFCE
Null
0 A 0
0 B 1 0 C 1
1 D 0 1 E 1
1 F 1
中序线索二叉树 A
F E D
C B
中序遍历序列: DFBAEC
Null
Null
0 A 0
0 B 1 0 C 1
1 D 0 1 E 1
1 F 1
后序线索二叉树 A
F E D
C B
后序遍历序列: FDBECANull
线索二叉链表的类型定义( C 语言描述)
typedef struct node{ char data; struct node *lchild,*rchild; int ltag , rtag;}thbitree;
ltag
data
rtag
lchild
rchild
二 . 有关算法均以中序线索二叉树为例:二叉树的线索化
线索化 :在遍历的过程中修改空指针域和标志域。在线索二叉树中查找结点的前驱、后继
1. 中序遍历线索化二叉树thbitree *pre=Null;/* pre 指向当前结点的前趋 , 初值为空 */Void Inthread( thbitree *p){ if (p !=Null) {Inthread(p->lchild);/* 左子树线索化 */ if (p->lchild==Null) p->ltag=1; /* 修改标志域 */ if (p->rchild==Null) p->rtag=1; if (pre!=Null){ /* 修改空指针域 */ if (pre->rtag==1) pre->rchild=p; if (p->ltag==1) p->lchild=pre; } pre=p; /* pre 指向当前结点 */ Inthread(p->rchild); /* 右子树线索化 */ }/*end-of-if-p*/}
2. 在中序线索二叉树中查找结点的直接前趋若 p->lchild==Null, 则 p->lchild 指向p的直接前趋(按定义)若 p->lchild !=Null, 则从 p的 lchild 出发,找“最右下”的结点
p0
0
0
0
1
void pre_Inorder_ThreadTree(thbitree *p, thbitree *pre){ thbitree *q; if (p->ltag = =1) pre=p->lchild; else{ q=p->lchild; while(q->rtag = = 0) q=q->rchild; pre = q; } }
q
q
3. 在中序线索二叉树中查找结点的直接后继若 p->rchild==Null, 则 p->rchild 指向p的直接后继(按定义)若 p->rlchild !=Null, 则从 p的 rchild 出发,找“最左下”的结点
p
0
0
0
0
1
void next_Inorder_ThreadTree(thbitree *p,thbitree *next){ thbitree *q; if (p->rtag = =1) next=p->rchild; else{ q=p->rchild; while(q->ltag = = 0) q=q->lchild; next = q; } }
q
q
6.4 树和森林
6.4.1 树的结构