什么是树
- 是一种层次结构 (家谱 / 社会组织 / 图书馆 / 电脑的文件树)
- 层次结构效率更高 (数据管理的基本操作 : 查找)
查找 Search
- 根据某个给定关键字 K , 从集合R中找出关键字与K相同的记录
- 分为静态查找(集合中记录固定) : 没有插入和删除操作
和动态查找(集合中记录是变化的) : 除了查找还可能发生插入和删除 - 静态查找 例子 : 查字典
- 动态查找 例子 : - -
静态查找
方法1 顺序查找
用数组
哨兵的目的
时间复杂度为
方法2 二分查找
数据结构 : 二叉查找树 二叉查找树有两个属性 : 1. 所有结点都比左子树中的结点大 2. 所有结点都比其右子树的结点小
根据属性有如下性质 : 二叉查找树最小结点位于最顶端结点最左边的子树的末尾
最大结点位于最顶端结点最右边的子树的末尾
二叉树
定义
由根节点和左子树、右子树组成
特殊二叉树
- 斜二叉树 (相当于链表)
- 完美二叉树 / 满二叉树
- 完全二叉树 (相对于完美二叉树而言 允许缺少叶子结点)
重要性质
(1) 一个二叉树第 i 层的最大结点数为 :
(例如第三层最大节点数是 2 = 4 , i = 3)
(2) 深度为 k 的二叉树由最大结点总数为 : (满二叉树时)
(例如第三层最大节点总数为 1+2+4 = 7 )
(3) 对任何非空二叉树T , 若 n 表示叶结点的个数 , n 是度为2的非叶结点个数 , 那么两者满足关系
(度 : 结点的分支数)
操作集
- 判别二叉树是否为空 :
Boolean IsEmpty(BinTree BT)
- 遍历 , 按顺序访问每个结点 :
void Traversal(BinTree BT)
- 创建二叉树 :
BinTree CreateBinTree()
存储结构
- 顺序存储结构 (用数组实现)
**完全二叉树 : 从上至下、从左至右顺序存储
**性质(1) : 非根结点 (序号i>1)的父节点的序号是 i/2
例如 B C
性质(2) : 结点 i 的左孩子结点的序号是 **2i**
** , 有孩子序号是 2i+1
( 2i<=n
, 否则没有左孩子 / 2i+1<=n
, 否则没有右孩子 )
**对于一般二叉树 , 也可以采用这种结构 , 方法是补齐空结点 , 但这样会造成空间浪费
- 链表存储结构定义结构为 :
数据 + 左指针 + 右指针 = 一个元素 (^
表示空指针)
二叉树的遍历
(这里使用链式存储结构的遍历 , 主要有如下四种形式)
常用的遍历方法
- 先序遍历 : 根 -> 左子树 -> 右子树
PreOrderTraversal
- 中序遍历 : 左子树 -> 根 -> 右子树
InOrderTraversal
- 后序遍历 : 左子树 -> 右子树 -> 根
PostOrderTraversal
- 层次遍历 : 从上到下、从左到右
LevelOrderTraversal
先序遍历
过程为 :
- 访问根节点
- 先序遍历其左子树 (递归地)
先序遍历其右子树 (递归地)
void PreOrderTraversal( BinTree BT )
{
if(BT){ /*判断是否为空,不为空则继续,空则直接结束*/
printf("%d", BT->Data);
PreOrderTraversal(BT->Left);
PreOrderTraversal(BT->Right);
}
}
访问顺序 :
起始时指针指向 A 的位置 ; 对左子树递归 (B D F E) 然后回到A ; 对右子树递归 (C G H I )
先序遍历 : A B D F E C G H I
**中序遍历
过程为 :
中序遍历其左子树 (递归地)
- 访问根节点
- 中序遍历其右子树 (递归地)
先遍历左边 , 所以从最左边的子节点 D 开始遍历 , 左 根 右的顺序void InOrderTraversal( BinTree BT ) { if(BT){ /*判断是否为空,不为空则继续,空则直接结束*/ InOrderTraversal(BT->Left); printf("%d", BT->Data); InOrderTraversal(BT->Right); } }
中序遍历 :(D B E F) A (G H C I)
后序遍历
过程为 :
- 后序遍历其左子树 (递归地)
- 后序遍历其右子树 (递归地)
访问根节点
void PostOrderTraversal( BinTree BT ) { if(BT){ /*判断是否为空,不为空则继续,空则直接结束*/ PostOrderTraversal(BT->Left); POstOrderTraversal(BT->Right); printf("%d", BT->Data); } }
左 右 根 , 所以对于左子树来说从D开始然后右边最后到 B
后序遍历 : (D E F B) (H G I C) A
**重点
先序、中序和后序遍历 : 遍历过程中经过结点的路线一样 , 只是访问各结点的时机不同
(路线会经过同一条结点2次 , 打印次序的不同决定是哪种遍历)
备注
实际上是用堆栈实现递归 , 下面讲解如何把递归变成非递归
二叉树的非递归遍历
有没有可能直接用堆栈实现遍历而非递归 ?
中序遍历的非递归遍历算法
五角星代表中序 , 顺序为(D B E F) A (G H C I)
从入口进经过 A 时不print , 先把 A 存进堆栈 ;
经过 B 时不print , 把 B 存进堆栈 ;
经过 C 时存入并且取出 , 直接print ;
以此类推
- 遇到结点 , 把它压栈 , 并去遍历左子树
- 左子树遍历结束后 , 从栈顶弹出这个结点并访问
- 然后按其有指针再去中序遍历该结点的右子树
void InOrderTraversal(BinTree BT) { BinTree T = BT; /*把BT赋给临时变量T*/ Stack S = CreateStack(MaxSize); /*创建并初始化堆栈S*/ while(T || !IsEmpty(s) ){ while(T){ /*一直向左并将沿途结点压入堆栈*/ Push(S , T); T = T->Left; } if(!IsEmpty(S)){ T = Pop(S); /*结点弹出堆栈*/ printf("%5d", T->Data); /*访问结点(打印结点)*/ T = T->Right; /*转向右子树*/ } } }
中序遍历的非递归遍历算法
电路符号代表先序 , 顺序为 A (B D F E) (C G H I)
**
按中序遍历的代码修改 :
后序遍历的非递归遍历算法
课后题
1 后序遍历实在第三次访问结点(最后一次访问结点)时,将结点的数据打印出来。但在老师给的框架中,只出现前两次的访问经过。我的解决办法是,对于能够出现第三次访问的结点(即存在左右儿子的结点),在第二次访问时,再压栈,当其右儿子访问过之后,再出栈,打印数据。具体代码如下,大家可以用递归的算法验证一下,我目前只验证了老师例子的情况。可能会有错误
2 我认为不能,因为在前序和中序遍历中,只须分别在入栈、出栈时打印出值就可以了,分别对应二叉树遍历中的第一次经历、第二次经历。而后序遍历是第三次经历时才能打印出值,而此时数据早已出栈, 我们无法获得相关信息,自然无法打印出值。
所以我的思路是出栈时先获取T->right中的地址信息,然后push一个right指针信息为空的结点入栈,这样就可以存储值的信息了。以下是我的代码,望各位批评指正。
void PostOrderTraversal(BinTree BT)
{
Bintree Q;
BinTree T = BT;
stack S = CreatStack(Maxsize);
while( T || !Empty(S)){
while(T){
Push(S, T);
T = T->left;
}
if(!Empty(S)){
T = Pop(S);
if(T->right){
Q = (BinTree *)malloc(sizeof BinTree);
Q->Data = T->Data;
Q->right = NULL;
Push(S,Q);
}else{
printf("%d", T->Data);
}
T = T->right;
}
}
}
二叉树的层序遍历
二叉树遍历的核心问题是 : 二维结构的线性化 (怎么样把二维结构变成一维线性的结构 的过程 )
(因此使用不同的方法遍历 产生的一维线性序列不同)
二维 → 一维 的问题点 :
- 从结点访问其左右儿子结点
- 访问左儿子后 , 右儿子结点怎么半 ?
-> 需要一个存储结构保存暂时不访问的结点
-> 存储结构 : 堆栈(访问左儿子保存自己)、队列(访问自己把右儿子存在队列里)
队列实现
遍历从根节点开始 , 首先将根节点入队 , 然后开始执行循环 : 结点出队、访问该结点、其左右儿子入队
每次循环 : 抛出根节点 , 把其左右儿子放进去
(抛出A放进B C)
(抛出B 放进 D F)
(抛出 C 放进G I)
(抛出 D )
(抛出F 放进 E)
(抛出G 放入 H) -> (抛出I) -> (抛出E) -> (抛出H)
层序遍历 : A B C D F G I E H (按层次从上至下 顺序从左至右访问)
**
基本过程
- 根节点入队
- 从队列中取出一个元素
- 访问该元素所指结点
- 若该元素所指结点的左右孩子结点非空 , 则将其左右孩子的指针顺序入队
实现
void LevelOrderTraversal(BinTree BT)
{
Queue Q; BinTree T;
if(!BT) return; /*若是空树则直接返回*/
Q = CreateQueue(MaxSize); /*创建并初始化队列Q*/
AddQ(Q, BT);
while(!IsEmptyQ(Q)){
T = DeleteQ(Q);
printf("%d\n",T->Data); /*访问取出队列的结点*/
if(T->Left) AddQ(Q, T->Left);
if(T->Right) AddQ(Q, T->Right);
}
}
堆栈实现
前序遍历:直接改为堆栈,其中将入栈顺序改为先右儿子后左儿子,即为前序遍历。
中序遍历:结点出栈时,如果是第一次出栈,则按照右儿子、当前结点、左儿子顺序入栈,然后如果是第二次出栈,则输出。即为中序遍历。
后序遍历:把中序遍历中的第一次出栈后的入栈顺序改为当前结点、右儿子、左儿子。即可得到后序遍历。
也是一种树的遍历。如果直接将队列改成堆栈,那么得到的遍历结果就是根节点>右子树>左子树的顺序。
如果节点出栈时是先压入右节点再压入左节点,那么得到的就是前序遍历。
如果节点出栈时按照右节点>自身>左节点的顺序压入(第二次出栈时才打印出来),可以实现中序遍历。
如果节点出栈时按照自身>右节点>左节点的顺序压入,可以实现后序遍历。
遍历的应用
输出叶子结点
例1 : 已知一颗二叉树 , 输出二叉树中的叶子结点
思路 : 在二叉树遍历算法中增加检测结点的 “左右子树是否都为空”
(改造先序遍历)
void PreOrderTraversal( BinTree BT )
{
if(BT){ /*判断是否为空,不为空则继续,空则直接结束*/
if(!BT->Left && !BT->Right) /*此行为新增条件,本题答案*/
printf("%d", BT->Data);
PreOrderTraversal(BT->Left);
PreOrderTraversal(BT->Right);
}
}
求二叉树的高度
(能不能用递归来算 ?)
(改造后序遍历)
int PostOrderGetHeight(BinTree BT)
{
int HL, HL, MaxH;
if(BT){
HL = PostOrderGetHeight(BT->Left); /*求左子树的深度*/
HR = PostOrderGetHeight(BT->Right); /*求右子树的深度*/
MaxH = (HL>HR) ? HL ; HR; /*取左右子树较大的深度*/
return (MaxH +1);
}
else return 0; /*空树深度为0*/
}
二元运算表达式树及其遍历
表达式树 : 叶子结点是运算数 , 非叶子结点是运算符
表示表达式 :
三种遍历可以得到不同的访问结果 :
中缀表达式不准确 , 会受到运算优先级的影响
如何解决 ? - 遍历时每到左子树的时候加左括号
由两种遍历序列确定二叉树
已知三种遍历中的任意两种遍历序列 , 能否唯一确定一颗二叉树呢 ?
**不行 , 必须有中序才能唯一确定 (不然没法区分哪边是左和右)
先序和中序确定一颗二叉树
分析 :
- 根据先序遍历序列第一个结点确定根结点
- 根据根结点在中序遍历序列中分割出左右两个子序列
- 对左子树和右子树分别递归使用相同的方法继续分解
小测验
课件
C语言实现 : 四种遍历
void InorderTraversal( BinTree BT )
{
if( BT ) {
InorderTraversal( BT->Left );
/* 此处假设对BT结点的访问就是打印数据 */
printf("%d ", BT->Data); /* 假设数据为整型 */
InorderTraversal( BT->Right );
}
}
void PreorderTraversal( BinTree BT )
{
if( BT ) {
printf("%d ", BT->Data );
PreorderTraversal( BT->Left );
PreorderTraversal( BT->Right );
}
}
void PostorderTraversal( BinTree BT )
{
if( BT ) {
PostorderTraversal( BT->Left );
PostorderTraversal( BT->Right );
printf("%d ", BT->Data);
}
}
void LevelorderTraversal ( BinTree BT )
{
Queue Q;
BinTree T;
if ( !BT ) return; /* 若是空树则直接返回 */
Q = CreatQueue(); /* 创建空队列Q */
AddQ( Q, BT );
while ( !IsEmpty(Q) ) {
T = DeleteQ( Q );
printf("%d ", T->Data); /* 访问取出队列的结点 */
if ( T->Left ) AddQ( Q, T->Left );
if ( T->Right ) AddQ( Q, T->Right );
}
}