1.二叉树的基本概念 (二叉树建立的flash演示)
定义:二叉树是 n ( n ≥0)个元素的有限集,若 n =0,则称这棵二叉树是棵空二叉树;当 n > 0时,二叉树中存在唯一的称作根结点的元素root,且每个结点至多只有二棵子树,二棵子树有左右之分,其次序不能任意颠倒。
由于二叉树中每个结点具有其左右子树的次序不能任意颠倒这一特性,一棵包含三个结点的二叉树具有五种基本形态 ,如图11.4所示。
图11.4 包含三个结点的二叉树的基本形态
2.二叉树的性质
二叉树有几个非常重要的特性。具体如下:
性质 1 在一棵非空的二叉树的第 i 层上至多有2 i -1 个结点( i ≥1)。
利用归纳法很容易证明此性质。这里证明从略。
性质 2 一棵深度为 k 的二叉树至多有2 k -1个结点。( k ≥1)
证明:由性质 1可见,深度为 k 的二叉树的最大结点个数为:
第 i 层上的最大结点个数= =2 k -1
性质 3 对于任何一棵非空的二叉树 T ,如果其叶结点个数为 n 0 ,度为2的结点个数为 n 2 ,则 n 0 = n 2 +1。
证明:设 n 1 为二叉树 T 中度为 1的结点个数,又已知二叉树中所有结点的度均小于或等于2,故二叉树的结点总数为:
n = n 0 + n 2 + n 1 (5.1)
再看二叉树中的分支数 :除了根结点外,其余结点都有一个分支进入,设 B 为分支总数,则n=B + 1。由于这些分支是由1或2的结点射出的,所以又有 B = n 1 +2 n 2 ,于是得:
n = n 1 +2 n 2 +1 (5.2)
由式( 5.1)和式(5.2)得:
n 0 = n 2 +1
3.特殊形态的二叉树
满二叉树 一棵深度为 k 且有2 k -1个结点的二叉树称为满二叉树。如图11.5(a)所示。
完全二叉树 如果我们对满二叉树上的结点进行连续编号,约定编号从根结点起,自上而下,自左至右顺序进行编号。那么,一棵深度为 k 的,有 n ( n < 2 k -1)个结点的二叉树,当且仅当其每一个结点的编号都与深度为 k 的满二叉树中编号从1到 n 的结点编号一一对应时,称之为完全二叉树。如图11.5(b)所示。
图11.5 两种特殊形态的二叉树
性质 4 具有 n 个结点的完全二叉树的深度为log2n向下取正+1
证明:设完全二叉树的深度为 k,则根据性质2和完全二叉树的定义有:
2 k -1 -1 < n ≤2 k -1 或 2 k-1 ≤n < 2 k
于是 k - 1≤ log 2 n < k 因为k是整数,所以 k=log2n向下取正+ 1
性质 5 对于具有 n 个结点的完全二叉树,如果按自上而下、从左到右的顺序对各结点进行编号,则对于任意结 点 i(1≤i≤n)有:
(1)如果i =1,则i 结点是二叉树的根,无双亲;如果 i > 1,则其双亲的编号为 ? i /2 ? 。
(2)如果2 i ≤ n ,则编号为 i 的结点的左孩子结点的编号为2 i ;如果2 * i > n ,则编号为 i 的结点无左孩子。
(3)如果2 i +1≤ n ,则编号为 i 的结点的右孩子编号为2 i +1;如果2 i +1 > n ,则编号为 i 的结点无右孩子。
此性质可以用归纳法证明,这里证明从略。
平衡二叉树 如果二叉树上每个结点的左右子树的深度之差的绝对值都不大于 1,则称这棵二叉树是平衡二叉树。
二叉排序树 在一棵二叉树上,对于每个结点来说,如果其左子树上所有结点的关键字均小于根结点的关键字,而其右子树上所有结点的关键字都大于或等于根结点的关键字,则称这样的二叉树是二叉排序树。
二叉树的存储结构
1.顺序存储结构
用一组连续的存储单元顺序存放二叉树中所有结点的数据元素即为顺序存储结构。但这样的结构最好用于满二叉树或完全二叉树这样的特殊二叉树。因为由满二 叉树和完全二叉树的性质可知:这样的二叉树中各结点的编号可以唯一地反映出结点之间的逻辑关系,所以可以用一维数组按自上而下、从左至右的顺序存放各结点 的数据信息,通过数据元素存储位置下标关系来反映满二叉树或完全二叉树中结点间的逻辑关系。如图 11.6所示为图11._5的二叉树的存储结构。
图11.6 二叉树的顺序存储结构
对于一般形态的二叉树,由于其每个结点不一定有左右两棵子树,因此,若采用顺序存储结构,则应将其每个结点与满二叉树上的结点相对照,空缺的子树在其相应的位置补 “NULL”,表示不存在此结点,而非空缺的子树,则按其位置存储在一维数组的相应分量中。
用 C语言描述的顺序存储结构如下:
# define MAXNODE <二叉树中结点个数的最大值>
Elem BT[MAXNODE];
2.链式存储结构
由二叉树的定义可知,二叉树中的结点是由一个数据元素和分别指向其左、右子树的两个分支构成。基于此逻辑结构,我们可以很容易的设计出二叉树的链式存 储结构中的各结点结构:即每个链结点至少包含三个域:数据域和左、右孩子指针域。如图 11.7(a)所示。有时,为了便于找到结点的双亲,还可以在每个结点中增加一个指向其双亲的指针域。如图11.7(b)所示。利用这两种结点结构所得到 的二叉树的存储结构分别称之为二叉链表、三叉链表。二叉树的二叉链表存储如图11.8所示。
图 11.7 二叉树链结点示意图
图11.8 二叉链表结构示意图
当二叉树采用二叉链表存储结构时,如果某结点的左孩子或右孩子不存在,则相应的指针域置 “空”。此外,对于一棵二叉树,还应设置一个指针指向二叉树的根结点,我们把这个指针仍称作头指针。与单链表中的头指针的作用相似,二叉链表中的头指针可 以唯一地确定一棵二叉树。
用 C语言描述的二叉链表的存储结构如下:
Typedef struct btnode{
Elem data;
struct btnode * Lchild, * Rchild;}BTNODE;
BTNODE * Root; / * 二叉树的头指针 * /
二叉树的遍历
在二叉树的应用中,最典型的算法是要求在二叉树中查找具有某种特征的结点,或者对树中全部结点逐一进行某种处理。这些算法要解决的关键问题是对二叉树 按某一搜索路径访问每个结点,使得每个结点均被访问一次,且仅被访问一次,这样的访问我们称之为 遍历。 所谓 “访问”,含义可以很广,可以是对每个结点作某种处理,例如输出结点的数据信息等。为了讨论问题方便,我们这里约定:遍历就是输出结点的数据信息。
由二叉树的定义可知:二叉树是由三个基本单元组成:根结点、左子树、右子树。如果我们分别以 D 、 L 、 R 表示根结点、左子树、右子树,要依次遍历这三部分,按其不同的遍历顺序可以有六种遍历二叉树的方案: LDR,DLR,LRD,RDL,DRL,RLD。如果限定先左后右的顺序为合法顺序,则遍历方案缩减为三种。我们按照访问根结点的先后,分别称之为先序 遍历、中序遍历和后序遍历。此外还可以有一种按照二叉树中结点自上而下、自左至右的顺序遍历的方案,称之为层序遍历。下面分别就前三种遍历的递归算法进行 讨论。在算法中我们规定,二叉树的存储结构采用二叉链表(结构如前所定义)
1.先序遍历(DLR)(flash演示)
先序递归操作定义为:若二叉树为空,遍历结束;否则:
(1)访问根结点;
(2)先序遍历根结点的左子树;
(3)先序遍历根结点的右子树;
递归算法如下:
Preorder(BTNODE * Root)
{if(Root= =NULL)return ok; / * 递归出口 * /
printf(“%c\n”,Root - >data); / * 访问该结点,其数据类型设为字符型 * /
if(Root - >Lchild != NULL)
Preorder(Root - >Lchild); / * 先序递归遍历左子树 * /
if(Root - >Rchild !=NULL)
Preorder(Root - >Rchild); / * 先序递归遍历右子树 * /
}
如图 11.9所示的二叉树的先序遍历序列为:
A B D G E H C F
图11.9 一般形态的二叉树
2.中序遍历(LDR) (flash演示)
中序递归操作定义:若二叉树为空,遍历结束;
否则,( 1)中序遍历根结点的左子树;(2) 访问根结点;( 3)中序遍历根结点的右子树;
递归算法如下:
void Inorder(BTNODE * Root)
{if(Root = = NULL)return ok; / * 递归出口 * /
if(Root - >Lchild != NULL)
Inorder(Root - >Lchild); / * 中序递归遍历左子树 * /
Printf(“%c\n”,Root - >data); / * 访问根结点 * /
if(Root - >Rchild != NULL)
Inorder(Root - >Rchild); /* 中序递归遍历右子树 */
}
如图11.9所示二叉树的中序遍历序列为:
G D B E H A C F
3.后序遍历(LRD) (flash演示)
后序递归操作定义:若二叉树为空,遍历结束。否则,
(1)后序遍历根结点的左子树;
(2)后序遍历根结点的右子树;
(3)访问根结点;
递归算法如下:
void Postorder(BTNODE * Root)
{if( Root = = NULL)return ok; / * 递归出口 * /
if( Root - >Lchild != NULL)
Postorder(Root - >Lchild); / * 后序递归遍历左子树 * /
if( Root - >Rchild != NULL)
Postorder(Root - >Rchild); / * 后序递归遍历右子树 * /
Printf(“%c\n”,Root - >data); / * 访问根结点 * /
}
如图 11.9所示二叉树的后序遍历序列为:
G D H E B F C A
4.层序遍历
层序遍历是对二叉树逐层按自上而下、自左至右的顺序进行访问。在进行层序遍历时,在一层结点访问完成后,再按照它们的被访问顺序对各结点的左孩子、右 孩子顺序访问,这样就完成了对下一层各结点从左到右的访问。因此,在层序遍历算法设计时,需设置一个队列,当遍历从根结点开始时,首先将根结点指针入列,然后从队首取出一个元素,每取出一个元素做两个操作:( 1)访问结点数据;(2)若该结点的左孩子、右孩子非空,则分别将它们的左孩子、右孩子指针顺序入列。如此循环直至队列为空时结束。此算法描述从略。
综上所述,无论何种遍历方案,从算法上来说,只是访问根结点的先后顺序问题。而为了解决访问顺序问题,在先序、中序、后序遍历算法中通常采用递归调用形式,实际上,递归调用本身就隐含地应用了线性结构中的堆栈(即后进先出表);在层序遍历算法中,应用了线性结构中的队列。因此,二叉树的遍历算法看似复杂,而其基本操作乃是对线性结构操作的推广应用。