树和二叉树的定义
1、树(Tree):是n(n>=0)个结点的有限集。
- 若n = 0,称为空树;
- 若n > 0,则它满足如下两个条件:
1)有且仅有一个特定的称为根的结点;
2)其余结点可分为m(m>=0)个互不相交的有限集T1,T2,T3,…,Tm,其中每一个集合本身又是一棵树,并称为根的子树。
2、树的基本术语
- 结点:树中的一个独立单元。
- 结点的度:结点拥有的子树数成为结点的度。
- 树的度:树的度是树内各结点度的最大值。
- 叶子:度为0的结点称为叶子或中端结点。
- 非终端结点:度不为0的结点称为非终端结点或分支结点。
- 双亲和孩子:结点的子树的根称为该结点的孩子,相应地,该结点称为孩子的双亲。,。
- 兄弟:同一个双亲的孩子之间互称兄弟。
- 祖先:从根到该结点所经分支上的的所有结点。
- 子孙:以某结点为根的子树中的任一结点都称为该结点的子孙。
- 层次:结点的层次从根开始定义起。
- 堂兄弟:双亲在同一层的结点互为堂兄弟但不为同一个双亲。
- 树的深度:树种结点的最大层次称为树的深度或高度。
- 有序树和无序树:如果将树中结点的各子树看成从左只有是有次序的(即不能互换),则称该树为有序树,否则称为无序树。(在有序树中最左边的子树的根称为第一个孩子,最右边的称为最后一个孩子)
- 森林:是m(m>=0)棵互不相交的树的集合。
3、二叉树的定义
定义-> 二叉树是n(n>=0)个结点所构成的集合,它或为空树(n=0);或为非空树。
对于非空二叉树:
- 有且仅有一个称之为根的结点;
- 除根结点以外的其余结点分为两个互不相交的子集T1和T2,分别称为T的左子树和右子树,且T1和T2本身又都是二叉树。
4、二叉树和树的区别
-二叉树的每个结点至多只有两棵子树(即二叉树中不存在度大于2的结点)。
- 二叉树的子树有左右之分,其次序不能任意颠倒。
5、特殊二叉树
(1)满二叉树
- 满二叉树:一颗深度为k且有2^k-1个结点的二叉树称为满二叉树
- 特点
1)每一层上的结点数都是最大结点数(即每层都满)。
2)叶子节点全部在最底层。
3)满二叉树在同样深度的二叉树中结点个数最多
4)满二叉树在同样深度的二叉树中叶子结点个数最多
-对满二叉树进行编号
1)规则:从根结点开始,自上而下,自左而右。
2)每一结点位置都有元素。
(2)完全二叉树:
-定义:深度为k的具有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号为1~n的结点一一对应时,称之为完全二叉树。
对上图满二叉树来说
以下情况为完全二叉树
以下情况为非完全二叉树
-特点:
1)叶子只可能分布在层次最大的两层上。
2)对任一结点,如果其右子树的最大层次为i,则其左子树的最大层次必为i或i+1。
-关系
满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树
二叉树的性质和存储结构
1、二叉树的性质
- 在二叉树的第i层上至多有1^(i-1)个结点(i>=1)。
- 深度为k的二叉树至多有2^k-1个结点(k>=1).
- 对任何一棵二叉树T,如果叶子数为n0,度为2的结点数为n2,则n0=n2+1
- 具有n个结点的完全二叉树的深度为⎿log2n⏌+1
- 如果对一棵树有n个结点的完全二叉树(深度为⎿log2n⏌+1)的结点按层序编号(从第一层)
到第⎿log2n⏌+1层,每层从左到右),则对任意结点i(1<=i<=n),有:
1)如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点⎿i/2⏌。
2)如果2i>n,则结点i为叶子结点,无左孩子;否则,其左孩子是结点2i。
3)如果2i+1>n,则结点i无右孩子;否则,其右孩子是结点2i+1。
2、二叉树的存储结构:
二叉树的顺序存储
->按满二叉树的结点层次编号,依次存放二叉树中的数据元素。
//仅供理解
#define MAXTSIZE 100
Typedef TElemType SqBiTRee[MAXTSIZE]
SqBiTree bt;
假设有
这样一棵二叉树,那么它的存储形式为:
上图(root
)所示。
二叉树的顺序存储区缺点:
最坏情况:深度为k的且只有k个结点的单支树需要长度为2^k -1的一维数组。
特点:
结点间关系蕴含在其存储位置中
浪费空间,适于存满二叉树和完全二叉树。
3、二叉树的链式存储结构:
typedef struct BiNode{
TElemType data;
struct BiNode *lchild,*rchild;
}BiNode,*BiTree;
如图:
对root图来说,其二叉链表形式如下:
–>在n个结点的二叉链表中,有n+1
个空指针域
空指针数目 = 2n(总链域) - (n-1)(存放指针的链域) = n+1
三叉链表
(哈夫曼树)
typedef struct TriTNode{
TelemType data;
struct TriTNode *lchild,*parent,*rchild;
}TriTNode,*TriTree;
对三叉链表来说,只是在二叉链表的基础上多了一个指向双亲结点的指针。
如图:
遍历二叉树
1、遍历的定义、目的及用途
定义:顺着某一条搜索路径巡防二叉树中的结点,使得每个结点均被访问一次,
而且仅被访问一次(又称周游)。
目的:得到树中所有结点的一个线性排列
用途:它是树结构插入、删除、修改、查找和排序运算的前提,是二叉树一切运算
的基础和核心
2、二叉树的排列方式
L:遍历左子树
D:访问根结点
R:遍历右子树
方案:
DLR 、LDR 、 LRD 、DRL 、RDL 、RLD
其中常用的是:
DLR – 先序遍历
LDR – 中序遍历
LRD – 后序遍历
其中先中后都是指的根的位置
3、二叉树的遍历
(1)先序遍历:
若二叉树为空,则为空操作
否则
访问根节点
先序遍历左子树
先序遍历右子树
对root图来说,中序遍历之后的顺序为:
a - e - f - g - b - d - h - c
(2)中序遍历:
若二叉树为空,则为空操作
否则
中序遍历左子树
访问根节点
中序遍历右子树
对root图来说,后序遍历之后的顺序为:
g - f- e - a - h - d - b - c
(3)后序遍历:
若二叉树为空,则为空操作
否则
中序遍历左子树
中序遍历右子树
访问根节点
对root图来说,后序遍历之后的顺序为:
g - f - e - h - d - c - b - a
4、二叉树遍历的算法(运用递归):
1)先序遍历
Status PreOrderTraverse(BiTree T){
if(T == NULL) return OK;//判断是否为空
else{
visit(T);//访问根节点-->cout<<T->data<<endl;
PreOrderTraverse(T->lchild);//递归遍历左子树
PreOrderTraverse(T->rchild);//递归遍历右子树
}
2)中序遍历
Status PreOrderTraverse(BiTree T){
if(T == NULL) return OK;//判断是否为空
else{
PreOrderTraverse(T->lchild);//递归遍历左子树
visit(T);//访问根节点-->cout<<T->data<<endl;
PreOrderTraverse(T->rchild);//递归遍历右子树
}
3)后序遍历
Status PreOrderTraverse(BiTree T){
if(T == NULL) return OK;//判断是否为空
else{
PreOrderTraverse(T->lchild);//递归遍历左子树
PreOrderTraverse(T->rchild);//递归遍历右子树
visit(T);//访问根节点-->cout<<T->data<<endl;
}
我们可以发现运用递归的思想,先序、中序、后序的遍历只是改变了访问根节点的位置。
下面我们不用递归来实现遍历二叉树。
5、遍历二叉树的非递归算法
中序遍历非递归算法
关键:在中序遍历过某结点的整个左子树后,如何找到该结点的根以及右子树。
基本思想:
①建立一个栈。
②根结点进栈,遍历左子树。
③根结点出栈,输出根结点,遍历右子树。
Status InOrderTraverse(BiTree T){
BiTree p;
InitStack(S);
p =T;
while(p || !StackEmpty(S)){
if(p){ //p非空
Push(S,p); //根指针进栈
p = p->lchild; //根指针进栈,遍历左子树
}
else{ //p为空
Pop(S,q); //出栈
cout<<q->data<<endl; //访问根节点
p = q->rchild; //遍历右子树
}
}
retuen OK;
6、二叉树的层次遍历
算法思路:
1)将根结点入队;
2)队不空时循环:从队列中出列一个结点*p,访问它;
①:若它有左孩子结点,将左孩子结点进队;
②:若它有右孩子结点,将右孩子结点进队。
typedef struct{
BTNode data[MaxSize]; //存放队中元素
int front,rear; //队头和队尾指针
}SqQueue; //顺序循环队列类型
void LevelOrder(BTNode *b){
BTNode *p;
SqQueue *q;
InitQueue(q); //初始化队列
enQueue(q,b); //根结点指针进入队列
while(!QueueEmpty(q)){ //队不为空,则循环
deQueue(q,p); //出队结点p
cout<<p->data<<endl; //访问结点p
if(p->lchild != NULL) enQueue(q,p->lchild); //有左孩子时将其进队
if(p->rchild != NULL) enQueue(q,p->rchild); //有有孩子时将其进队
}
}
7、二叉树的建立:
1)按先序遍历序列建立二叉树的二叉链表
例:已知先序序列为:
ABCDEGF
①从键盘输入二叉树的结点信息,建立二叉树的存储结构;
②在建立二叉树的过程中按照二叉树先序方式建立。
如要实现上图需插入字符的顺序为:
ABC##DE#G##F###
Status CreateBiTree(BiTree &T){
scanf(&c);//-->cin>>c;
if(c == '#') T = NULL; //递归结束,建空树
else{ //递归建立二叉树
if(!(T = BiTNode*)malloc(sizeof(BiTNode)))
exit(OVERFLOW);//T = new BiTNode;
T->data = c;//生成根结点
CreateBiTree(T->lchild); //构造左子树
CreateBiTree(T->rchild); //构造右子树
}
return OK;
}
8、复制二叉树(利用先序遍历):
思想:
1)如果是空树,递归结束;
2)否则,申请新结点空间,复制根结点
递归复制左子树
递归复制右子树
Status 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); //递归创建右子树
}
}
9、计算二叉树的深度:
思想:
1)如果是空树,则深度为0;
2)否则,递归计算左子树的深度记为m,递归计算右子树的深度记为n,二叉树的深度则为m和n的较大者加1。
Status Depth(BiTree T){
if(T == NULL) return 0;
else{
m = Depth(T->lChild);
n = Depth(T->rChild);
if(m > n) return(n+1);
else return(n+1);
}
}
10、计算二叉树结点总数
1)如果是空树,则结点个数为0;
2)否则,结点个数为左子树的结点个数 + 右子树的结点个数再+1。
Status NodeCount(BiTree T){
if(T == NULL)
return 0;
else
return NodeCount(T->lchild) + NodeCount(T->rchild) + 1;//加1是加上了根结点
}
11、计算叶子结点的总数:
思想:
1)如果是空树,则结点个数为0;
2)否则,为左子树的叶子结点个数 + 为右子树的叶子结点个数。
Status LeafCount(BiTree T){
if(T == NULL) //如果是空树返回0
return 0;
if(T->lchild == NULL && T->rchild == NULL)
return 1;//如果根结点是叶子结点返回1
else
return LeafCount(T->lchild) + LeafCount(T->rchild);//左子树叶子结点 + 右子树叶子结点
}
线索二叉树
1、 前提:利用二叉链表中的空指针域
–>如果某个结点的左孩子为空,则将空的左孩子指针域改为指向其前驱;
如果某结点的右孩子为空,则将空的右孩子指针域改为指向其后继
–这种改变指向的指针称为"线索"
加上了线索的二叉树称为线索二叉树。
对二叉树按某种遍历次序使其变为线索二叉树的过程叫线索化。
线索二叉树的结点结构为
lchild | ltag | data | rtag | rchild |
---|
存储结构如下:
typedef struct BiThrNode{
int data;
int ltag,rtag;
struct BiThrTree *lchild,rchild;
}BiThrNode,*BiThrTree;
一般情况下我们默认为:
ltag为 0是指向该结点的左孩子,为 1时指向该结点的前驱
rtag为 0是指向该结点的右孩子,为 1时指向该结点的后继
我们仍然对root图来说
按中序遍历的话,结点的顺序为:
g - f - e - a - h - d - b - c
则线索二叉树如图:
如果我们遍历二叉树的首个结点的左子树为空的话,则没有前驱,同理,最后一个结点的右子树为空的话没有后继,所以我们来增设一个头结点
作用:
操作方便,避免指针悬空
如下图: