第三讲 树(上)
3.1 树与树的表示
什么是树?
客观事物许多事物之间存在层次关系
-
人类社会家谱
-
社会组织结构
-
图书信息管理
-
磁盘文件管理
-
分层次组织管理再管理上有更高的效率。
如何实现有效率的查找?
查找:根据某个给定的关键字K,从集合R中找出关键字与K相同的记录。
静态查找:
集合中记录固定。 -
没有插入和删除操作,只有查找。
动态查找:
集合中记录是动态变化的。 -
除查找,还可能发生插入和删除。
3.1.1 引子:顺序查找
静态查找
方法1:顺序查找 O ( n ) O(n) O(n)
typedef struct LNode *List;
struct LNode
{
int Element[MAXSIZE];
int Length;
};
int SequentialSearch(List Tbl,int K)
{//在Element[1]~Element[n]中查找关键字为K的数据元素
int i;
Tbl->Element[0] = K;//建立哨兵
for (i = Tbl->Length; Tbl->Element[i] != K;--i);
return i;//查找成功返回单元下标,不成功返回0
}
3.1.2 引子:二分查找的例子
方法2:二分查找(Binary Search) O ( l o g n ) O(logn) O(logn)
假设n个数据元素的关键字满足有序(比如:小到大)
并且是连续存放的数组,那么可以进行二分查找。
3.1.3 二分查找实现
二分查找算法
int BinarySearch(List Tbl,int K)
{//在Tbl中查找关键字为k的数据元素
int left, right, mid, NoFound = -1;
left = 1;//初始左边界
right = Tbl->Length;//初始右边界
while(left<=right)
{
mid = (left + right) / 2;
if(K<Tbl->Element[mid])
right = mid - 1;//调整右边界
else if(K>Tbl->Element[mid])
left = mid + 1;//调整左边界
else
return mid;//查找成功,返回下标
}
return NoFound;
}
11个元素的判定树
- 判定树上每个结点需要查找的次数刚好为该结点所在的层数。
- 查找成功时查找次数不会超过判定树的深度。
- n个结点的判定树深度为 [ l o g 2 n ] + 1 [log_2^n]+1 [log2n]+1
- ASL:总查找次数/结点个数=平均查找次数
启示:
3.1.4 树的定义和术语
树的定义:
树(Tree)😗*n(n≥0)**个结点
- 树中有一个称为**根(root)**的特殊结点,用r表示
- 其余结点可以分为m(m>0)个互不相交的有限集T1,T2,…,Tm,其中每个集合本身又是一棵树。
树与非树? - 子树是不相交的
- 除了根结点外,每个结点有且只有一个父结点。
- ⭐一棵N个结点的树有N-1条边。
树的一些基本术语:
- 1.结点的度
- 2.树的度:树的所有结点中最大的度数。
- 3.叶结点(Leaf):度为0
- 4.父节点,子节点,兄弟结点
- 5.路径和路径长度:路径所包含的边的个数
- 6.结点的层次(Level):规定根在1层,其他结点+1
- 7.树的深度(Depth):树中所有结点中的最大层次是这棵树的深度。
⭐3.1.5 树的表示
看似可以用链表表示,但各个结点结构不一样,指针数目不一样。
设置每个结点相同的结构。按度最大的那个结点的指针域大小配置各个结点,3N,会造成比较大的浪费。
儿子-兄弟表示法
Elemtent |
---|
FirstChild |
每个结点都是两个指针域,
旋转45°得到一个图:
Elemtent |
---|
Left |
二叉树!
讨论3.2 森林及表示
树的集合称为森林。是否也可以使用“儿子-兄弟”表示法存储森林?如何实现?
答:可以设置一个森林节点forest,forest的FirstChild指向第一棵树的根节点root1,root1的NextSibling指向第二课树的根节点root2,依次往后指
3.2 二叉树及存储结构
3.2.1 二叉树的定义及性质
二叉树的定义
二叉树T:一个有穷的结点集合
这个集合可以为空
若不为空,则它是由根结点和称为其左子树
T
L
T_L
TL和右子树
T
R
T_R
TR的两个不相交的二叉树组成。
二叉树的子树有左右顺序之分。
特殊二叉树
-
斜二叉树
-
完美二叉树/满二叉树
除了叶子结点都是两个子结点。并且叶子结点都在最底层。
-
完全二叉树
完全二叉树:有n各结点的二叉树,对树中结点按从上至下,从左到右的顺序进行编号,编号为i(1≤i≤n)结点与满二叉树中编号为i的结点在二叉树中位置相同。
完全二叉树不是满二叉树或完美二叉树,允许缺掉最下面一层靠右边的结点。
typedef struct TNode *Position;
typedef Position BinTree; /* 二叉树类型 */
struct TNode{ /* 树结点定义 */
int Data; /* 结点数据 */
BinTree Left; /* 指向左子树 */
BinTree Right; /* 指向右子树 */
};
⭐二叉树几个重要性质
- 1.一个二叉树第i层的最大结点数为:
2
i
−
1
,
i
≥
1
2^{i-1},i≥1
2i−1,i≥1
(层数自1开始) - 2.深度为k的二叉树有最大结点总数为: 2 k − 1 , k ≥ 1 2^k-1,k≥1 2k−1,k≥1
- 3.对任何非空二叉树T,若n0表示叶节点的个数,n2是度为2的结点个数,那么两者满足关系 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1
推导方法:总结点数-1得到总边数。
二叉树的抽象数据类型定义
数据对象集:
一个有穷的结点集合
若不为空,则由根节点和其左,右二叉子树组成
操作集:
BT∈BinTree,Item∈ElementType,重要操作有:
1.Boolean IsEmpty(BinTree BT)//判别BT是否为空
2.void Traversal(BinTree BT)//遍历,按某顺序访问每个结点
3.BinTree CreatBinTree()//创建一个二叉树
常用的遍历方法有:
void PreOrderTraversal(BinTree BT) //先序--根,左子树,右子树
void InOrderTraversal(BinTree BT) //中序--左子树,根,右子树
void PostOrderTraversal(BinTree BT) //后续--左子树,右子树,根
void LevelOrderTraversal(BinTree BT)//层次遍历。从上到下,从左到右
3.2.2 二叉树的存储结构
1.顺序存储结构
完全二叉树:从上到下,从左到右为n个结点的完全二叉树的结点的父子关系:
- 非根结点(序号i>1)的父结点的序号是**[i/2]**
- 结点(序号为i)的左孩子结点的序号是2i。
(若2i≤n,否则没有左孩子存在) - 结点(序号为i)的右孩子结点的序号是2i+1,(若2i+1≤n,否则没有右孩子)
一般二叉树也可以采用这样的结构,但会造成空间浪费…
⭐2.链表存储
Left | Data | Right |
---|
typedef struct TreeNode *BitTree;
typedef BitTree Position;
struct TreeNode
{
int Data;
BinTree Left;
BinTree Right;
};
3.3 二叉树的遍历
3.3.1 先序,中序后序遍历
(1)先序遍历
遍历过程为:
①访问根结点
②先序遍历其左子树
③先序遍历其右子树
void PreOrderTraversal(BinTree BT)
{
if(BT)
{
cout << BT->Data;
PreOrderTraversal(BT->Left);
PreOrderTraversal(BT->Right);
}
}
(2)中序遍历
遍历过程为:
①中序遍历其左子树
②访问根结点
③中序遍历其右子树
void InOrderTraversal(BinTree BT)
{
if(BT)
{
PreOrderTraversal(BT->Left);
cout << BT->Data;
PreOrderTraversal(BT->Right);
}
}
(3)后序遍历
遍历过程为:
①后序遍历其左子树
②后序遍历其右子树
③访问根结点
void PostOrderTraversal(BinTree BT)
{
if(BT)
{
PreOrderTraversal(BT->Left);
PreOrderTraversal(BT->Right);
cout << BT->Data;
}
}
总结:
先序,中序和后序遍历过程:遍历过程中经过结点的路线一样,只是访问各结点的时机不同。
3.3.2中序非递归遍历
中序遍历的非递归实现:
非递归算法实现的基本思路:使用堆栈
中序遍历的非递归遍历算法:
- 遇到一个结点,就把它压堆,并去遍历它的左子树
- 当左子树遍历结束后,从栈顶弹出这个结点并访问它
void InOrderTraversal(BinTree BT)
{
BinTree T = BT;
Stack S = CreatStack(MaxSize);//创建并初始化堆栈S
while(T||!IsEmpty(S))
{
while(T)//一直向左并将沿途结点压入堆栈
{
Push(S, T);
T = T->Left;
}
if(!IsEmpty(S))
{
T = Pop(S);//结点弹出堆栈
cout << T->Data;//访问打印结点
T = T->Right;//转向右子树
}
}
}
讨论3.4 如何用堆栈实现后序遍历的非递归程序
我们前面看到,借助堆栈可以实现前序遍历、中序遍历的非递归程序,而且两者的程序结构几乎一样。
那么,是否也可以借助堆栈实现后序遍历的非递归程序?是不是挪动一下printf语句就可以了?
参考:
如何用堆栈实现后序遍历的非递归程序
用堆栈实现后序遍历的非递归程序
3.3.3 层序遍历
二叉树遍历的核心问题:二维结构的线性化
- 从结点访问其左右儿子结点 。
- 访问左儿子后,右儿子怎么办?
-
- 需要一个存储结构保存暂时不访问的结点、
-
- 存储结构:堆栈,队列
队列实现
队列实现:遍历从根结点开始,首先将根结点入队,然后开始执行循环:结点出队,访问该结点,其左,右儿子入队。
分享一款做这个GIF用到的软件,非常小巧轻便:GifCam,搭配Quicker很流畅。
密码:2t2u
层序基本过程:先根结点入队,然后:
①从队列中取出一个元素
②访问该元素所指结点
③若该元素所指结点的左,右孩子结点非空。则将其左,右孩子的指针顺序入队。
void LevelOrderTraversal(BinTree BT)
{
Queue Q;
BinTree T;
if(!BT)//若是空树则直接返回
return;
Q = CreatQueue(Maxsize);//创建并初始化队列Q
ADD(Q, BT);//根结点放入队列
while(!IsEmptyQ(Q))
{
T = DeleteQ(Q);//从队列中抛出一个元素赋值为T
cout << T->Data;//访问取出队列的结点
if (T->Left)
AddQ(Q, T->Left);
if(T->Right)
AddQ(Q, T->Right);
}
}
讨论3.5 将层序遍历中的队列改为堆栈老师参与
如果将层序遍历中的队列改为堆栈,是否也是一种树的遍历?可以应用这种方法改造出一种前序、中序、后序的非递归遍历吗?
答:是树的遍历,但不属于中 前 后任意一个。用堆栈可以改造出前序遍历,入栈顺序是 右子树 再左子树。
⭐3.3.4 遍历应用例子
输出二叉树中的叶子结点。
- 在二叉树的遍历算法中增加检测结点的**“左右子树是否都为空”**
void PreOrderTraversal(BinTree BT)
{
if(BT)
{
if(!BT->Left&&!BT->Right)
cout << BT->Data;
PreOrderTraversal(BT->Left);
PreOrderTraversal(BT->Right);
}
}
求二叉树的高度(递归去做)
利用后序遍历的程序框架实现
int PostOrderGetHeight(BinTree BT)
{
int HL, HR, MaxH;
if(BT)
{
HL=PostOrderGetHeight(BT->Left);//左子树深度
HR = PostOrderGetHeight(BT->Right);//右子树深度
MaxH = (HL > HR) ? HL : HR;//取左右子树较大的深度
}
else
return 0;//空树深度为0
}
二元运算表达式树及其遍历
由两种遍历序列确定二叉树
给出的序列必须包含中序遍历才行
因为前序是根,左,右。后序是左,右,根。
没有根,这两个序列的方法都是一样的。
题目:根据先序和中序遍历序列来确定一棵二叉树
- 1.根据先序遍历序列第一个结点确定根结点。
- 2.根据根结点在中序遍历序列中分割出左右两个子序列。
- 3.对左子树和右子树分别递归使用相同的办法。
⭐3.5 树的同构
给定两棵树T1,T2。如果T1可以通过若干次左右孩子互换就变成了T2,则我们称这两棵树是"同构的"。现给定两棵树,判断是不是同构的。
7-3 树的同构 (25 分)
输入格式:
输入给出2棵二叉树树的信息。对于每棵树,首先在一行中给出一个非负整数N (≤10),即该树的结点数(此时假设结点从0到N−1编号);随后N行,第i行对应编号第i个结点,给出该结点中存储的1个英文大写字母、其左孩子结点的编号、右孩子结点的编号。如果孩子结点为空,则在相应位置上给出“-”。给出的数据间用一个空格分隔。注意:题目保证每个结点中存储的字母是不同的。
输出格式:
如果两棵树是同构的,输出“Yes”,否则输出“No”。
求解思路
- 1.二叉树表示
- 2.建二叉树
- 3.同构判别
二叉树表示
结构数组表示二叉树:静态链表
数组中每个分量都是一个结构。
不能用NULL表示空下标,因为NULL是0,也是一个下标。为了区分,定义NULL为-1。
#define MaxTree 10
#define ElementType char
#define Tree int
#define NULL -1
struct TreeNode
{
char Element;
Tree Left;
Tree Right;
}T1[MaxTree],T2[MaxTree];
判断哪个是根?
根据所有的Left和Right里没有出现过的,就是根结点(没有人指向它)。
程序框架搭建
int main()
{
//建二叉树1
//建二叉树2
//判别是否同构并输出
return 0;
}
需要设计的函数:
- 读取该二叉树
- 二叉树同构异构判断
int main()
{
Tree R1, R2;
R1 = BuilidTree(T1);
R2 = BuildTree(T2);
if(lsomorphic(R1,R2))
cout << "Yes!";
else
cout << "No!";
return 0;
}
怎么建二叉树?
Tree Build(struct TreeNode T[])
{
int N;
cin >> N;
if(N)
{
for (i = 0; i < N;++i)
{
check[i] = 0;
}
for (i = 0; i < N; ++i)
{
cin >> T[i].Element;
cin >> cl;
cin >> cr;
if(cl!='-')
{
T[i].Left = cl - '0';
check[T[i].Left] = 1;
}//对cl的处理
else
T[i].Left = NULL;
//...同样对cr处理
}//做完之后就只有根没有人check过
for (i = 0; i < N;++i)
{
if(!check[i])
break;
Root = i;
}
return Root;
}
}
如何判别两二叉树同构
int lsomorphic(Tree R1,Tree R2)
{
if((R1==Null)&&(R2==Null))
return 1;
if(((R1==Null)&&(R2!=Null))||((R1!=Null)&&(R2==Null)))
return 0;
if (T1[R1].Element != T2[R2].Element)
//roots are different
return 0;
if((T1[R1].Left==Null)&&(T2[R2].Left==Null))
//both have no left subtree
return lsomorphic(T1[R1].Right, T2[R2].Right);
if (((T1[R1].Left != Null) && (T2[R2].Left != Null)) && ((T1[T1[T1].Left].Element)==(T2[T2[R2].Left].Element))))
//左边同时不空,且看左边element是不是同时不一样的
return (lsomorphic(T1[R1].Left,T2[R2].Left)&&lsomorphic(T1[R1].Right,T2[R2].Right));
else
//需要交换
return (lsomorphic(T1[R1].Left,T2[R2].Right)&&lsomorphic(T1[R1].Right,T2[R2].Left));
}
⭐完整代码
#include <iostream>
#define MaxTree 10
#define ElementType char
#define Tree int
#define Null -1
using namespace std;
struct TreeNode
{
char Element;
Tree Left;
Tree Right;
}T1[MaxTree],T2[MaxTree];
Tree BuildTree(struct TreeNode T[])
{
int i,N;
Tree Root;
cin >> N;
int check[MaxTree];
char cl,cr;
if(N)
{
for (i = 0; i < N;++i)
{
check[i] = 0;
}
for (i = 0; i < N; ++i)
{
cin >> T[i].Element;
cin >> cl;
cin >> cr;
if(cl!='-')//判断左节点
{
T[i].Left = cl - '0';
check[T[i].Left] = 1;
}
else
T[i].Left = Null;
if(cr!='-')//判断右结点
{
T[i].Right = cr - '0';
check[T[i].Right] = 1;
}
else
T[i].Right = Null;
}
for (i = 0; i < N;++i)
{
if(!check[i])
break;
}
Root = i;
}
else return Null;
return Root;
}
int lsomorphic(Tree R1,Tree R2)
{
if((R1==Null)&&(R2==Null))
return 1;
if(((R1==Null)&&(R2!=Null))||((R1!=Null)&&(R2==Null)))
return 0;
if (T1[R1].Element != T2[R2].Element)
//roots are different
return 0;
if((T1[R1].Left==Null)&&(T2[R2].Left==Null))
//both have no left subtree
return lsomorphic(T1[R1].Right, T2[R2].Right);
if (((T1[R1].Left != Null) && (T2[R2].Left != Null)) && ((T1[T1[R1].Left].Element)==(T2[T2[R2].Left].Element)))
//左边同时不空,且看左边element是不是同时不一样的
return (lsomorphic(T1[R1].Left,T2[R2].Left)&&lsomorphic(T1[R1].Right,T2[R2].Right));
else
//需要交换
return (lsomorphic(T1[R1].Left,T2[R2].Right)&&lsomorphic(T1[R1].Right,T2[R2].Left));
}
int main()
{
Tree R1, R2;
R1 = BuildTree(T1);
R2 = BuildTree(T2);
if(lsomorphic(R1,R2))
cout << "Yes";
else
cout << "No";
return 0;
}