引言
二叉树是一种非常重要的非线性结构,许多实际问题抽象出来的数据结构往往都是二叉树的形式.与树相比,二叉树更加规范并更具确定性,并且实现二叉树的存储结构及其算法都较为简单,因此二叉树就显得格外重要.
在计算机科学中,二叉树(Binary tree)是每个节点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)。二叉树常被用于实现二叉查找树和二元堆积。
一.二叉树的定义
I. 定义:
二叉树是n(n>=0)个节点的有限集合,它或者是空树(n=0),或者是由一个根节点及两颗互不相交,分别称作该根节点的"左子树"和"右子树"的二叉树构成.
II. 特点:
二叉树与树的定义一样,都是递归的.
并且,二叉树具有如下两个特点:
1. 二叉树不存在度大于2的结点.
2. 二叉树每个节点至多有两棵子树且有左,右之分,次序不能颠倒.
III. 区别:
与树的区别:
1. 树的节点个数至少为1,而二叉树的节点个数可以为0;
2. 树中节点的最大度数没有限制,而二叉树节点的最大度数为2;
3. 树的节点无左、右之分,而二叉树的节点有左、右之分。(二叉树任何一个节点的子树都要区分左右子数,即使这个节点只有一棵子树时也要明确指出它是左子树还是右子树.)
IV. 形态:
根据二叉树的定义,二叉树有如图的五种形态:
a. 为空二叉树(用Φ表示∅Ø )
b. 为只有一个根节点而无子树的二叉树
c. 为只有左子树而无右子树的二叉树
d. 为只有右子树而无左子树的二叉树
e. 为左右子树均非空的二叉树
v. 类型
1). 满二叉树
我们将称具有如下性质的二叉树为满二叉树:
1. 不存在度为1的节点,即所有分支节点都有左子树和右子树.
2. 所有叶子节点都在同一层上.
图a即为满二叉树,图b则不是满二叉树,因为其叶子节点不在同一层上.
一棵深度为k,且有 2^(k-1)
个节点的二叉树,称为满二叉树(Full Binary Tree)。这种树的特点是每一层上的节点数都是最大节点数。
2). 完全二叉树
而在一棵二叉树中,除最后一层外,若其余层都是满的,并且最后一层或者是满的,或者是在右边缺少连续若干节点,则此二叉树为完全二叉树(Complete Binary Tree)。
对一棵具有n个节点的二叉树,将树中的节点按自上而下,从左向右的顺序进行编号,如果编号i(1 ≤ i ≤ n)的节点与满二叉树中的编号为i的节点在二叉树中的位置相同,则这棵二叉树成为完全二叉树.
二.二叉树的性质
(向下取整的运算称为Floor,用数学符号⌊⌋表示,
⌊x⌋
表示不大于x的最大整数,如⌊3.7⌋=7
;
与之相对的,向上取整的运算称为Ceiling,用数学符号⌈⌉表示。⌈x⌉
表示不小于x的最小整数,⌈3.7⌉=4
) ;
性质1.
非空二叉树的第i
层上最多有2^(i-1)
个节点(i≥1);性质2.
深度为k
的二叉树最多有2^k-1
个结点(k≥1),最少有h个结点;性质3.
对于任意一棵非空二叉树,如果其叶结点(度为0)数为n0
,而度数为2的结点总数为n2
,则n0=n2+1
;性质4.
具有n
个结点的完全二叉树的深度为⌊log2n⌋+1
;性质5.
有n
个结点的完全二叉树各结点如果按层次自上而下且每层从左到右用顺序方式存储,从1
到n
进行编号,则对任意序号为i
的结点有如下关系:
1. 如果i>1
,则其父结点的编号为⌊i/2⌋
;若i=1,则i为根节点序号;
2. 如果2i ≤ n
,则其左儿子(即左子树的根结点)的编号为2i
;否则i
无左儿子;
3. 如果2i+1 ≤ n
,则其右儿子的结点编号为2i+1
;否则i
无右儿子。性质6.
给定n
个节点,能构成h(n)
种不同的二叉树。
h(n)
为卡特兰数的第n
项。
h(n)=C(2*n,n)/(n+1)
。性质7.
设有i
个枝点,i
为所有枝点的道路长度总和,j
为叶的道路长度总和j=i+2i
三.二叉树的存储
二叉树的存储方法有以下几种。
- 1. 顺序存储
二叉树可以用数组或链接串列来存储,而且如果这是满二叉树,这种方法不会浪费空间。
/* 二叉树的顺序存储表示 */
#define MAX_TREE_SIZE 100 /* 二叉树的最大节点数 */
typedef TElemType SqBiTree[MAX_TREE_SIZE]; /* 0号单元存储根节点 */
typedef struct
{
int level,order; /* 节点的层,本层序号(按满二叉树计算) */
}position;
用这种紧凑排列,如果一个节点的索引为i
,它的子节点能在索引2i+1
和2i+2
找到,并且它的父节点(如果有)能在索引floor((i-1)/2)
找到(假设根节点的索引为0)。
这种方法更有利于紧凑存储和更好的访问的局部性,特别是在[前序遍历]中。然而,它需要连续的存储空间,这样在存储高度为h的n个结点组成的一般普通树时将会浪费很多空间。一种最极坏的情况下如果深度为h的二叉树每个节点只有右孩子需要占用2的h次幂减1,而实际却只有h个结点,空间的浪费太大,这是顺序存储结构的一大缺点。
将数组下标作为结点名称(编号),就可将二叉树中所有结点的标号存储在一维数组中。
在二叉树的这种表示方式下,各结点之间的逻辑关系是隐含表示的。近似满二叉树中,除最下面一层外,各层都充满了结点。可能除最底层外,每一层的结点个数恰好是上一层结点个数的2倍。因此,从一个结点的编号就可推知其父亲,左、右儿子,和兄弟等结点的编号。例如,对于结点i
我们有:
- 仅当i=1时,结点i为根结点;
- 当i>1时,结点i的父结点为i/2;
- 结点i的左儿子结点为2i;
- 结点i的右儿子结点为2i+1;
- 当i为奇数且不为1时,结点i的左兄弟结点为i-1;
- 当i为偶数时,结点i的右兄弟结点为i+1。
由上述关系可知,近似满二叉树中结点的层次关系足以反映结点之间的逻辑关系。因此,对近似满二叉树而言,顺序存储结构既简单又节省存储空间。
对于一般的二叉树,采用顺序存储时,为了能用结点在数组中的位置来表示结点之间的逻辑关系,也必须按近似满二叉树的形式来存储树中的结点。显然,这将造成存储空间的浪费。在最坏情况下,一个只有k个结点的右单枝树却需要2k-1个结点的存储空间。
例如,只有3个结点的右单枝树,如下图(a)所示,添上一些实际不存在的虚结点后,成为一棵近似满二叉树,相应的顺序存储结构如下图(b)所示。
2. 二叉树的结点度表示法
二叉树的顺序存储结构可看作是二叉树的一种无边表示,即树中边信息是隐含的。二叉树的另一种无边表示称为二叉树的结点度表示。这种表示法将二叉树中所有结点依其后序列表排列,并在每个结点中附加一个0到3之间的整数,以表示结点的状态。该整数为0时,表示相应的结点为一叶结点;为1时,表示相应结点只有一个左儿子;为2时,表示相应结点只有一个右儿子;为3时,表示相应结点有两个儿子。例如,图(a)中的二叉树的结点度表示如图(b)所示。
3. 二叉树的链式存储结构
在使用记录或内存地址指针的程序设计语言中,二叉树通常用树结点结构来存储。有时也包含指向唯一的父节点的指针。如果一个结点的子结点个数小于2,一些子结点指针可能为空值,或者为特殊的哨兵结点。 使用链表能避免顺序存储浪费空间的问题,算法和结构相对简单,但使用二叉链表,由于缺乏父链的指引,在找回父节点时需要重新扫描树得知父节点的节点地址。
/* 二叉树的二叉链表存储表示 */
typedef struct BiTNode
{
TElemType data;
struct BiTNode *lchild, /* 左孩子指針 */
struct BiTNode *rchild; /* 右孩子指針 */
}BiTNode,*BiTree;
- 4. 三叉链表存储表示
改进于二叉链表,增加父节点的指引,能更好地实现节点间的访问,不过算法相对复杂。 当二叉树用三叉链表表示时,有N个结点,就会有N+2个空指针。
/* 二叉树的三叉链表存储表示 */
typedef struct BiTPNode
{
TElemType data;
struct BiTPNode *parent; /* 父孩子指針 */
struct BiTPNode *lchild; /* 左孩子指針 */
struct BiTPNode *rchild; /* 右孩子指針 */
}BiTPNode,*BiPTree;
四.二叉树的遍历
我们经常希望访问树中的每一个结点并且查看它的值。有很多常见的顺序来访问所有的结点,而且每一种都有有用的性质。
由于二叉树的定义是递归的,因此一棵非空二叉树可以看作是由根节点,左子树和右子树这三个基本部分组成.如果能依次遍历这三个部分的信息,也就遍历了整个二叉树.因此,二叉树的遍历就是按某种策略访问二叉树中每一个节点并且访问一次的过程.
若以字母DLR分别表示访问根节点,遍历根节点的左子树,遍历根节点的右子树,则二叉树的遍历方式有六种:DLR,LDR,LRD,DRL,RDL和RLD.如果限定先左后右则只有前三种方式:DLR,LDR和LRD,分别被称之为先序(前序)遍历,中序遍历和后序遍历.
遍历二叉树的实质就是对二叉树线性化的过程.即遍历的结果是将非线性结构中的节点排成一个线性序列,而且三种遍历的结果都是线性序列.遍历二叉树的基本操作就是访问节点,对含有n个节点的二叉树不论按哪种次序遍历,其时间复杂度均为O(n),这是因为在遍历过程中实际是按照节点的左右指针遍历二叉树的每一个节点.此外,遍历所需的辅助空间为栈的容量,在遍历过程中每递归调用一次都要将有关节点的信息压入栈中,栈的容量恰为树的深度,最坏情况是n个节点的单支树,这时树的深度为n,所以空间复杂度为O(n).
二叉树三种遍历的方法如表所示.
遍历方式 | 操作步骤 |
---|---|
先序遍历 | 若二叉树非空: |
1.访问根节点 2.按先序遍历左子树 3.按先序遍历右子树 | |
中序便利 | 若二叉树非空: |
按中序遍历左子树 2.访问根节点 3.按中序遍历右子树 | |
后序便利 | 若二叉树非空: |
按后序遍历左子树 2.按后序遍历右子树 3.访问根节点 |
此外,二叉树的遍历还可以采用层次遍历的方法.
前序遍历:abdefgc
中序遍历:debgfac
后序遍历:edgfbca
I. 遍历二叉树的递归算法及遍历示例
1. 先序遍历二叉树的递归算法
void Preorder(BSTree *p) //先序遍历二叉树 { if(NULL != p) { printf("%3c",p->data); //访问根节点 Preorder(p->lchild); //先序遍历左子树 Preorder(p->rchild); //先序遍历右子树 } }
2. 中序遍历二叉树的递归算法
void Inorder(BSTree *p) //中序遍历二叉树 { if(NULL != p) { Inorder(p->lchild); //中序遍历左子树 printf("%3c",p->data); //访问根节点 Inorder(p->rchild); //中序遍历右子树 } }
3. 后序遍历二叉树的递归算法
void Postorder(BSTree *p) //后序遍历二叉树 { if(NULL != p) { Postorder(p->lchild); //后序遍历左子树 Postorder(p->rchild); //后序遍历右子树 printf("%3c",p->data); //访问根节点 } }
II.遍历二叉树的非递归算法
递归算法虽然简洁,但可读性较差且执行效率不高.因此,就存在着如何把遍历二叉树的递归算法转化为非递归算法的问题.
由二叉树的遍历可知,先序,中序和后序遍历都是从根节点开始的,并且在遍历过程中所经历的节点路线都是一样的,只不过访问节点信息的时机不同.也即,二叉树的遍历路线是从根节点开始沿左子树向下深入,当深入到最左端节点时,则因无法继续深入下去而返回,然后再逐一进入刚才深入时所遇节点的右子树,并且重复前面深入和返回的过程,直到最后从根节点的右子树返回到根节点时为止.由于节点返回的顺序正好和节点深入的顺序相反,即后深入先返回,它恰好符合栈结构的后进先入特点,因此可以用栈来实现遍历二叉树的非递归算法.注意,在三种便利方式中,先序遍历是在深入过程中饭凡遇到节点就访问该节点信息,中序遍历则是从左子树返回时访问该节点信息,而后序遍历是从右子树返回时访问节点信息.
- 1. 先序遍历二叉树的非递归算法
先序非递归遍历二叉树的方法是:由根节点沿左子树(即p->lchild`所指)一直遍历下去,在遍历过程中每经过一个节点时就输出(访问)该节点的信息并且同时将其压栈,当某个节点无左子树时就将这个节点由栈中弹出,并由这个节点的右子树的根开始继续沿其左子树向下遍历(对此时右子树的根节点也进行输出和压栈操作),直到栈中无任何节点时就实现了先序遍历.先序遍历的非递归算法如下:
void Preorder(BSTree *p) //先序遍历二叉树
{
BSTree *stack[MAXSIZE]; //MAXSIZE为大于二叉树节点个数的容量
int i = 0;
stack[0] = NULL; //栈初始化
while(p!= NULL || i>0) //当指针p不为空或栈stack不空(i>0)时
{
if(p!=NULL) //当指针p不为空时
{
printf("%3c", p->data);//输出该节点信息
stack[++i] = p; //将该节点压栈
p = p->lchild; //沿左子树向下遍历
}
else //当指针p为空时
{
p = stack[i--]; //将这个无左子树的节点由栈中弹出
p = p->rchild; //从该节点右子树的根开始沿左子树向下遍历
}
}
}
- 2. 中序遍历二叉树的非递归算法
中序非递归遍历二叉树与先序非递归遍历二叉树的过程基本相同,仅是输出节点信息的语句位置发生了变化,即每当需要沿当前节点的右子树根开始继续沿其左子树向下遍历时(即此时已经遍历过当前节点的左子树了),就先输出这个当前节点的信息.中序便利二叉树非递归算法如下:
void Inorder(BSTree *p) //中序遍历二叉树
{
BSTree *stack[MAXSIZE]; //MAXSIZE为大于二叉树节点个数的容量
int i = 0;
stack[0] = NULL; //栈初始化
while(i > 0) //栈stack不空(i>0)时
{
if(p!=NULL) //当指针p不为空时
{
stack[++i] = p; //将该节点压栈
p = p->lchild; //沿左子树向下遍历
}
else //当指针p为空时
{
p = stack[i--]; //将这个无左子树的节点由栈中弹出
printf("%3c", p->data);//输出该节点信息
p = p->rchild; //从该节点右子树的根开始沿左子树向下遍历
}
if(p == NULL && i == 0) //当指针p为空且栈stack也为空时结束循环
break;
}
}
- 3. 后序遍历二叉树的非递归算法
后序非遍历二叉树与前面两种非递归遍历算法有所不同,它除了使用栈stack之外,还需使用一个数组b来记录二叉树中节点i(i=1,2,3,…n)当前遍历的情况:如果b[i]为0,则表示仅遍历过节点i的左子树,它的右子树还没遍历过;如果b[i]为1,则表示节点i的左右子树都被遍历过~~
右序非递归便利二叉树的过程仍然是由根节点开始沿左子树向下进行遍历,并且将遇到的所有节点顺序压栈.当某个节点j无左子树时就将节点j由栈stack中弹出,然后检查b[j]是否为0,如果b[j]为0则表示节点j的右子树还未遍历过,也即必须遍历过节点j的右子树后方可输出节点j的信息,所以必须先节点j的右子树,即将节点j重新压栈并置b[j]为1(作为遍历过左右子数的标识),然后再将节点j的右孩子压栈并沿右孩子的左子树继续向下遍历.直到某一时刻该节点j再次由栈中弹出,因为此时b[j]已经为1,即表示此时节点j的左右子树都已遍历过(节点j的左右子树上的所有节点信息都已经输出),或者j本身就是一个叶节点,这时就可以输出节点j的信息了.为了统一,对于前者,在输出了节点j的信息后即置节点j的父节点指向节点j的指针值为NULL.这样,当某个节点的左右孩子指针都已为NULL时,则意味着或者该节点本身就为叶节点,或者该节点左右子树中的节点信息都已输出过,此时就可以输出该节点的信息了.后序遍历二叉树非递归算法如下:
void Postorder(BSTree *p) //后序遍历二叉树
{
int i = 0;
int b[MAXSIZE]; //数组b用于标识每个节点是否已遍历过其左右子树
BSTree *stack[MAXSIZE]; //MAXSIZE为大于二叉树节点个数的容量
stack[0] = NULL; //栈初始化
do
{
if(p != NULL) //当指针p不为空时
{
stack[++i] = p; //将遍历中遇到的所有节点依次压栈
b[i] = 0; //置该节点右子树未访问过的标志
p = p->lchild; //沿该节点左子树继续向下遍历
}
else //当指针p为空时
{
p = stack[i--]; //将这个无左子树(或左子树已遍历过)的当前节点由栈中弹出
if(!b[i+1]) //b[i+1]为0则当前节点的右子树未遍历
{
stack[++i] = p; //将当前节点重新压栈
b[i] = 1; //置当前节点右子树已访问过标志
p = p->rchild; //沿当前节点右孩子继续向下遍历
}
else //当前节点的左右子树都已遍历(即节点信息都已输出)
{
printf("%3c", p->data);//输出当前节点信息
p = NULL; //将指向当前节点的指针置为空
}
}
}while(p != NULL || i > 0) //当指针p 不空或栈stack不空(i>0)时继续遍历
}
这种后序遍历二叉树的非递归算法,其优点是只需一重循环即可实现,而缺点是遍历之后二叉树就被破坏.另一种不破坏二叉树却需两重循环实现的后序遍历非递归算法如下:
void Postorder1(BSTree *p)
{
int b = 0;
int i = -1;
BSTree *q ;
BSTree *stack[MAXSIZE]; //MAXSIZE为大于二叉树节点个数的常量
do
{
while(p != NULL) //将*p节点左分支上的所有左孩子入栈
{
stack[++i] = p;
p = p->lchild;
}
//栈顶节点已没有左孩子或其左子树上的节点都已访问过
q = NULL;
b = 1; //置已访问过的标记
while(i >= 0 && b) //栈stack不空且当前栈顶节点的左子树已经遍历过
{
p = stack[i]; //取出当前栈顶节点
if(p->rchild == q) //当前栈顶节点*p无右孩子或*p的右孩子已经遍历过
{
printf("%3c", p->data);//输出当前节点*p的值
i --;
q = p; //q指向刚访问过的节点*p
}
else //当前栈顶节点*p有右子树
{
p = p->rchild; //p指向当前栈顶节点*p的右孩子节点
b = 0; //置该右孩子节点未遍历过其右子树标记
}
}
}while(i >= 0) //当栈stack非空时继续遍历
}
算法中,表达式p->rchild == q
的含义是:若q等于NULL,则表