在讲解了之前的数据结构后我们讲解一下最经典的数据结构二叉树
树
树(Tree)是n(n≥0)个结点的有限集。n=0时称为空树。
在任意一棵非空树中:
- 有且仅有一个特定的称为根(Root)的结点;
- 当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、…、Tm,其中每一个集合本身又是一个树,并且称为根的子树(SubTree)。
下图表示一棵树:
根的子树:
需要注意两点:
- n≥0时根节点是唯一的。
- m>0时,子树的个数没有限制,但他们一定是互不相交的。
树的结点包含一个数据元素及若干指向其子树的分支。结点拥有的子树数目称为结点的度(Degree),树的度是树内各结点的度的最大值。
根据树中的结点的子树的数目不同即度的不同可以将所有结点大致分为以下几类:
- 度为0的结点称为叶结点(Leaf)或终端结点;
- 度不为0的结点称为非终端结点或分支结点。除根结点之外,分支结点也称为内部结点。
我们可以对树进行分层,层数就是树的深度,比如上图的树的深度为4,每层结点之间的关系为:
树的存储结构有多种:
- 双亲表示法:在每个结点中附带其双亲结点的位置信息
- 孩子表示法:在每个结点中附带其孩子结点的位置信息
- 孩子兄弟表示法:让每个结点中存放第一个孩子和该结点的右兄弟的位置信息
我们主要研究的是树中的一种结构:二叉树
二叉树
二叉树是n个结点的有限集合,该集合或者为空集,或者由一个根节点和两棵互不相交的、分别称为根节点的左子树和右子树的二叉树组成
二叉树特点:
- 叶子结点只能出现在最下面两层
- 最下层的叶子一定集中在左部连续位置
- 倒数二层若有叶子结点,一定在右部连续位置
- 如果结点度为1,则该节点只有左孩子,不存在有右子树的情况
- 同样结点的二叉树,完全二叉树的深度最小
二叉树性质:
- 在二叉树的第i层上至多有2^(i-1)个结点。
- 在深度为k的二叉树至多有2k−1个结点。
- 对任何一个二叉树T,如果其终端节点数为n0,度为2的结点数为n2,则n0=n2+1。
- 具有n个结点的完全二叉树深度为[log2n]+1([x]表示不大于x的最大整数)。
- 如果对一棵有n个结点的完全二叉树(其深度为[log2n]+1)的结点按层序编号(从第1层到底[log2n]+1层,每层从左到右)对任一结点i有:
1. 如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点[i/2]
2. 如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i
3. 如果2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1
二叉树存储结构
二叉树顺序存储
二叉树的顺序存储结构就是用一维数组存储二叉树中结点,并且结点的存储位置也就是数组的下标能体现结点之间的逻辑关系,比如双亲关系与孩子的关系
上图的树在一维数组中的存储逻辑关系为
通过这样的存储结构就可以将完全二叉树完整的存入一维数组中,并能体现树中结点的逻辑关系,但对于一般二叉树可以按照完全二叉树进行编号,把不存在的结点设置为 ^
但是存在一种极端情况就是树是一棵右斜树
这时它在一维数组中的存储为
这样显然很浪费空间,多以我们提出了二叉树的链式存储
二叉链表
二叉树每个结点最多有两个孩子,所以设计一个数据域和两个指针域,这样的链表叫做二叉链表
data是数据域,lchild和rchild都是指针域,分别存放指向左孩子和右孩子的指针
代码实现为
# define MAXSIZE 1000
# define OK 1
# define ERROR 0
# define TRUE 1
# define FALSE 0
typedef int Status;
typedef int TElemType;
typedef struct BiTNode{
TElemType data;
struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;
遍历二叉树:
二叉树遍历是指从根节点出发,按照某种次序依次访问二叉树中所有的结点,使得每个结点被访问一次且仅被访问一次
1.前序遍历
若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树
遍历的顺序为: ABDGHCEIF
2.中序遍历
若树是空,则空操作返回,否则中序遍历根结点的左子树,然后访问根结点,最后中序遍历右子树
遍历的顺序为 : GDHBAEICF
3.后续遍历
若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后访问根结点
遍历的顺序为 : GHDBIEFCA
遍历的代码实现:
三种遍历都是通过递归实现:
前序遍历
void PreOrderTraverse(BiTree T){
if(T == NULL){
return;
}
printf("%c", T->data);
PreOrderTraverse(T->lchild);
PreOrderTraverse(T->rchild);
}
中序遍历
void InOrderTraverse(BiTree T){
if(T == NULL){
return;
}
InOrderTraverse(T->lchild);
printf("%c", T->data);
InOrderTraverse(T->rchild);
}
后序遍历
void PostOrderTraverse(BiTree T){
if(T == NULL){
return;
}
PostOrderTraverse(T->lchild);
PostOrderTraverse(T->rchild);
printf("%c", T->data);
}
这三种遍历中前、中、后是指访问树和子树根结点的次序,递归很好理解,需要注意递归需要基例,这样递归才不会一直执行下去
二叉树的创建:
二叉树也使用递归来进行创建,当然创建的方式和遍历类似,包括前序、中序和后序,但是这三种的任意一个输入顺序都不能唯一的确定一棵二叉树
但是在创建二叉树时,为了能让每个结点确认是否由左右孩子,对树进行了拓展:将二叉树中每个结点的空指针引出一个虚结点,其值为一特定值,比如“#”,我们称这种处理后的二叉树为拓展二叉树,拓展二叉树就可以做到一个输入顺序确定一棵二叉树
为了创建一棵和上图普通二叉树所示的一样的树,我们需要按照上图拓展二叉树的前序遍历顺序将树中各结点的数据进行录入:AB#D##C##,这样我们才能创建自己想要的二叉树
创建的代码实现为: