1、树的定义
我们需要研究这种一对多的数据结构——“树”,考虑它的各种特性,用来解决我们在编程中遇到的各种问题。
树是n个结点的有效集(n>=0)。n=0时称为空树。在任意一颗非空树(1)有且仅有一个特定的称为根(Root)的结点。(2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、......、Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)
强调两点(1)n>0 时根结点时唯一的,不可能存在多个根结点,数据结构中的树是只能有一个根结点。
(2)m>0 时, 子树的个数没有限制,但它们一定是不相交的。
2、结点分类
1、树的结点包含一个数据元素及若干指向其子树的分支。结点拥有的子树数称为结点的度(Degree)。度为0的结点称为树的叶结点(leaf)或终端结点;度不为0的结点称为非终端结点或分支结点。除根结点之外,分支结点也称为内部结点。树的度是树内各结点的度的最大值。
2、结点间关系
结点的子树的根称为该结点的孩子,相应地该结点称为孩子的双亲节点,同一个双亲的孩子之间互称兄弟。结点的祖先是从根到该结点所经分支上的所有结点。,反之,以某结点为根的子树的任一结点都成为该结点的子孙。
3、树的其他相关概念
结点的层次(level)从根开始定义起,根为第一层,根的孩子在第二层。双亲在同一层的结点互为堂兄弟。树中结点的最大层次称为树的深度(Depth)或高度。如果将树中结点的各子树看成从左至右是有次序的,不能能互换的,则称该树为有序树,否则称为无序树。
森林是m(m>=0)颗互不相交的树的集合。
对比线性表和树的结构,它们有很大的不同:
(1)线性表的第一个元素:无前驱 。最后一个元素:无后继 。中间元素:一个前驱一个后继。
(2)树结构: 根结点:无双亲,唯一 。叶结点:无孩子,可以多个。中间结点:一个双亲多个孩子
3、树的存储结构
简单的顺序存储结构是不能满足树的实现要求的。(谁是谁的双亲,谁是谁的孩子呢?)。不过充分利用顺序顺序存储结构和链式存储结构的特点,完全可以实现对树的存储结构的表示。我们这里要介绍三种不同的表示法:双亲表示法,孩子表示法,孩子兄弟表示法
1、双亲表示法
我们假设以一组连续空间存储空间存储树的结点,该结点由数据域(data域)用于存储数据元素值,指针域(parent)用于存储该结点的双亲结点下标。
#include <stdio.h>
#define MAX_TREE_SIZE 100
typedef int ElemType;
struct _PTNode
{
ElemType data;
int parent;
}PTNode;
树结点的定义如上。下来可以定义树结构,实际上就定义了一个结构体数组,树结构定义如下:
typedef struct _PTree
{
PTNode nodes[MAX_TREE_SIZE]; /*结构体数组*/
int root, n; /*定义根结点,结点数*/
}PTree;
定义根结点(root)的双亲结点下标为-1,即不存在。
这样的存储结构我们可以很容易的找到它的双亲结点,所以时间复杂度为O(1),但是我们如果要知道该结点的孩子结点时什么,这时就需要遍历整个结构才能找到。
2、孩子表示法
这里引入一个例子对接下来的结构进行说明:一个树如下
这里采用两种结点结构:一种是孩子链表的孩子结点,另一种是表头数组的表头结点。孩子结点里面有child域——用来存储该结点在表头数组的下标、next域——用来存储指向该结点的下一个结点的指针;表头结点有data数据域——用来存储结点名称、firstchild域——用于存储该结点的孩子链表的头指针(下标)。
结构定义如下:
typedef struct _CTNode
{
ElemType child; /*存储该结点的孩子在表头数组的下标*/
CTNode* next; /*存储指向该结点下一个孩子结点的指针*/
}CTNode;
typedef struct _ListHNode
{
ElemType1 name; /*结点名称*/
CTNode* firstchild; /*用来存储该结点的孩子链表的头指针*/
}ListHNode;
typedef struct _CTree
{
ListHNode data[MAX_TREE_SIZE]; /*表头结点数组*/
int root, n; /*定义根结点,结点数*/
}CTree;
大体说下整体的结构:定义一个结构体数组,一个数组元中,包含结点名称与孩子链表。
结构如图:
这样的结构可以查找某个结点的某个孩子结点,或找某个结点的兄弟,只需要查找这个结点的孩子链表即可。但是这也存在着问题,如何知道某个结点的双亲是谁呢?其实只需往上面定义的表头结点,加一个属性,即当前结点的双亲结点的下标。结构定义如下:
typedef struct _ListHNode
{
ElemType1 name; /*结点名称*/
int parent; /*该结点双亲结点下标*/
CTNode* firstchild; /*用来存储该结点的孩子链表的头指针*/
}ListHNode;
3、孩子兄弟表示法
任意一棵树,它的结点的第一个孩子如果存在就是唯一的,那么它的右兄弟如果存在也是唯一的。因此,我们设置两个指针,分别指向该结点的第一个孩子和此结点的右兄弟
此类结点结构包含data数据域——用来存储结点名称,firstchild为指针域——用存储该结点的第一个孩子结点的地址,rightbro是指针域——用来存储该结点的右兄弟的地址。
树的孩子兄弟表示法结构定义如下:
/*孩子兄弟表示法的结构*/
typedef struct _CBroNode
{
ElemType1 name;
CBroNode* firstdhild; /*指向第一个孩子结点*/
CBroNode* RightBro; /*指向右兄弟*/
}CBroNode;
这个方法给查找某个结点的孩子结点带来方便,只需通过该结点的第一个孩子结点(长子结点),然后通过长子结点的ringhtbro找到它右兄弟,这样进行下去,就可以找到,某个结点的所有孩子结点。
但是这样的结构,如果想找某个结点的双亲结点,也是有缺陷的。怎么办呢?可以在结点中再加入一个parent指针——用于指向该结点的双亲结点。定义结构如下:
/*孩子兄弟表示法的结构*/
typedef struct _CBroNode
{
ElemType1 name;
CBroNode* parent; /*指向该结点的双亲结点*/
CBroNode* firstdhild; /*指向第一个孩子结点*/
CBroNode* RightBro; /*指向右兄弟*/
}CBroNode;
4、二叉树的定义
二叉树(Binary Tree)是n(n>=0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者有一个根结点和两颗互不相交的、分别称为根结点的左子树和右子树的二叉树组成。
1、二叉树特点(1)每个结点最多有两颗子树,即结点的度最大为2.。(2)左子树和右子树是有顺序的,次序不能颠倒,就像人的左右手一样。(3)即使树中某结点只有一颗子树,也要区分它是左子树还是右子树。
二叉树具五种基本形态1.空二叉树 (n=0) 2.只有一个根结点(n=1) 3.根结点只有左子树 4.根结点只有右子树 5.根结点既有左子树又有右子树。
2、特殊二叉树
(1)斜树
所有结点都只有左子树的二叉树叫作斜树,当然所有结点都只有右子树的二叉树也称为斜树,如图所示的两个二叉树都为斜树
其实,这样的斜树与我们线性表的结构是相同的,所以可以理解线性表是树的一种极其特殊的表现形式。
(2)满二叉树
在一颗二叉树中,如果所有分支结点(非终端结点)都存在左子树和右子树,并且所有叶子结点都在同一层上,这样的二叉树称为满二叉树。一个满二叉树如图所示:
满二叉树的特点:
(3)完全二叉树(1)叶子只能出现在最下一层。(2)非叶子结点的度一定是2(3)在同样深度(高度)的二叉树中,满二叉树的结点数最多,叶子数最多。
对一棵具有n个结点的二叉树按层序编号,如果编号为(1<=i<=n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中的位置完全相同,则这棵二叉树为完全二叉树。如图:
满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树。
完全二叉树的特点:
(1)叶子结点只能出现在最下面两层。(2)最下层的叶子结点一定集中在左部连续位置。(3)倒数第二层,若有叶子结点,一定都在右部连续位置。(4)如果结点的度为1,则该结点只有左子树,不存在右子树。(5)同样结点数的二叉树,完全二叉树的深度最小。
完全二叉树按层序编号。
5、二叉树的性质
性质1:在二叉树的第i层上至多有2的i-1次方个结点
性质2:深度为k的二叉树至多有2的k次方,减1个结点
性质3:对任意一棵二叉树T,如果其叶子结点(终端结点)数为n0,度为2的结点数为n2,则n0=n2+1。推导过程为:结点总数n=n0+n1+n2,分支线的总数=n1+2*n2=n-1。联立这几个式子可以得到n0=n2+1。
性质4:具有n个结点的完全二叉树的深度为logn向下取整加1。
性质5:如果对一棵有n个结点的完全二叉树的结点按层序编号(从上到下,从左到右),对任意结点i(1<=i<=n)有:
(1)如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲结点是结点i/2向下取整。(2)如果2i>n,则结点i无左孩子(结点i为叶子结点);否则结点2i是其左孩子。(3)如果2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1。
6、二叉树的存储结构
1、二叉树顺序存储结构
二叉树的顺序存储结构就是用一维数组存储二叉树中的结点,并且结点的存储位置,也就是数组的下标要能体现结点之间的逻辑关系。先来看看二叉树组的顺序存储,一棵完全二叉树如图所示:
将上图所示的二叉树存入到数组中,相应的下标对应其同样的位置
由于完全二叉树的严格定义,所以用顺序存储结构也可以表现出二叉树的结构来。
2、二叉链表
由于顺序存储的使用性不强,所以需要考虑链式存储结构。二叉树每个结点最多有两个孩子结点,所以为它设计一个数据域和两个指针域。我们成这样的链表叫二叉链表。结点结构如图:
其中data是数据域,lchild和rchild都是指针域,分别存放指向其左孩子和右孩子的指针。
以下是我们二叉链表的结构定义代码:
/*二叉树的链式存储——二叉链表*/
typedef struct _BiNode
{
ElemType data; /*数据域*/
BiNode* lchild; /*左孩子指针域*/
BiNode* rchild; /*右孩子指针域*/
}BiNode;
typedef BiNode* Bitree;
7、遍历二叉树
定义:二叉树的遍历(traversing binary tree)是指从根结点出发,按照某种次序一次访问二叉树中的所有结点,使得每个结点被访问一次且仅一次。
二叉树遍历方法
1、前序遍历
规则是若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,在前序遍历右子树。
如图是一个二叉树对它的前序遍历过程为:先访问根结点A,再遍历A的左子树,先访问左子树的根B,再访问B的左子树根D,再访问D的左子树G,然后访问D的左子树的根H,再访问H的左子树I,然后访问A的右子树的根C,再访问C的左子树的根E,由于E没有左子树,所以接下来访问右子树J,C的左子树访问完来,接下来访问C的右子树F,遍历结束。
遍历的过程为:A B D G H I C E J F。
下面是关于前序遍历的代码实现:
void PreOrderTraverse(Bitree T)
{
if (T == NULL)
return;
printf("%d,",T->data);
PreOrderTraverse(T->lchild);
PreOrderTraverse(T->rchild);
}
主要是利用了递归的思想,前序遍历主要是先对根结点访问,然后对左子树进行前序遍历,然后对右子树进行遍历。
2、中序遍历
中序遍历,与前序遍历的访问次序不同而已。首先遍历根结点的左子树,然后再访问根结点,然后再中序遍历右子树。
算法具体实现代码如下:
void InOrderTraverse(Bitree T)
{
if (T == NULL)
return;
InOrderTraverse(T->lchild);
printf("%c,",T->data);
InOrderTraverse(T->rchild);
}
3、后序遍历
后序遍历与之前两个访问的顺序完全不同,首先后序遍历左子树,然后再后序遍历右子树,最后在访问根结点。
后序遍历算法代码实现如下:
void PostOrderTraverse(Bitree T)
{
if (T == NULL)
return;
PostOrderTraverse(T->lchild);
PostOrderTraverse(T->rchild);
printf("%c,",T->data);
}
8、二叉树的建立
说了很多,我们如何生成一棵二叉树呢?首先,需要考虑到一个问题,需要知道每个结点是否有左右孩子。举个例子:一棵二叉树如下图所示:
如图是一个普通的二叉树,为了能确定每个结点是否有左右孩子,我们对它进行了扩展,如下图所示:
扩展成为如上图的样子,将二叉树中的每个结点的空指针引出一个虚结点。假设结点的data域的类型为char型,我们把这个二叉树的前序遍历AB#D##C## 用键盘依次输入。建树算法代码如下:
void CreateBiTree(BiTree T)
{
ElemType1 ch;
scanf("%c",&ch);
if (ch == '#')
{
T = NULL;
}
else
{
T = (BiNode*)malloc(sizeof(BiNode));
T->data = ch;
CreateBiTree(T->lchild);
CreateBiTree(T->rchild);
}
}
9、线索二叉树
由于二叉树的每个结点都是二叉链表结点,即每个结点都有一个数据域与两个指针域,分别为左孩子指针(lchild)与右孩子指针(rchild)。但是这些指针域并不会得到充分利用,会造成内存浪费。比如一个深度为四的满二叉树,其最下面一层的叶子结点,均没有子结点,所以这些指针域均没有得到利用。
所以为了更加充分的利用指针域,根据某种遍历方式,把这些空指针用于指向前驱和后继指针。我们把指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树。
对二叉树以某种次序遍使其变为线索二叉树的过程称作线索化。
那么如何知道某一结点的lchild指针域是指向它的左孩子还是指向前驱呢?因此我们再增加两个标志域ltag和rtag,只是存放0或1的布尔型变量,结点结构如下图:
当标志为0时,指向该结点对应的孩子结点。当标志为1时,指向该结点对应的前驱或后继结点。
由于线索化的实质就是将二叉链表中的空指针改为指向前驱或后继的线索。而前驱和后继的信息只有在遍历该二叉树是才能得到,所以线索化的过程就是在遍历过程中修改空指针的过程。