终于到了这一块了!到这里数据结构的难度才会慢慢体现出来,与此相比之前的都是小儿科,树的内容会用到栈与队列,线性表相关的知识如果你还不清楚,可以先去看看。
ps:点击蓝色字体即可进入相关内容
数据结构之线性表
数据结构之栈和队列
同时也欢迎大家关注本专栏,持续更新中!
树是一种比较复杂的数据结构,种类也比较多,在这里我们主要说一下树以及比较简单的二叉树的一些操作定义与运用。
树首先是一种逻辑结构,它是n(n≥0)个结点的有限集合,n=0 时,称为空树。
而任意非空树应满足:
1.有且仅有一个特定的称为根的结点。
2.当n>1时,其余结点可分为m(m>0)个互不相交的有限集合,其中每一个集合本身又是一棵树,称为根结点的子树。
如下就是一个简单的树结构,其中A结点就是唯一的根的结点,B,C,D,E,F又是根结点的子树
通过这个图其实我们就可以发现:n个结点的树中只有n-1条边,因为每一个树都有一个像A的唯一的根结点。
我们先来看一个简单的树,由这个树我们分别来说一下树的一些常用基本术语。
双亲结点和孩子结点
一个结点若干分支下的结点都为该结点的孩子结点(或称子节点),并且,该结点称为孩子结点的双亲节点。如:
A结点的孩子结点为:B,C,D;B,C,D的双亲节点为A结点
祖先结点和子孙结点
这个关系就同父亲和孩子一样。从根结点到该结点路径上的所有结点都为该结点的祖先。如:
K结点的祖先结点为:A,B,E;G结点的祖先节点为:A,C
兄弟结点和堂兄弟结点
兄弟结点:父节点下的所有子结点互为兄弟结点
堂兄弟结点:位于同一层的,并且父节点之间是兄弟结点的结点互为堂兄弟结点。如:
E,F结点互为兄弟结点;E,G结点互为堂兄弟结点
度
树中一个结点的子结点的个数称为该结点的度,树中最大度数称为树的度。
其中A为唯一的根结点,其他11个结点均为根结点的子树结点。
以这个树为例,A结点的度为3,B结点的度为2,C结点的度为1。
这个数最大度数为3,所以这个树的度也为3。
分支结点与叶子节点
度大于0的结点称为分支结点,度为0的结点称为叶子结点
层次,高度和深度
结点的层次为从结点到根结点的路径中边的条数,并且认为根结点的层次为0,因为根结点到自身的路径中边的条数为0(也有一些教科书假设根结点的层次为1,这个时候要注意书中相应的说明)
结点的深度是从根结点出发,向着叶子结点方向前进的,并且深度这个概念一般只用来描述树,当描述结点的深度时,层次更为恰当,但是用深度也无妨。树的深度也是选取结点中的最大深度(或最大层次)
结点的高度是从一个结点出发,一直到它的叶子结点的最大路径中的边的条数。叶子结点的高度认为是0,因为从叶子结点出发,到它本身的路径只有一条,并且边数为0
有序树与无序树
顾名思义有序树就是有顺序的树,无序树就是没有顺序的树。有序数结点调换位置树就发生了变化,不再是原来的树了。而无序树只要是树的结构没有变化,相应调换顺序还是原来的树还是原来的树。
路径与路径长度
路径:树中两个结点之间的路径是由这两个结点之间所经过的结点序列构成的。树中的分支是有向的,即从双亲结点指向孩子结点,所以路径一定是自上而下的。
路径长度:路径上所经历边的个数。
森林
森林是 m(m≥0)棵互不相交的树的集合,如:
树中的结点数等于所有结点的度数加1
每个结点的度数分别对应结点的子结点,设所有结点的度数为 d,总结点数为n,则 n = d + 根节点,即 n = d + 1
。
度为m的树中第i层上至多有 mi-1 个结点(i≥1)
高度为h的m叉树至多有( mh-1)/(m-1)个结点
节点数最多,则每层都有mi−1 个结点(i>=1)。则总结点数为:n=m0+m1+m2+……+mh-1,等比数列求和公式得:n=( mh-1)/(m-1)
ps:⌊⌋
表示向下取整,⌈⌉
表示向上取整
具有n个结点的m叉树的最小高度为 ⌈logm(n(m - 1)+1)⌉
这个算起来比较简单把已知的n带入公式n=( mh-1)/(m-1),解方程即可得到最小高度h,又由于多余节点也是一层,所以需要向上取整h= ⌈logm(n(m - 1)+1)⌉
满m叉树节点编号为 i 的第 k 个孩子节点编号为
(i-1)*m+k+1( 1 ≤ k ≤ m )
按照从上到下,从左到右算
首先我们应该明白二叉树与栈和队列一样也是一种逻辑结构,后面又根据不同的存储结构去实现它
二叉树是n(n≥0)个结点的有限集合。
n=0时,二叉树为空树
n>0时,由根结点和两个互不相交的被称为根的左子树和右子树组成。左子树和右子树也分别是一棵二叉树
因为二叉树是区分左子树与右子树的,所以二叉树一共有5种形态。如下:
这里有人可能想到我们在上文讲到的有序树,如果是一颗度为2的有序树,那么和我们的二叉树有什么区别呢?
这里还是有所不同的,如下:
二叉树可以为空,而度为2的有序树至少有三个结点
二叉树的孩子结点始终有左右之分,而度为2有序树的孩子结点次序是相对的
二叉树的性质
非空二叉树上的叶子结点数等于度为2的结点数加1,即n0=n2+1
设度为0、1、2的节点个数为n0, n1, n2
则结点总数n为:n=n0+n1+n2
也可以是:n=n1+2n2+1
根据上面两个式子联立即可得到n0=n2+1(n0为叶子结点)
非空二叉树上第k层上至多有2k-1个结点(k≥1),如:
同时可以得到:
高度为h的二叉树至多有2h-1个结点(h≥1)
对完全二叉树按从上到下、从左到右的顺序依次编号1,2,…,n,则有以下关系:
当i>1是,结点i的双亲结点标号为⌊i/2⌋ 。即当i为偶数时,其双亲结点的编号为i/2,他是双亲结点的左孩子;当i为奇数时,其双亲结点的编号为(i-1)/2,他是双亲结点的右孩子。
当2i≤n时,结点i的左孩子编号为2i,否则无左孩子。
当2i+1≤n时,结点i的右孩子编号为2i+1,否则无右孩子。
结点i所在层次为⌊log2i ⌋ +1
高度为h的二叉树至多有2h -1个结点,设结点数为n,则:
具有n个(n>0)结点的完全二叉树的高度为⌊log2 n⌋+1或⌈log2(n+1)⌉
满二叉树
这里的 2h -1是怎么来的呢?因为高度为h的m叉树至多有(mh-1)/(m-1)个结点
性质:
在二叉树中,对于编号为i的结点,若存在,其
双亲的编号为 i/2 ,左孩子为2i,右孩子为2i+1
完全二叉树
设一个高度为h、有n个结点的二叉树,当且仅当其每个结点都与高度为h的满二叉树中编号1~n的结点一一对应时,称为完全二叉树。
这时候有人可能不明白了,满二叉树不是与完全二叉树一样吗?并不是它们虽然长得很像,但是还是有一定区别的:完全二叉树的节点数是任意的,最后那一行可能不是完整的,其他位置均与满二叉树相同。
性质:
若
i≤⌊n/2⌋
,则结点i为分支结点,否则为叶子结点
叶子结点值可能在层次最后的两层上出现。对于最大层次的叶子结点,都依次排在最左边的位置上。
度为1的结点若存在,则可能有一个,且是编号最大的分支节点,并孩子结点一定是左结点。
二叉排序树
一棵二叉排序树,若树非空则具有如下性质:
平衡二叉树
树上任意结点的左子树和右子树的深度只差不超过1。
ps:结点的深度是从根结点出发,向着叶子结点方向前进的
如下图,右边的二叉树因为根结点的左子树的左子树深度为2,右子树结点深度为0,所以不是平衡二叉树。
二叉树的存储结构有两种,顺序结构和链式存储。我们一般常用的是链式存储。
用一组连续的存储单元(数组)依次自上而下、自左至右存储完全二叉树上的结点元素
这种连续的存储单元我们一般使用数组实现,而且一般从数组下标为开始储存树的结点元素,这样可以保证数组下标与树结点元素序号一致,而在下标为0的存储单元上来储存树的总结点个数。
那我们又该如何保证它的逻辑结构呢?
首先看在完全二叉树中依次编号,对于结点i:若存在左孩子,则编号为2i;若存在右孩子,则编号为2i+1。这是因为数组下标与树结点元素序号一致。由此我们可以实现它的逻辑结构
而对于一个普通的二叉树,它的节点元素序号不一定等于数组下标,所以无法实现它的逻辑结构。如下:
那我们没有办法来解决这个问题吗?当然是有的。我们可以添加一些不存在的空结点,把树补成完全二叉树。在数组中用 0 表示,这样就可以使数组下标与树结点元素序号一致,来实现它的逻辑结构了。
顺序存储最坏情况下会非常浪费存储空间,如下:
顺序存储只是比较适合完全二叉树,所以对于树我们还是常用链式存储结构。
用链表来存放一棵二叉树,二叉树中每个结点用链表的一个链结点来存储。
一个结点包括三部分,一个数据域来存放树的结点元素,两个指针域,分别指向左子树与右子树。用代码实现为:
typedef struct BiTNode {
ElemType data;
struct BiTNode *lchild;
struct BiTNode *rchild;
}BiTNode,*BiTree;
含有n个结点的二叉链表中,有n+1个空链域
这里n+1由2n-(n-1)得来。
每个结点处数据域外有两个指针域,又有n个结点,除根结点外,其他结点都有对应双亲结点指向它,它们,所以减去n-1。
按某条搜索路径访问树中的每个结点,树的每个结点均被访问一次,而且只访问一次。
遍历又分为三种:先序遍历,中序遍历,后序遍历。
中序遍历
若二叉树非空:
1.中序遍历左子树
2.访问根结点
3.中序遍历右子树
因为树是一种递归的数据结构,所以遍历的时候使用到递归算法。中序遍历的代码实现:
void InOrder(BiTree T) {
if (T == NULL) {
return
}
IneOrder(T->lchild);
visit(T);
IneOrder(T->rchild);
}
中序遍历非递归算法
算法思想:
1.初始时依次扫描根结点的所有左侧结点并将它们一一进栈;
2.出栈一个结点,访问它;
3.扫描该结点的右孩子结点并将其进栈;
4.依次扫描右孩子结点的所有左侧结点并一一进栈;
5.反复该过程直到栈空为止。
void InOrder(struct BiTNode *t) //t为根指针
{ struct BiTNode *st[maxleng];//定义指针栈
int top=0; //置空栈
do{
while(t) //根指针t表示的为非空二叉树
{ if (top==maxleng) exit(OVERFLOW);//栈已满,退出
st[top++]=t; //根指针进栈
t=t->lchild; //t移向左子树
} //循环结束表示以栈顶元素的指向为根结点的二叉树
//的左子树遍历结束
if (top) //为非空栈
{ t=st[--top]; //弹出根指针
printf("%c",t->data); //访问根结点
t=t->rchild; //遍历右子树
}
} while(top||t); //父结点未访问,或右子树未遍历
}
先序遍历
若二叉树非空:
1.访问根结点
2.先序遍历左子树
3.先序遍历右子树
用代码实现为:
void PreOrder(BiTree T) {
if (T == NULL) {
return
}
visit(T);
PreOrder(T->lchild);
PreOrder(T->rchild);
}
先序遍历非递归算法
算法思想与中序遍历一致,代码实现为:
void Preorder(struct BiTNode * t){
struct BiTNode * St[MaxSize], *p;
int top = 0; //置空栈
if (t! = NULL){
St[top++] = t;
while(top){
p = St[--top]; printf("%c ", p->data);
if(p->rchild != NULL)
St[top++] = p->rchild;
if(p->lchild != NULL)
St[top++] = p->lchild;
}
printf("\n");
}
}
后序遍历
若二叉树非空:
1.后序遍历左子树
2.后序遍历右子树
3.访问根结点
后续遍历的代码实现:
void PostOrder(BiTree T) {
if (T == NULL) {
return
}
PostOrder(T->lchild);
PostOrder(T->rchild);
visit(T);
}
先序遍历非递归算法
算法思想与中序遍历一致,代码实现为:
void Postorder(struct BiTNode * t){
struct BiTNode * St[MaxSize], *pre;
int flag, top = 0;
if (t != NULL){
do{
while(t != NULL){
St[top++] = t; t = t->lchild;
}
pre = NULL; flag = 1;
while(top && flag){
t = St[top-1];
if(t->rchild == pre){
printf(“%c ”, t->data); top--; pre = t;
}
else{ t=t->rchild; flag = 0;}
}
}while(top);
printf(“\n”);
}
}
层次遍历
所谓层次遍历就是一层一层遍历树的结点,从第0层开始。算法思想:
1.初始将根入队并访问根结点,然后出队;
2.若有左子树,则将左子树的根入队;
3.若有右子树,则将右子树的根入队;
4.然后出队,访问该结点;
5.反复该过程直到队列空为止;
代码实现为:
void levelOrder(BiTree T){
InitQueue(Q);
BItree p;
EnQueue(Q,T);
while(!isEmpty(Q)){
DeQueue(Q,p);
visit(p);
if(p->lchild!=NULL)
EnQueue(Q,p->lchild);
if(p->rchild!=NULL)
EnQueue(Q,p->rchild);
}
}
综上学习完所有的遍历我们看一个栗子回顾一下:
先序遍历序列: 124536
中序遍历序列: 425163
后序遍历序列: 452631
层次遍历序列:123456
由遍历序列构造二叉树
先(后)序遍历序列和中序遍历序列可以确定一棵二叉树,而后序遍历序列和先序遍历序列不可以确定一棵二叉树
中序遍历序列和先序遍历序列构造过程:
1.在先序序列中,第一个节点是根结点;
2.根结点将中序遍历序列划分为两部分;
3.然后在先序序列中确定两部分的结点,并且两部分的第一 个结点分别为左子树的根和右子树的根;
4.在子树中递归重复该过程,便能唯一确定一棵二叉树
线索二叉树就是将普通的二叉树线索化,线索化的具体实现为:
若无左子树,则将左指针指向其前驱结点;
若无右子树,则将右指针指向其后继结点。
结构化线索二叉树
结点结构:
标志域 | 左子树指针域 | 数据域 | 右子树指针域 | 标志域 |
---|---|---|---|---|
ltag | lchild | date | rchild | rtag |
其中:
ltag为0是指向该结点的左孩子,为1时指向该结点的前驱
rtag为0是指向该结点的右孩子,为1时指向该结点的后继
代码实现为:
//二叉树线索结点存储结构
typedef struct ThreadNode{
ElemType date; //结点数据
struct ThreadNode *lchild, *rchild; //左右孩子指针
int ltag, rtag; //左右标志
}ThreadNode, *ThreadNode;
这种结点结构构成的二叉链表作为二叉树的存储结构,称为线索链表
这里的前驱结点,后继结点就需要看具体的遍历方式来决定了,如:
先序遍历序列为:124536;则线索化为
中序遍历序列为:425163;则线索化为:
后序遍历序列为:452631;则线索化为:
这三种遍历方式我们最常用的就是中序遍历了,接下来看看如何用代码实现中序遍历的线索化。
中序线索二叉树
中序遍历序列为:425163;则线索化为:
前驱结点:
若左指针为线索,则其指向结点为前驱结点
若左指针为左孩子,则其左子树的最右侧结点为前驱结点
后驱结点:
若右指针为线索,则其指向结点为后驱结点
若右指针为右孩子,则其右子树的最左侧结点为后驱结点
代码实现:
//初始化
void CreateInThread(ThreadTree T){
TheradTree pre = NULL;
if(T!=NULL){
InThread(T,pre);
pre->rchild = NULL:
pre->rtag=1;
}
}
//线索化
void InThread(ThreadTree &p,ThreadTree &pre){
if(p!=NULL){
InThread(p->lchild,pre);
if(p->lchild==NULL){
p->lchild = pre;
pre->rtag = 1;
}
pre = p;
InThread(p->rchild,pre);
}
}
//中序线索二叉树遍历
ThreadNode *Firstnode(ThreadNode *p){
while(p->tag==0)
p=p->lchild;
return p;
}
ThreadNode *Nextnode(ThreadNode *p){
if(p->rtag==0)
return Firstnode(p->rchild);
else
return p->rchild;
}
void Inorder(ThreadNode *T){
for(ThreadNode *p=Firstnode(T);p!=NULL;p=Nextnode(p))
visit(p);
}
之前学的二叉树只是树的其中一种,是比较特殊的一种树,现在我们来学习普通的树。
树的存储结构不是顺序存储与链式存储,它比较特殊有三种分别是:双亲表示法,孩子表示法,孩子兄弟表示法。
采用一组连续的存储空间来存储每个结点,同时在每个节点中增设一个伪指针,指示双亲结点在数组中的位置。根结点的下标为0,其伪指针域为-1。
这里的连续存储空间还是数组,伪指针就是双亲结点所在数组下标,虽然不是指针但是却有指针的作用。
用代码实现为:
#define MAX_TREE_SIZE 100
//每一个节点结构
//包括数据域和指向双亲结点的伪指针域
typedef struct{
ElemType date;
int parent;
}PTNode;
//
typedef struct{
PTNode nodes[MAX_TREE_SIZE];
int n;
}PTree;
将每个结点的孩子结点都用单链表连接起来形成一个线性结构,n个结点具有n个孩子链表。
用代码实现为:
#define MAX_TREE_SIZE 100
typedef struct{
int child;
struct CNode *next;
}CNode;
typedef struct{
ElemType date;
struct CNode *child;
}PNode;
typedef struct{
PNode nodes[MAX_TREE_SIZE];
int n;
}CTree;
以二叉链表作为树的存储结构,又称二叉树表示法或者左孩子右兄弟表示法
结构如下:
指针域 | 数据域 | 指针域 |
---|---|---|
结点第一个孩子指针 | 结点值 | 结点下一个兄弟结点指针 |
代码实现为:
typedef struct CSNode{
ElemType date;
struct CSNode *firstchild,*nextsibling;
}CSNode,CSTree;
学习完树的存储结构,那他们各自有什么优缺点呢?总结了一张表大家可以看看:
`` | 优点 | 缺点 |
---|---|---|
双亲表示法 | 寻找结点的双亲结点效率高 | 寻找结点的孩子结点效率低 |
孩子表示法 | 寻找结点的孩子结点效率高 | 寻找结点的双亲结点效率低 |
孩子兄弟表示法 | 寻找结点的孩子结点效率高方便实现树转换为二叉树 | 寻找结点的双亲结点效率低 |
之前我们已经系统的学习了树、森林以及二叉树,接下来我们就来学习它们之间的转换把!
树的存储结构有三种,我们要实现树与二叉树的转换,我们的树的存储结构必须得是孩子兄弟表示法,因为孩子兄弟表示法是由二叉树存储结构转换的。
转换规则为:
每个结点左指针指向它的第一个孩子结点,右指针指向它在树中相邻兄弟结点
森林其实就是很多树的一个集合,它转换为二叉树的步骤为:
将每一棵树转换为二叉树,将每棵二叉树的根依次作为上一棵二叉树的右子树
由二叉树转换为树其实就是以上过程的逆过程,也是类似的。
之前我们学习过二叉树的遍历,它是先序遍历,中序遍历,后序遍历。但是普通的树没有左右子树之分,所以树与森林的遍历与二叉树的遍历就不同了
树的遍历指按照某种方式访问树中的每个结点,且仅访问一次。它有三种分别是:先根遍历,后根遍历和层次遍历。
层次遍历与二叉树的层次遍历相同,都比较简单就不做叙述了。
先根遍历
若树非空,则先访问根结点,再按从左到右的顺序遍历根结点的每棵子树。
例如对于这棵树,它的遍历顺序为:RADEBCFGHK
我们把这棵树转换为二叉树,为:
它的先序遍历次序为:RADEBCFGHK
我们会发现
树的先根遍历序列与这棵树对应二叉树的先序遍历序列相同
后根遍历
若树非空,则先按从左到右的顺序遍历根结点的每棵子树,再访问根结点
还是这棵树,它的后根遍历序列为:DEABGHKFCR
我们把这棵树转换为二叉树,为:
它的中序遍历次序为:DEABGHKFCR
我们会发现:
树的后根遍历序列与这棵树对应二叉树的中序遍历序列相同
森林的遍历有两种:先序遍历和中序遍历。接下来我们分别看一下吧!
先序遍历
若森林非空,则:
1.访问森林中第一棵树的根结点
2.先序遍历第一棵树的子树森林
3.先序遍历除去第一棵树之后剩余的树构成的子树森林
例如:对于下面这个森林它的先序遍历次序为:ADEBCFGHK
这个森林对应的二叉树为:
它的先序遍历次序为:ADEBCFGHK
我们会发现:
森林的先序遍历序列与森林对应二叉树的先序遍历序列相同
中序遍历
若森林非空,则:
1.中序遍历第一棵树的根结点的子树森林
2.访问第一棵树的根结点
3.中序遍历除去第一棵树之后剩余的树构成的子树森林
下面这个森林对应的中序遍历次序为:DEABGHKFC
这个森林对应的二叉树为:
这个二叉树对应中序遍历序列为:DEABGHKFC
我们会发现:
森林的中序遍历序列与森林对应二叉树的中序遍历序列相同
至此我们已经学习完了二叉树的遍历,树的遍历,森林的遍历。接下来我们看看他们之间有什么对应关系:
树 | 森林 | 二叉树 |
---|---|---|
先根遍历 | 先序遍历 | 先序遍历 |
后根遍历 | 中序遍历 | 中序遍历 |
大家可以根据这个表格来记忆它们的相同与不同。
到这里我们所有的关于树与二叉树的基础完全学习完了。文章很长,感谢能够读完的兄弟们!最近马上期末考试了,花了我近一周时间,希望能对你们有所帮助,感谢各位的支持!
下一篇:二叉树的应用
持续更新中…