基本概念
树形结构是一类重要的非线性结构。树形结构是结点之间有分支,并具有层次关系的结构。
应用
树结构在客观世界中是大量存在的,例如家谱、行政组织机构都可用树形象地表示。树在计算机领域中也有着广泛的应用,例如在编译程序中,用树来表示源程序的语法结构;在数据库系统中,可用树来组织信息;在分析算法的行为时,可用树来描述其执行过程。
树的定义
树的递归定义:
树(Tree)是n(n≥0)个结点的有限集T,T为空时称为空树,否则它满足如下两个条件:
(1)有且仅有一个特定的称为根(Root)的结点;
(2)其余的结点可分为m(m≥0)个互不相交的子集Tl,T2,…,Tm,其中每个子集本身又是一棵树,并称其为根的子树(Subree)。
树的递归定义刻画了树的固有特性:一棵非空树是由若干棵子树构成的,而子树又可由若干棵更小的子树构成。
----------------------------------------------------------------------------------------------------------------------------------------------------
二叉树是树形结构的一个重要类型。许多实际问题抽象出来的数据结构往往是二叉树的形式,即使是一般的树也能简单地转换为二叉树,而且二叉树的存储结构及其算法都较为简单,因此二叉树显得特别重要。
二叉树的递归定义
二叉树(BinaryTree)是n(n≥0)个结点的有限集,它或者是空集(n=0),或者由一个根结点及两棵互不相交的、分别称作这个根的左子树和右子树的二叉树组成。
二叉树的五种基本形态
二叉树可以是空集;根可以有空的左子树或右子树;或者左、右子树皆为空。
二叉树不是树的特例
(1)二叉树与无序树不同
二叉树中,每个结点最多只能有两棵子树,并且有左右之分。
二叉树并非是树的特殊情形,它们是两种不同的数据结构。
(2)二叉树与度数为2的有序树不同
在有序树中,虽然一个结点的孩子之间是有左右次序的,但是若该结点只有一个孩子,就无须区分其左右次序。而在二叉树中,即使是一个孩子也有左右之分。
【例】下图中(a)和(b)是两棵不同的二叉树,它们同右图中的普通树(作为有序树或无序树)很相似,但却不等同于这棵普通树。若将这三棵树均看做普通树,则它们就是相同的了。
----------------------------------------------------------------------------------------------------------------------------------------------------
二叉树性质
性质1 二叉树第i层上的结点数目最多为2i-1(i≥1)。
证明:用数学归纳法证明:
归纳基础:i=1时,有2i-1=20=1。因为第1层上只有一个根结点,所以命题成立。
归纳假设:假设对所有的j(1≤j<i)命题成立,即第j层上至多有2j-1个结点,证明j=i时命题亦成立。
归纳步骤:根据归纳假设,第i-1层上至多有2i-2个结点。由于二叉树的每个结点至多有两个孩子,故第i层上的结点数至多是第i-1层上的最大结点数的2倍。即j=i时,该层上至多有2×2i-2=2i-1个结点,故命题成立。
性质2 深度为k的二叉树至多有2k-1个结点(k≥1)。
证明:在具有相同深度的二叉树中,仅当每一层都含有最大结点数时,其树中结点数最多。因此利用性质1可得,深度为k的二叉树的结点数至多为:
20+21+…+2k-1=2k-1
故命题正确。
性质3 在任意-棵二叉树中,若终端结点的个数为n0,度为2的结点数为n2,则no=n2+1。
证明:因为二叉树中所有结点的度数均不大于2,所以结点总数(记为n)应等于0度结点数、1度结点(记为n1)和2度结点数之和:
n=no+n1+n2 (式子1)
另一方面,1度结点有一个孩子,2度结点有两个孩子,故二叉树中孩子结点总数是:
nl+2n2
树中只有根结点不是任何结点的孩子,故二叉树中的结点总数又可表示为:
n=n1+2n2+1 (式子2)
由式子1和式子2得到:
no=n2+1
满二叉树和完全二叉树是二叉树的两种特殊情形。
1、满二叉树(FullBinaryTree)
一棵深度为k且有2k-1个结点的二又树称为满二叉树。
满二叉树的特点:
(1) 每一层上的结点数都达到最大值。即对给定的高度,它是具有最多结点数的二叉树。
(2) 满二叉树中不存在度数为1的结点,每个分支结点均有两棵高度相同的子树,且树叶都在最下一层上。
【例】图(a)是一个深度为4的满二叉树。
2、完全二叉树(Complete BinaryTree)
若一棵二叉树至多只有最下面的两层上结点的度数可以小于2,并且最下一层上的结点都集中在该层最左边的若干位置上,则此二叉树称为完全二叉树。
特点:
(1) 满二叉树是完全二叉树,完全二叉树不一定是满二叉树。
(2) 在满二叉树的最下一层上,从最右边开始连续删去若干结点后得到的二叉树仍然是一棵完全二叉树。
(3) 在完全二叉树中,若某个结点没有左孩子,则它一定没有右孩子,即该结点必是叶结点。
【例】如图(c)中,结点F没有左孩子而有右孩子L,故它不是一棵完全二叉树。
【例】图(b)是一棵完全二叉树。
性质4 具有n个结点的完全二叉树的深度为
证明:设所求完全二叉树的深度为k。由完全二叉树定义可得:
深度为k得完全二叉树的前k-1层是深度为k-1的满二叉树,一共有2k-1-1个结点。
由于完全二叉树深度为k,故第k层上还有若干个结点,因此该完全二叉树的结点个数:
n>2k-1-1。
另一方面,由性质2可得:
n≤2k-1,
即:2k-1-l<n≤2k-1
由此可推出:2k-1≤n<2k,取对数后有:
k-1≤lgn<k
又因k-1和k是相邻的两个整数,故有
,
由此即得:
----------------------------------------------------------------------------------------------------------------------------------------------------
把二叉树的所有结点按照一定的线性次序存储到一片连续的存储单元中。结点在这个序列中的相互位置还能反映出结点之间的逻辑关系。
1.完全二叉树结点编号
(1) 编号办法
在一棵n个结点的完全二叉树中,从树根起,自上层到下层,每层从左至右,给所有结点编号,能得到一个反映整个二叉树结构的线性序列。
(2) 编号特点
完全二叉树中除最下面一层外,各层都充满了结点。每一层的结点个数恰好是上一层结点个数的2倍。从一个结点的编号就可推得其双亲,左、右孩子,兄弟等结点的编号。假设编号为i的结点是ki(1≤i≤n),则有:
①若i>1,则ki的双亲编号为 ;若i=1,则Ki是根结点,无双亲。
②若2i≤n,则Ki的左孩子的编号是2i;否则,Ki无左孩子,即Ki必定是叶子。因此完全二叉树中编号 的结点必定是叶结点。
③若2i+1≤n,则Ki的右孩子的编号是2i+1;否则,Ki无右孩子。
④若i为奇数且不为1,则Ki的左兄弟的编号是i-1;否则,Ki无左兄弟。
⑤若i为偶数且小于n,则Ki的右兄弟的编号是i+1;否则,Ki无右兄弟。
2.完全二叉树的顺序存储
将完全二叉树中所有结点按编号顺序依次存储在一个向量bt[0..n]中。
其中:
bt[1..n]用来存储结点
bt[0]不用或用来存储结点数目。
【例】表6.1是图6.8所示的完全二叉树的顺序存储结构,bt[0]为结点数目,b[7]的双亲、左右孩子分别是bt[3]、bt[l4]和bt[15]。
3.一般二叉树的顺序存储
(1) 具体方法
① 将一般二叉树添上一些 "虚结点",成为"完全二叉树"
② 为了用结点在向量中的相对位置来表示结点之间的逻辑关系,按完全二叉树形式给结点编号
③ 将结点按编号存入向量对应分量,其中"虚结点"用"∮"表示
(2) 优点和缺点
① 对完全二叉树而言,顺序存储结构既简单又节省存储空间。
② 一般的二叉树采用顺序存储结构时,虽然简单,但易造成存储空间的浪费。
【例】最坏的情况下,一个深度为k且只有k个结点的右单支树需要2k-1个结点的存储空间。
③在对顺序存储的二叉树做插入和删除结点操作时,要大量移动结点。
----------------------------------------------------------------------------------------------------------------------------------------------------
链式存储结构
1.结点的结构
二叉树的每个结点最多有两个孩子。用链接方式存储二叉树时,每个结点除了存储结点本身的数据外,还应设置两个指针域lchild和rchild,分别指向该结点的左孩子和右孩子。结点的结构为:
2.结点的类型说明
typedef char DataType; //用户可根据具体应用定义DataType的实际类型
typedef struct node{
DataType data;
Struct node *lchild,*rchild; //左右孩子指针
}BinTNode; //结点类型
typedef BinTNode *BinTree;//BinTree为指向BinTNode类型结点的指针类型
3.二叉链表(二叉树的常用链式存储结构)
在一棵二叉树中,所有类型为BinTNode的结点,再加上一个指向开始结点(即根结点)的BinTree型头指针(即根指针)root,就构成了二叉树的链式存储结构,并将其称为二叉链表。
【例】下面左图所示二叉树的二叉链表如下面中图所示。
① 一个二叉链表由根指针root惟一确定。若二叉树为空,则root=NULL;若结点的某个孩子不存在,则相应的指针为空。
② 具有n个结点的二叉链表中,共有2n个指针域。其中只有n-1个用来指示结点的左、右孩子,其余的n+1个指针域为空。
4.带双亲指针的二叉链表
经常要在二叉树中寻找某结点的双亲时,可在每个结点上再加一个指向其双亲的指针parent,形成一个带双亲指针的二叉链表。
【例】上面右图是上面左图所示的二叉树的带双亲指针的二叉链表。
----------------------------------------------------------------------------------------------------------------------------------------------------
所谓遍历(Traversal)是指沿着某条搜索路线,依次对树中每个结点均做一次且仅做一次访问。访问结点所做的操作依赖于具体的应用问题。遍历是二叉树上最重要的运算之一,是二叉树上进行其它运算之基础。
1.遍历方案
从二叉树的递归定义可知,一棵非空的二叉树由根结点及左、右子树这三个基本部分组成。因此,在任一给定结点上,可以按某种次序执行三个操作:
(1)访问结点本身(N),
(2)遍历该结点的左子树(L),
(3)遍历该结点的右子树(R)。
以上三种操作有六种执行次序:
NLR、LNR、LRN、NRL、RNL、RLN。
前三种次序与后三种次序对称,故只讨论先左后右的前三种次序。
2.三种遍历的命名
根据访问结点操作发生位置命名:
① NLR:前序遍历(PreorderTraversal亦称(先序遍历))
——访问结点的操作发生在遍历其左右子树之前。
② LNR:中序遍历(InorderTraversal)
——访问结点的操作发生在遍历其左右子树之中(间)。
③ LRN:后序遍历(PostorderTraversal)
——访问结点的操作发生在遍历其左右子树之后。
由于被访问的结点必是某子树的根,所以N(Node)、L(Left subtlee)和R(Right subtree)又可解释为根、根的左子树和根的右子树。NLR、LNR和LRN分别又称为先根遍历、中根遍历和后根遍历。
遍历算法
1.中序遍历的递归算法定义:
若二叉树非空,则依次执行如下操作:
(1)遍历左子树;
(2)访问根结点;
(3)遍历右子树。
2.先序遍历的递归算法定义:
若二叉树非空,则依次执行如下操作:
(1) 访问根结点;
(2) 遍历左子树;
(3) 遍历右子树。
3.后序遍历得递归算法定义:
若二叉树非空,则依次执行如下操作:
(1)遍历左子树;
(2)遍历右子树;
(3)访问根结点。
4.中序遍历的算法实现
用二叉链表做为存储结构,中序遍历算法可描述为:
void InOrder(BinTree T)
{ //算法里①~⑥是为了说明执行过程加入的标号
① if(T) { // 如果二叉树非空
② InOrder(T->lchild);
③ printf("%c",T->data); // 访问结点
④ InOrder(T->rchild);
⑤ }
⑥ } // InOrder
遍历序列
1.遍历二叉树的执行踪迹
三种递归遍历算法的搜索路线相同(如下图虚线所示)。
具体线路为:从根结点出发,逆时针沿着二叉树外缘移动,对每个结点均途径三次,最后回到根结点。
2.遍历序列
(1) 中序序列
中序遍历二叉树时,对结点的访问次序为中序序列
【例】中序遍历上图所示的二叉树时,得到的中序序列为:
D B A E C F
(2) 先序序列
先序遍历二叉树时,对结点的访问次序为先序序列
【例】先序遍历上图所示的二叉树时,得到的先序序列为:
A B D C E F
(3) 后序序列
后序遍历二叉树时,对结点的访问次序为后序序列
【例】后序遍历上图所示的二叉树时,得到的后序序列为:
D B E F C A
(1) 在搜索路线中,若访问结点均是第一次经过结点时进行的,则是前序遍历;若访问结点均是在第二次(或第三次)经过结点时进行的,则是中序遍历(或后序遍历)。只要将搜索路线上所有在第一次、第二次和第三次经过的结点分别列表,即可分别得到该二叉树的前序序列、中序序列和后序序列。
(2) 上述三种序列都是线性序列,有且仅有一个开始结点和一个终端结点,其余结点都有且仅有一个前趋结点和一个后继结点。为了区别于树形结构中前趋(即双亲)结点和后继(即孩子)结点的概念,对上述三种线性序列,要在某结点的前趋和后继之前冠以其遍历次序名称。
【例】上图所示的二叉树中结点C,其前序前趋结点是D,前序后继结点是E;中序前趋结点是E,中序后继结点是F;后序前趋结点是F,后序后继结点是A。但是就该树的逻辑结构而言,C的前趋结点是A,后继结点是E和F。
----------------------------------------------------------------------------------------------------------------------------------------------------
二叉链表的构造
1. 基本思想
基于先序遍历的构造,即以二叉树的先序序列为输入构造。先序序列中必须加入虚结点以示空指针的位置。
【例】建立上图所示二叉树,其输入的先序序列是:ABD∮∮CE∮∮F∮∮。
2. 构造算法
假设虚结点输入时以空格字符表示,相应的构造算法为:
void CreateBinTree (BinTree *T)
{ //构造二叉链表。T是指向根指针的指针,故修改*T就修改了实参(根指针)本身
char ch;
if((ch=getchar())=='') *T=NULL; //读人空格,将相应指针置空
else{ //读人非空格
*T=(BinTNode *)malloc(sizeof(BinTNode)); //生成结点
(*T)->data=ch;
CreateBinTree(&(*T)->lchild); //构造左子树
CreateBinTree(&(*T)->rchild); //构造右子树
}
}
调用该算法时,应将待建立的二叉链表的根指针的地址作为实参。
【例】设root是一根指针(即它的类型是BinTree),则调用CreateBinTree(&root)后root就指向了已构造好的二叉链表的根结点。
----------------------------------------------------------------------------------------------------------------------------------------------------
线索二叉树
1.定义
n个结点的二叉链表中含有n+1个空指针域。利用二叉链表中的空指针域,存放指向结点在某种遍历次序下的前趋和后继结点的指针(这种附加的指针称为"线索")。这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种。
线索链表解决了二叉链表找左、右孩子困难的问题,出现了无法直接找到该结点在某种遍历序列中的前趋和后继结点的问题。
2.线索链表的结点结构
线索链表中的结点结构为:
其中:
ltag和rtag是增加的两个标志域,用来区分结点的左、右指针域是指向其左、右孩子的指针,还是指向其前趋或后继的线索。
3.线索二叉树的表示
【例】下面(a)图所示的中序线索二叉树,其线索链表如下面(b)图所示。
图中的实线表示指针,虚线表示线索。
结点C的左线索为空,表示C是中序序列的开始结点,无前趋;
结点E的右线索为空,表示E是中序序列的终端结点,无后继。
线索二叉树中,一个结点是叶结点的充要条件为:左、右标志均是1。
二叉树的线索化
1.线索化和线索化实质
将二叉树变为线索二叉树的过程称为线索化。
按某种次序将二叉树线索化的实质是:按该次序遍历二叉树,在遍历过程中用线索取代空指针。
2.二叉树的中序线索化
(1)分析
算法与中序遍历算法类似。只需要将遍历算法中访问结点的操作具体化为建立正在访问的结点与其非空中序前趋结点间线索。
该算法应附设一个指针pre始终指向刚刚访问过的结点(pre的初值应为NULL),而指针p指示当前正在访问的结点。结点*pre是结点*p的前趋,而*p是*pre的后继。
(2)将二叉树按中序线索化的算法
typedef enum { Link,Thread} PointerTag; //枚举值Link和Thread分别为0,1
typedef struct node{
DataType data;
PointerTag ltag,rtag; //左右标志
Struct node *lchild,*rchild;
} BinThrNode;//线索二叉树的结点类型
typedef BinThrNode *BinThrTree;
BinThrNode *pre=NULL; //全局量
void lnorderThreading(BinThrTree p)
{//将二叉树p中序线索化
if(p){ //p非空时,当前访问结点是*p
InorderThreading(p->lchild); //左子树线索化
//以下直至右子树线索化之前相当于遍历算法中访问结点的操作
p->ltag=(p->lchild)?Link:Thread; //左指针非空时左标志为Link
//(即0),否则为Thread(即1)
p->rtag=(p->rchild)?Link:Thread;
*(pre){ //若*p的前趋*pre存在
if(pre->rtag==Thread) //若*p的前趋右标志为线索
pre->rchild=p; //令*pre的右线索指向中序后继
if(p->ltag==Thread) //*p的左标志为线索
p->lchild=pre; //令*p的左线索指向中序前趋
} // 完成处理*pre的线索
pre=p; //令pre是下一访问结点的中序前趋
InorderThreeding(p->rehild); //右子树线索化
}//endif
} //InorderThreading
(3)算法分析
和中序遍历算法一样,递归过程中对每结点仅做一次访问。因此对于n个结点的二叉树,算法的时间复杂度亦为O(n)。
3.二叉树的前序线索化和后序线索化
线索二叉树的运算
1. 查找某结点*p在指定次序下的前趋和后继结点
(1)在中序线索二叉树中,查找结点*p的中序后继结点
在中序线索二叉树中,查找结点*p的中序后继结点分两种情形:
① 若*p的右子树空(即p->rtag为Thread),则p->rchild为右线索,直接指向*p的中序后继。【例】下图的中序线索二叉树中,结点D的中序后继是A。
② 若*p的右子树非空(即p->rtag为Link),则*p的中序后继必是其右子树中第一个中序遍历到的结点。也就是从*p的右孩子开始,沿该孩子的左链往下查找,直至找到一个没有左孩子的结点为止,该结点是*p的右子树中"最左下"的结点,即*P的中序后继结点。
【例】上图的中序线索二叉树中:
A的中序后继是F,它有右孩子;
F的中序后继是H,它无右孩子;
B的中序后继是D,它是B的右孩子。
----------------------------------------------------------------------------------------------------------------------------------------------------