树形结构(非线性结构):结点之间有分支,具有层次关系。
树(Tree)是n(n≥0)个结点的有限集。
**树是n个结点的有限集。**显然,树的定义是一个递归的定义。
树的其他表示形式:
**根结点:**非空树中无前驱结点的结点。
**结点的度:**结点拥有的子树数。
**树的度:**树内各结点的度的最大值。
**叶子结点:**度为0的点,也称为终端结点。
**分支结点:**度≠0的结点,也称为非终端结点。
**内部结点:**根结点以外的分支结点称为内部结点。
**双亲结点:**结点的子树的根称为该结点的孩子,该结点称为孩子的双亲。
**兄弟结点:**结点之前有共同的双亲结点称为兄弟结点。
**堂兄弟结点:**双亲在同一层的结点。
**结点的祖先:**从根到该结点所经分支上的所有结点。
**结点的子孙:**以某结点为根的子树中的任一结点。
**树的深度:**树中结点的最大层次。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XagGN3sY-1691068168684)(https://ts1.cn.mm.bing.net/th/id/R-C.fe38e3b271e2321ef483becbb761c23b?rik=2QonkZPTVvq%2btg&riu=http%3a%2f%2fpic.baike.soso.com%2fp%2f20131206%2f20131206141836-722390134.jpg&ehk=mNDvDkeq8qFKHHsXCLhiWhru8%2fGKWK1lU%2f3sEGzBvh4%3d&risl=&pid=ImgRaw&r=0&sres=1&sresct=1)]
有序树:树中结点的各子树从左至右有次序(最左边的为第一个孩子)。
**无序树:**树中结点的各子树无次序。
**森林:**是m(m≥0)颗互不相交的树的集合。把根结点删除树就变成了森林。
一棵树可以看成是一个特殊的森林。
给森林中的各子树加上一个双亲结点,森林就变成了树。
树一定是森林,但森林不一定是树。
线性结构 | 树结构 | ||
---|---|---|---|
第一个数据元素 | 无前驱 | 根结点(只有一个) | 无双亲 |
最后一个数据元素 | 无后继 | 叶子结点(可以有多个) | 无孩子 |
其他数据元素 | 一个前驱,一个后继 | 其他结点中间结点 | 一个双亲,多个孩子 |
一对一 | 一对多 |
为什么要重点研究每结点最多只有两个“叉”的树?
普通树(多叉树)若不转化为二叉树,则运算很难实现
二叉树在树结构的应用中起着非常重要的作用,因为对二叉树的许多操作算法简单,而任何树都可以与二叉树相互转换,这样就解决了树的存储结构及其运算中存在的复杂性。
二叉树是n(n≥0)个结点的有限集,它或者是空集(n=0),或者由一个根结点及两颗互不相交的分别称作这个根的左子树和右子树的二叉树组成。
特点:
注意:二叉树不是树的特殊情况,它们是两个概念。
二叉树结点的子树要区分左子树和右子树,即使只有一颗子树也要进行区分,说明它是左子树,还是右子树。
树当结点只有一个孩子时,就无须区分他是左还是右的次序。因此,二者是不同的。这是二叉树与树的最主要的区别。
也就是二叉树每个结点位置或者说次序都是固定的,可以是空,但是不可以说它没有位置,而树的结点位置是相对于别的结点来说的,没有别的结点时,它就无所谓左右了。
二叉树的五种基本形态
注意:虽然二叉树与树概念不同,但有关树的基本术语对二叉树都适用。
CreateBiTree(&T,definition)
初始条件:definition给出二叉树T的定义。
操作结果:按definition构造二叉树T。
PreOrderTraverse(T)
初始条件:二叉树T存在。
操作结果:先序遍历T,对每个结点访问一次。
InOrderTraverse(T)
初始条件:二叉树T存在。
操作结果:中序遍历T,对每个结点访问一次。
POSTOrderTraverse(T)
初始条件:二叉树T存在。
操作结果:后序遍历T,对每个结点访问一次。
性质1:在二叉树的第 i 层上至多有 2i-1个结点(i≥1)。
性质2:深度为 k 的二叉树至多有2k-1个结点(k≥1)。
深度为k时至少有 k 个结点。
性质3:对任何一颗二叉树T,如果其叶子数为n0,度为2的结点数为n2,则 n0=n2+1。
总边数为B B = n - 1 或 B = n2 * 2 + n1 * 1
总结点数为n n = n2 * 2 + n1 * 1 + 1 或者 n = n2 + n1 + n0
一棵深度为 k 且有 2k-1 个结点的二叉树称为满二叉树。
特点:
对满二叉树结点位置进行编号
满二叉树在同样深度的二叉树中结点个数最多。
满二叉树在同样深度的二叉树中叶子结点个数最多。
深度为k的具有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号为1~n的结点——对应时,称之为完全二叉树。
注意:在满二叉树中,从最后一个结点开始,连续去掉任意个结点,即是一颗完全二叉树。
一定是连续的去掉!!!
特点:
满二叉树一定是完全二叉树,完全二叉树不一定满二叉树。
**性质4:**具有 n 个结点的完全二叉树的深度为 [log2n] + 1。
注意:[ x ]:称作x的底,表示不大于x的最大整数。
表明了完全二叉树结点数n与完全二叉树深度k之间的关系。
性质5:如果对一棵有n个结点的完全二叉树(深度为[ log2n ]+1)的结点按层序编号(从第1层到第[log2n]+1层,每层从左到右),则对任一结点 i (1≤ i ≤ n)有:
表明了完全二叉树中双亲结点编号与孩子结点编号之间的关系。
二叉树的存储结构分为顺序存储结构以及链式存储结构(二叉链表,三叉链表)。
实现:按满二叉树的结点层次编号,依次存放二叉树中的数据元素。
二叉树顺序存储表示:
#define MAXSIZE 100
typedef int SqBiTree[MAXSIZE];
SqBiTree bt;
二叉树的顺序存储缺点:
特点:
二叉链表存储结构:
typedef struct BiNode {
int data;
struct BiNode* lchild, * rchild;//左右孩子
}BiNode,*BiTree;
在n个结点的二叉链表,有__n+1__个空指针域。
分析:必有2n个链域。除根结点外,每个结点有且仅有一个双亲,所以只会有 n-1 个结点的链域存放指针,指向非空子女结点。
空指针数目 = 2n -(n-1)=n+1
三叉链表
typedef struct TriTNode{
int data;
struct TriTNode *lichild,*parent,*rchild;
}TriTNode,*TriTree;
遍历定义——顺着某一条搜索路径巡访二叉树中的结点,使得每个结点均被访问一次,而且仅被访问一次(又称周游)。
遍历目的——得到树中所有结点的一个线性排列。
遍历用途——它是树结构插入、删除、修改、查找和排序运算的前提,是二叉树一切运算的基础和核心。
遍历方法:依次遍历二叉树中的三个组成部分,便是遍历了整个二叉树。
则遍历整个二叉树方案共有:
ABC、ACB、BAC、BCA、CAB、CBA六种。
若规定先左后右,则只有前三种情况:
ABC —— 先(根)序遍历,
BAC —— 中(根)序遍历,
BCA —— 后(根)序遍历。
先序遍历二叉树 | 中序遍历二叉树 | 后序遍历二叉树 |
---|---|---|
若二叉树为空,则空操作; 否则 | 若二叉树为空,则空操作; 否则 | 若二叉树为空,则空操作; 否则 |
(1)访问根结点; | (1)中序遍历左子树; | (1)后序遍历左子树; |
(2)先序遍历左子树; | (2)访问根结点; | (2)后序遍历右子树; |
(3)先序遍历右子树。 | (3)中序遍历右子树。 | (3)访问根结点。 |
由二叉树的递归定义可知,遍历左子树和遍历右子树可如同遍历二叉树一样“递归”进行。
若二叉树为空,则空操作;若二叉树非空,
访问根结点(D)
前序遍历左子树(L)
前序遍历右子树(R)
bool PreOrderTraverse(BiTree T) {
if (T == NULL) return true;//空二叉树
else {
visit(T);//访问根结点
PreOrderTraverse(T->lchild);//递归遍历左子树
PreOrderTraverse(T->rchild);//递归遍历右子树
}
}
void Pre(BiTree T) {
if (T != NULL) {
printf("%d\t", T->data);
Pre(T->lchild);
Pre(T->rchild);
}
}
若二叉树为空,则空操作;若二叉树非空,
中序遍历左子树(L)
访问根结点(D)
中序遍历右子树(R)
bool InOrderTraverse(BiTree T) {
if (T == NULL) return true;
else {
InOrderTraverse(T->lchild);//递归遍历左子树
visit(T);//访问根结点
InOrderTraverse(T->rchild);//递归遍历右子树
}
}
若二叉树为空,则空操作;若二叉树非空,
后序遍历左子树(L)
后序遍历右子树(R)
访问根结点(D)
bool PostOrderTraverse(BiTree T) {
if (T == NULL) return true;
else {
PostOrderTraverse(T->lchild);//递归遍历左子树
PostOrderTraverse(T->rchild);//递归遍历右子树
visit(T);//访问根结点
}
}
如果去掉输出语句,从递归的角度看,三种算法是完全相同的,或者说这三种算法的访问路径是相同的,只是访问结点的时机不同。
从虚线的出发点到终点的路径上,每个结点都要经过 3 次。
第一次经过时访问 = 先序遍历
第二次经过时访问 = 中序遍历
第三次经过时访问 = 后序遍历
二叉树中序遍历的非递归算法的关键:在中序遍历过某结点的整个左子树后,如何找到该结点的根以及右子树。
基本思想:
bool InOrderTraver(BiTree T) {
BiTree p,q;
SqStack S;
InitStack(S);
p = T;
while (p||!StackEmpty(S))
{
if (p) {
Push(S, p);
p = p->lchild
}
else {
Pop(S, q);
printf("%c", q->data);
p = q->rchild;
}
}
return true;
}
对于一颗二叉树,从根结点开始,按从上到下,从左到右的顺序访问每一个结点。每个结点仅仅访问一次。
**算法设计思路:**使用一个队列
使用队列类型定义如下:
void LevelOrder(BTNode *b){
BTNode *p;
SqQueue *qu;
InitQueue(qu);
EnQueue(qu,b);//根结点指针进入队列
while(!QueueEmpty(qu)){
DeQueue(qu,p);//出队结点p
printf("%c",p->data);
if(p->lchild!=NULL)
enQueue(qu,p->lchild);//有左孩子时将其进队
if(p->rchild!=NULL)
enQueue(qu,p->rchild);//有右孩子时将其进队
}
}
按先序遍历序列建立二叉树的二叉链表
例:已知先序序列为:ABCDEGF
(1)从键盘输入二叉树的结点信息,建立二叉树的存储结构;
(2)在建立二叉树的过程中按照二叉树先序方式建立。
对于上图所示二叉树,按下列顺序读入字符:
ABC##DE#G##F###
bool CreatBiTree(BiTree& T) {
char* ch;
scanf("%c", & ch);
if (ch == "#") T = NULL;
else {
if (!(T = (BiNode*)malloc(sizeof(BiNode))))
return false;
T->data = ch;//生成根结点
CreatBiTree(T->lchild);//构建左子树
CreatBiTree(T->rchild);//构建右子树
}
return true;
}
算法思想:
int Copy(BiTree T, BiTree& NewT) {
if (T == NULL) {
NewT = NULL;
return 0;
}
else {
NewT = new BiNode;
NewT->data = T->data;
Copy(T->lchild, NewT->lchild);
Copy(T->rchild, NewT->rchild);
}
}
int Depth(BiTree T) {
if (T == NULL) return 0;//如果是空树返回0
else {
int m = Depth(T->lchild);
int n = Depth(T->rchild);
if (m > n) return(m + 1);
else return(n + 1);
}
}
int NodeCount(BiTree T) {
if (T == NULL)
return 0;
else
return NodeCount(T->lchild) + NodeCount(T->rchild) + 1;
}
int LeafCount(BiTree T) {
if (T == NULL)
return 0;//如果是空树返回0
if (T->lchild == NULL && T->rchild == NULL)
return 1;//如果是叶子结点返回1
else
return LeafCount(T->lchild) + LeafCount(T->rchild);
}
问题:为什么要研究线索二叉树?
当用二叉链表作为二叉树的存储结构时,可以很方便地找到某个结点的左右孩子;但一般情况下,无法直接找到该结点在某种遍历序列中的前驱和后继结点。
提出的问题:如何寻找特定遍历序列中二叉树结点的前驱和后继?
解决方法:
回顾:二叉树链表中空指针域的数量:
利用二叉链表中的空指针域:
如果某结点的左孩子为空,则将空的左孩子指针域改为指向其前驱;如果某结点的右孩子为空,则将空的右孩子指针域改为指向其后继——这种改变指向的指针称为“线索”
加上了线索的二叉树称为线索二叉树(Threaded Binary Tree)
对二叉树按某种遍历次序使其改变为线索二叉树的过程叫线索化。
为了区分lchild和rchild指针到底是指向孩子的指针,还是指向前驱或者后继的指针,对二叉链表中每个结点增设两个标志域ltag和rtag,并约定:
ltag = 0 lchild指向该结点的左孩子
ltag = 1 lchild指向该结点的前驱
rtag = 0 rchild指向该结点的右孩子
rtag = 1 rchild指向该结点的后继
线索二叉树结点的结构为:
typedef struct BiThrNode {
int data;
int ltag, rtag;
struct BiThrNode* lchild, * rchild;
}BiThrNode,*BiThrTree;
树(Tree)是n(n≥0)个结点的有限集。若n=0,称为空树;若n>0
森林:是m(m≥0)棵互不相交的树的集合。
实现:定义结构数组存放树的结点,每个结点含两个域:
C语言的类型描述:
typedef struct PTNode{
TElemType data;
int parent;//双亲位置域
}PTNode;
树结构:
#define MAXSIZE 100
typedef struct{
PTNode nodes[MAXSIZE];
int r,n;//根结点的位置和结点个数
}PTree;
把每个结点的孩子结点排列起来,看成是一个线性表,用单链表存储,则n个结点由n个孩子链表(叶子的孩子链表为空表)。而n个头指针又组成一个线性表,用顺序表(含n个元素的结构数组)存储。
C语言的类型描述:
孩子结点结构:child | next
typedef struct CTNode{
int child;
struct CTNode *next;
}*ChildPtr;
双亲结点结构:data | firstchild
typedef struct{
TElemType data;
ChildPtr firstchild;//孩子链表头指针
}CTBox;
树结构:
typedef struct{
CTBox nodes[MAXSIZE];
int n,r;//结点数和根结点的位置
}CTree;
特点:找孩子容易,找双亲难。
也称为二叉树表示法,二叉链表表示法。
实现:用二叉链表作为树的存储结构,链表中每个结点的两个指针域分别指向其第一个孩子结点和下一个兄弟结点。
typedef struct CSNode{
ElemType data;
struct CSNode *firstchild,*nextsibling;
}CSNode,*CSTree;
将树转化为二叉树进行处理,利用二叉树的算法来实现对树的操作。
树转二叉树转换步骤:兄弟相连留长子
二叉树转树步骤:左孩右右连双亲,去掉原来右孩线
森林转换成二叉树:树边二叉根相连
二叉树转换成森林:去掉全部右孩线,孤立二叉再还原。
先根(次序)遍历:
若树不空,则先访问根结点,然后依次先根遍历各课子树。
后根(次序)遍历:
若树不空,则先依次后根遍历各棵子树,然后访问根结点。
按层次遍历:
若树不空,则自上而下自左而右访问树中每个结点。
将森林看作三部分构成:
先序遍历:依次从左至右对森林中的每一棵树进行先根遍历
若森林不为空,则
中序遍历:依次从左至右对森林中的每一颗树进行后根遍历
若森林不为空,则