树TREE
如图所示:
树的概念
需要注意:
树( tree)是n(n≥0)个结点的有限集合。当n=0时,称为空树;任意一棵非空树满足以下条件:
1)有且仅有一个特定的称为根(root)的结点。
(2)当n>1时,除根结点之外的其余结点被分成m(m>0)个互不相交的有限集合:T1,T2,…,Tm,其中每个集合又是一棵树,并称为这个根结点的子树(subtree)。需要强调的是,树中根结点的子树之间是互不相交的。
结点分类:
结点之间的关系:
(1)结点的度、树的度
某结点所拥有的子树的个数为该节点的度,树的度是每个结点的度中的最大值。
(2)叶子结点、分支结点
度为0的结点称为叶子结点或者称为终端结点:度不为0的结点称为分支结点,也称为非终端结点。 (3)孩子结点、双亲结点、兄弟结点
某结点的子树的根结点称为该结点的孩子结点(child node):反之,该结点称为其孩子结点的双亲结点(parent node):具有同一个双亲的孩子结点互称为兄弟结点(brother node)。
树其他相关概念:
树具有的特点:
树的相关术语:
树的存储结构
1.双亲表示法:
树除了根结点外,其余每个结点,它不一定有孩子,但一定有且仅有一个双亲。 假设以一组连续空间存储树的结点,同时在每个结点中,附设一个指示器指示其双亲结点到链表中的位置。也就是说,每个结点除了知道自己是谁以外,还知道它的双亲在哪里。它的结点结构如图6-4-1所示。 其中data是数据域,存储结点的数据信息,parent是指针域,存储该结点的双亲在数组中的下标。 以下是双亲表示法的结点结构定义代码:
/*树的双亲表示法结点结构定义*/
#define MAX_TREE_SIZE 100
typedef int TElemType;/*树结点的数据类型,暂且定为整型*/
typedef struct PTNode/*结点结构*/
{
TElemType data;/*结点数据*/
int parent;/*双亲位置*/
}PTNode;
typedef struct/*树结构*/
{
PTNode nodes[MAX_TREE_SIZE];/*结点数组*/
int r, n;/*根的位置和结点数*/
}PTree;
由于根结点是没有双亲的,所以我们约定根结点的位置域设置为-1,这就意味着我们所有的结点都存有它的双亲位置。如图6-4-1的树结构和表6-4-2的树双亲表示法所示。
这样的存储结构我们可以根据结点的parent指针很容易找到它的双亲位置,所用的时间复杂度为O(1),知道parent=-1时表示找到了树结点的根。但如果要知道结点的孩子是什么,需要遍历整个结构。
为了更方便地找到结点的孩子,我们增加一个结点最左边孩子的域,称为长子域,这样就可以很容易地得到结点地孩子。如果没有孩子地结点,这个长子域就设置为-1,如表6-4-3所示。
对于有0个或1个孩子结点来说,这样地结构是解决了要找孩子结点的问题了。甚至是有两个孩子,知道了长子是谁,另一个当然是次子了。 为了体现各兄弟之间的关系,可以增加一个有兄弟域来体现兄弟的关系,也就是说每一个结点如果它存在右兄弟,则记录下右兄弟的下标。同样地,如果右兄弟不存在,则赋值为-1,如表6-4-4所示。
但如果结点地孩子很多,超过了2个,我们又关注结点的孩子,又关注结点的双亲,还关注结点的右兄弟,而且对时间遍历要求还比较高,那么我们可以把此结构扩展为有双亲域、长子域、还有右兄弟域(即用 空间换取了时间)。存储结构的设计是一个非常长灵活的过程。一个存储结构设计得是否合理,取决于基于该存储结构的运算是否适合、是否方便,时间复杂度好不好等。注意也不是越多越好,有需要再设计相应的结构。
2.孩子表示法:
换一种完全不同的考虑方法。由于树中每个结点可能有多棵子树,可以考虑用多重链表,即每个结点有多个指针域,其中每个指针指向一棵子树的根结点,我们把这种方法叫作多重链表表示法。不过树的每个结点的度,也就是它的孩子的个数是不相同的。所以可以设计两种方案来解决。
方案一: 一种是指针域的个数就等于树的度,因为树的度是树各个结点度的最大值。其结构如下表:
其中data是数据域,child1到childd是指针域,用来指向该结点的孩子结点。 对于图6-4-1的树来说,树的度是3,所以指针域的个数是3,这种方法实现如下图:
这种方法对于树中各结点的度相差很大时,显然是很浪费空间的,因为有很多的结点,它的指针域都是空的。不过如果树中各结点的度相差很小时,那就意味着开辟的空间被充分利用了,这时存储结构的缺点反而变成优点了。 既然很多指针域都可能为空,那就按需分配空间,即引出了第二中分配方法。
方案二: 第二种方案每个结点指针域的个数等于该结点的度,专门取一个位置来存储结点指针域的个数,其结构如下表所示:
其中data是数据域,degree为度域,也就是存储该结点的孩子结点的个数,child1到childd是指针域,用来指向该结点的孩子结点。 对于图6-4-2的树来说,该方法实现如下图:
这种方法克服了浪费空间的缺点,对空间利用率是很高了,但是由于各个结点的链表是不同的结构,加上要维护结点的度的数值,在运算上就会带来时间上的损耗。 为了既可以减少空指针的浪费又能使结点结构相同,可以把每个结点放到一个顺序存储的结构数组中,再对每个结点的孩子建立一个单链表体现它们的关系。 这就是我们要讲的孩子表示法。具体办法是,将每个结点的孩子结点排列起来,以单链表作存储结构,则n个结点有n个孩子链表,如果是叶子结点,则此单链表为空。然后n个头指针又组成一个线性表,采用顺序存储结构,存放进一个一维数组中,如图6-4-4所示。
为此,设计两种结点结构,一个是孩子链表的孩子结点,如下表:
其中,child是数据域,用来存储某个结点在表头数组中的下标。next是指针域,用来存储指向某结点的下一个孩子结点的指针。 另一个是表头数组 的表头结点,如下表 :
其中data是数据域,存储某结点的数据信息。firstchild是头指针域,存储该结点的孩子链表的头指针。 以下是孩子结点表示法的结构定义代码:
/*树的孩子表示法结构定义*/
typedef struct CTNode/*孩子结点*/
{
int child;/*孩子结点在表头数组中的下标*/
CTNode* next;/*指向某结点的下一个孩子结点*/
}*ChildPtr;
typedef struct/*表头数组 */
{
TElemType data;/*某结点的数据域,存储某节点的数据信息*/
ChildPtr firstchild;/*指向某结点的第一个孩子结点*/
}CTBox;
typedef struct/*树结构*/
{
CTBox nodes[MAX_TREE_SIZE];/*结点数组*/
int r, n;/*根的位置和结点数*/
}CTree
这样的结构对于要找某个结点的孩子,或者找某个结点的兄弟,只需要查找这个结点的孩子单链表即可。对于遍历整棵树也是很方便,对头结点的数组进行循环即可。 但是,这也存在着问题,要想知道某个结点的双亲是谁,需要整棵树遍历才行,比较麻烦。 把双亲表示法和孩子表示法综合,如下图:
把这种方法称为孩子双亲表示法,应该算是孩子表示法的改进,其结构代码定义如下:
typedef struct/*表头数组 */
{
TElemType data;/*某结点的数据域,存储某节点的数据信息*/
ChildPtr firstchild;/*指向某结点的第一个孩子结点*/
int parent;/*双亲位置*/
}CTBox;
其余和孩子表示法相同。
3.孩子兄弟表示法:
观察发现 任意发现,任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。因此,我们设置两个指针 分别指向该结点的第一个孩子和此节点的右兄弟。 结点结构如下表:
其中data是数据域,firstchild为指针域,存储该结点的第一个孩子结点的存储地址,rightsib是指针域,存储该结点的右兄弟结点的存储地址。 结构定义代码如下 :
/*树的孩子兄弟法结构定义 */
typedef struct
{
TElemType data;/*数据域*/
struct CSNode* firstchild, *rightsib;/*指向第一个孩子结点和右兄弟结点*/
}CSNode,*CSTree;
对于图6-4-1的树来说,这种方法实现的示意图如下图所示:
这样表示方法,给查找某个结点的某个孩子带来了方便,只需要通过firstchild找到此结点的长子,然后再通过此结点的rightsib找到它的二弟,接着一直下去,直到找到具体的孩子。如果要查找某个结点的双亲,可以再增加一个parent指针域来解决快速查找双亲的问题。 其实这个表示法最大的好处是它把一棵复杂的树变成了一棵二叉树。把图6-4-6变形即可得到下图这个样子:
这样就可以充分利用二叉树的性质和算法来处理这棵树了 。
二叉树
二叉树的定义
对于这种在某个阶段都是两种结果的情形,比如开和关、0和1、真和假、上和下、对与错、正面和反面等,都适合用树状结构来建模,而这种树是很特殊的树状结构,叫做二叉树。 二叉树(Binary Tree)是n(n≥0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。如下图:
(1)二叉树特点
二叉树的特点有:
1)每个结点最多有两棵子树,所以二叉树中不存在度大于2的结点。注意不是只有两棵子树,而是最多有。没有子树或者有一棵子树也是可以的。
2)左子树和右子树 是有顺序的,次序不能任意颠倒。
3)即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。如下图,树1和树2是同一棵树,但它们却是不同的二叉树。
二叉树具有五种基本形态:
1)空二叉树;
2)只有一个根结点;
3)根结点只有左子树;
4)根结点只有右子树;
5)根结点既有左子树又有右子树。
如果有三个结点,则存在如下物种二叉树:
(2)特殊二叉树
1)斜树 斜树一定是斜的,但往哪里斜还是有讲究的。所有结点都只有左子树的二叉树叫左斜树。所有结点都只有右子树的二叉树叫右斜树。这两者统称为斜树。图6-5-4中的树2就是左斜树,树3就是右斜树。斜树有很明显的特点,就是每一层都只有结点,结点的个数与二叉树的深度相同。 其实线性表的结构可以理解为树的一种极为特殊的表现形式。
2)满二叉树 在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树,如下图:
满二叉树的特点有: 叶子只出现在最下一层,出现在其他层就不可能达到平衡; 非叶子结点的度一定是2; 在同样深度的二叉树中,满二叉树的结点个数最大,叶子个数也最多。
3)完全二叉树
二叉树的性质
(1)第i层的至多结点个数
(2)深度为k的结点个数
(3)终端结点个数和度为2的结点个数的关系
(4)具有n个结点的完全二叉树的深度
(5)判断根、双亲、左右孩子的编号和总的结点个数的关系
二叉树的存储结构
(1)二叉树顺序存储结构
前面提到顺序存储对树这种一对多的关系结构实现起来是比较困难的,但是二叉树是一种特殊的树,它的特殊性使得用顺序存储结构也可以实现。 二叉树的顺序存储结构就是用一维数组存储二叉树中的结点,并且结点的存储位置,也就是数组的下标要能体现结点之间的逻辑关系,比如双亲与孩子的关系、左右兄弟的关系等。
/*二叉树顺序存储结构,其中数组下标即表示结点的编号*/
typedef int SBElemType;/*SBElemType依据实际类型而定,这里假设为int型*/
typedef struct
{
SBElemType data[MAX_TREE_SIZE];/*结点数据域*/
int len;/*数组长度*/
}SBTree;
1234567
(2)二叉链表
二叉树每个结点最多有两个孩子,所以为它设计一个数据域和两个指针域是比较自然的想法。我们称这样的链表叫二叉链表,其结点结构图如下表: 其中data是数据域,lchild和rchild是指针域,分别存放指向左孩子和右孩子的指针。 以下是二叉链表结点结构的定义代码:
/*二叉树的结点结构定义代码*/
typedef struct BiTNode/*结点结构*/
{
TElemType data;/*结点数据*/
struct BiTNode* lchild, *rchild;/*左右孩子指针*/
}BiTNode,*BiTree;
结构示意图如下所示: 就如同树的存储结构中讨论的一样,如果有需要,还可以再增加一个指向其双亲的指针域,那样就称之为三叉链表。
遍历二叉树
(1)二叉树遍历原理
对于二叉树的遍历来说,次序显得很重要。 二叉树的遍历(traversing binary tree)是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问依次且仅被访问一次。 这里有两个关键词,访问和次序。 访问其实是要根据实际的需要来确定具体做什么,比如对每个结点进行相关计算、输出打印等,它其实算是一个抽象操作。在这里我们可以简单假定就是输出结点的数据信息。 二叉树的遍历次序不同于线性结构,最多也是从头至尾、循环、双向等简单的遍历方式。树的结点之间不存在唯一的前驱和后继关系,在访问一个结点后,下一个被访问的结点面临着不同的选择,由于选择方式的不同,遍历的次序就完全不同了。
(2)二叉树遍历方法
二叉树的遍历方式可以有很多,如果我们限制了从左到右的遍历方式,那么主要就分为四种:
1)前序遍历
规则是若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树(注意是左子树而不是左结点),再前序遍历右子树,如下图,遍历次序为:ABDGHCEIF。
2)中序遍历
规则是若树为,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后是访问中结点,最后中序遍历右 子树。如下图,遍历的顺序为:GDHBAEICF。
3)后序遍历
规则是若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历 访问左右子树,最后是访问根结点。如下图,遍历次序为:GHDBIEFCA。
4)层次遍历
规则是若树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历 ,在同一层中,按从左到右的顺序对结点逐个访问。如下图,遍历顺序为:ABCDEFGHI。
我们用图形的方式来表现树的结构,应该说是非常直观和容易理解,但对于计算机来说,它只有循环、判断等方式来处理,也就是说,它只会处理线性序列,而我们刚才提到的四种遍历方法,其实都是在把 树中的结点变成某种意义的线性序列,这就给程序 的实现带来了好处。 另外不同的遍历提供了 对结点依次处理的不同方式,可以在遍历过程中对结点进行各种处理。
(3)前序遍历算法
二叉树的定义是用递归的方式,所以,实现遍历算法也可以采用递归,而且及其简单明了。二叉树的前序遍历算法,代码如下:
/*二叉树的前序遍历算法*/
void PreOrderTraverse(BiTree T)
{
if (NULL == T)/*退出递归的条件*/
return;
printf("%c", T->data);/*显示结点数据,可以改为其他对结点的操作*/
PreOrderTraverse(T->lchild);/*再先遍历左子树*/
PreOrderTraverse(T->rchild);/*最后遍历右子树*/
}
对过程6的理解: “访问了K结点的右孩子,也是null,返回。于是此函数执行完毕”:“此函数”是指T指向K结点时的函数,“执行完毕”是指该结点不存在左孩子和右孩子,因此不再发生下一级递归,故已完成该函数的递归。 “返回到上一级递归函数”:能返回的原因是本级的递归是在上一级调用PreOrderTraverse(T->lchild)时发生的,这就意味着,上一级的递归还没完成就已进入本级递归,故本级递归完成后,自然要继续执行上一级未完成的递归。 “也执行完毕”:因为结点K是结点H的右孩子,所以结点K的递归完成,也就意味着在结点H递归中已执行完PreOrderTraverse(T->rchild)语句,故结点H也完成了递归。 “返回到打印D时的函数,调用PreOrderTraverse(T->rchild)“:”函数“中 的T指向结点D;已完成结点D左子树的递归,继续执行其右子树的递归。 T指向J时的函数执行完之后,意味着所有结点的递归都执行完,因此完成了该算法的递归调用。
(4)中序遍历算法
/*二叉树的中序遍历递归算法*/
void InOrderTraverse(BiTree T)
{
if (NULL == T)/*递归退出的条件*/
return;
InOrderTraverse(T->lchild);/*先中序遍历左子树*/
printf("%c", T->data);/*显示结点数据,可以改为其他对结点的操作*/
InOrderTraverse(T->rchild);/*最后中遍历右子树*/
}
(5)后序遍历算法
/*二叉树的后序遍历递归算法*/
void PostOrderTraverse(BiTree T)
{
if (NULL == T)/*递归退出的条件*/
return;
PostOrderTraverse(T->lchild);/*先中序遍历左子树*/
PostOrderTraverse(T->rchild);/*再后序遍历右子树*/
printf("%c", T->data);/*显示结点数据,可以改为其他对结点的操作*/
}
打印后续结点的过程:打印K之后,结点H的递归执行完,打印H;执行上一级,结点D无右孩子,打印D,D结点完成递归;执行上一级,执行B结点的右孩子E递归,E无左右孩子,打印E,B的递归完成,打印B;执行上一级,执行A的右孩子C递归,C有左孩子F,F有左孩子I,I无左右孩子,打印I;F无右孩子,F递归结束,打印F;返回上一级,C有右孩子G,G无左孩子,有右孩子J,J无左右孩子,打印J;G完成递归,打印G;C完成递归,打印C;A完成递归,打印A。至此,该递归算法执行结束。
(6)推导遍历结果
本篇大部分是我向其他人学习的,如有不会,可以一起探讨。网上也有相应的课程学习,比如一个青岛大学的老师,我就在她那学习的。