逻辑结构:非线性结构(一对多,一个前驱多个后继)、具有层次关系
树(Tree)是n(n≥0)个结点的有限集,n=0时称为“空树”,n>0时满足两个条件:
1.有且仅有一个根(Root)
2.其余结点可以分为m个互不相交的有限集T1,T2,T3..Tm。每个集合本身又是一颗树,称为子树
树可以使用嵌套集合、凹入表示、广义表表示
结点的度:结点拥有的子树数(分支结点),若度为0(没有子树,称为叶子结点)
树的度:树内各结点的度的最大值
有共同双亲的结点成为兄弟结点(同层),从跟到X结点路径上经过的全部结点称为结点的祖先(仙人)
有序树:树中结点的各子树从左至右有次序(相反为无序树)
森林:有m颗不相交的树的集合(树删掉跟结点,树可以视为特殊的森林 <一颗>)
二叉树
普通树如果不转换为二叉树将难以实现运算
二叉树结构简单、规律性强
所有树都可以转换成唯一对应的二叉树,不失一般性
特点: 1.不存在度大于2的结点(至多两个孩子)
2.子树有左右之分,顺序不能颠倒
3.可以是空集,根可以有空的左子树或右子树
若树只有一个孩子,则无需区分左右,否则都需要说明是左子树还是右子树
案例:使用表达式求解表达式的值
左子树放第一操作数、右子树放第二操作数,根结点存放运算符,使用后序遍历即可得出结果
二叉树的抽象数据定义
ADT BinaryTree{
数据对象D:
数据关系R: D=空集 R=Ø
Root唯一
Dj∩Dk=Ø
基本操作P: ...
}ADT BinaryTree
性质以及存储结构
性质1:i 层上至多有 个结点(i>=1),至少有 1 个节点
性质2:深度为k的二叉树至多有 个结点,至少有 k 个结点
性质3:叶子数为N0,度为2的结点数为n2,则 n0=n2+1
满二叉树:
深度为k且有个结点的二叉树
标号:从上到下,从左到右
完全二叉树:
满二叉树中编号为1~n的结点一一对应时,称为完全二叉树。
性质1:叶子结点只能在最后一层或者倒数第二层
性质2:对任意结点,如果右子树最大层次为i,左子树最大层次必为i或i+1
性质3:具有n个结点的完全二叉树的深度为⌊⌋+1
⌊ ⌋表示向下取整
性质4:n个结点的完全二叉树,对其任一节点i(1≤i≤n)有:
1.i=1,结点i为root;i>1,则其双亲结点⌊i/2⌋
2.2i>n,则i为叶子结点,无左孩子;否则,其左孩子结点为结点2i
3.如果2i+1>n,则结点i无右孩子,否则,其右孩子结点2i+1
顺序存储结构
将二叉中的元素按照满二叉树进行编号,一次存放二叉树中的数据元素
缺点:深度为k且只有k个结点的单支树可能需要空间为 的一维数组。
链式存储结构
定义
typedef struct BiNode{
TElemType data;
struct BiNode *lchild,*rchild;//左右孩子
}BinNode,*BiTree;
必有2n个链域,除根结点外,每个结点由且仅有一个双亲结点,所以只会有n-1个结点的链域存放指针,指向非空子女结点
空指针数目 = 2n - (n-1) = n+1
三叉链表(有一个指针指向双亲,方便控制前驱)
typedef struct TriNode{
TElemType data;
struct BiNode *lchild,*rchild,*parent;//左右孩子
}TriNode,*TriTree;
遍历二叉树
遍历--得到树中所有结点的线性排列
遍历方法:(D-根、L-左结点、R-右结点),左子树都在右子树前
先(根)序遍历--DLR
中(根)序遍历--LDR
后(根)序遍历--LRD
先序遍历 | 中序遍历 | 后序遍历 |
1.访问根 2.先序遍历左子树 3.先序遍历右子树 | 1.中序遍历左子树 2.访问根 3.中序遍历右子树 | 1.后续遍历左子树 2.后续遍历右子树 3.访问根 |
先序遍历(根左右)
后序遍历(左右根)
中序遍历(左根右)
技巧:从最左边的最下层子树数起,依照对应的顺序,当子树完全数完后再数其双亲结点对应的序列(例如后续遍历)
实例
每一层的子树都遵从这个规则。例如中序遍历时:
a和+完成后应该轮到x,但是x存在左子树和右子树,应当先遍历 x 的左子树b和根+,然后遍历x的右子树 -,而右子树有是一个子树故先遍历左子树c,然后遍历根-,最后遍历d,得到的结果就是 a + [b x (c - d)]
可由先序序列和中序序列,或后续序列和中序序列确定唯一的二叉树
先序序列的第一个元素、后序序列的最后一个元素必定是根
例1: 先序:ABCDEFGHIJ
中序:CDBFEAIHGJ
1.
2.
3.
例2
先序遍历的递归实现(DLR)
Status PreOrderTraverse(BiTreee T){
if(T == NULL) return OK;//空二叉树
else{
visit(T);//访问根结点
PreOrderTraverse(T->lchild);//遍历左子树
PreOrderTraverse(T->rchild);//遍历右子树
}
}
中序遍历递归实现
Status InOrderTraverse(BiTreee T){
if(T == NULL) return OK;//空二叉树
else{
InOrderTraverse(T->lchild);//遍历左子树
visit(T);//访问根结点
InOrderTraverse(T->rchild);//遍历右子树
}
}
后序遍历递归实现
Status PostOrderTraverse(BiTreee T){
if(T == NULL) return OK;//空二叉树
else{
PostOrderTraverse(T->lchild);//遍历左子树
PostOrderTraverse(T->rchild);//遍历右子树
visit(T);//访问根结点
}
}
三类遍历访问的路径完全一致,只是访问时机有所区别
从虚线出发到终点,每个结点经过3次
中序遍历非递归实现(栈)
思路: 1.建立一个栈
2.根结点进栈,遍历左子树
3.根结点出栈,输出根结点,遍历右子树
Status InOrderTraverse(BiTree T){
BitTree p; InitStack(S); p=T;
while(p || !StatckEmpty(S)){
if(p) {Push(S,p); p=p->lchild; }
else {Pop(S,q); print(p->data);
p=q->rchild;}//q是弹栈的元素
}
return OK;
}
通用思路:第一次访问到根后不急于输出根,先将其入栈,然后按照先左后右的顺序遍历当前结点的左右子树,当需要输出时再输出,
层次遍历(队列实现)
从根结点开始,从上到下,从左到右的顺序访问没一个结点
思路: 1.将根结点入队
2.队不为空时循环:从队列中列出一个结点*p,访问它:
1.若其有左孩子,将左孩子进队
2.若其有右孩子,将右孩子进队
void LevelOrder(BTNode *b){
BTNode *p; SqQueue *qu;
InitQueue(qu);
while(!QueueEmpty(qu)){
deQueue(qu,p);
print(p->data);
if(p->lchild!=NULL) enQueue(qu,p->lchild);//左孩子入队
if(p->rchild!=NULL) enQueue(qu,p->rchild);//右孩子入队
}
}