浙大《数据结构》第三章:树(上)
Posted superorange
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浙大《数据结构》第三章:树(上)相关的知识,希望对你有一定的参考价值。
注:本文使用的网课资源为中国大学MOOC
https://www.icourse163.org/course/ZJU-93001
查找
查找:根据某个给定的关键字K,从集合R中找出关键字与K相同的记录。
静态查找:集合中的记录是固定的,没有插入删除的操作,只有查找;
动态查找:集合中记录是动态变化的,除查找,还可能发生插入和删除。
静态查找
方法1:顺序查找
int SequentialSearch( StaticTable *Tbl, ElementType K)
{
//在表Tbl[1]~Tbl[n]中查找关键字为K的数据元素
int i;
Tbl->Element[0] = K; //建立哨兵
for (i=Tbl->Length; Tbl->Element[i]!=K; i--);
return i; //查找成果则返回所在单元下标;不成功则返回0
}
顺序查找的时间复杂度为O(n)。
方法2:二分查找
int BinarySearch( StaticTable *Tbl, ElementType K)
{
int left, right, mid, NotFound = -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 NotFound; //查找不成功,返回-1
}
二分查找算法具有对数时间复杂度O(log(N))。
树的定义
树(Tree): n(n≥0)个结点构成的有限集合。当n=0时,称为空树,对于任意一棵非空树,它具备以下性质:
- 树中有一个称为“根(root)”的特殊结点,用r表示;
- 其余结点可分为若干个互不相交的有限集,其中每一个集合本身又是一棵树,称为原来树的子树(SubTree).
- 子树是不相交的;
- 除了根结点外,每个结点有且仅有一个父结点;
- 一棵N个结点的树有N-1条边。
树的基本术语
- 结点的度(Degree):结点的子树个数
- 树的度:树的所有结点中最大的读数
- 叶结点(Leaf):度为0的结点
- 父结点(parent):有子树的结点是其子树的根结点的父结点
- 子节点(child):若A结点是B结点的父结点,则称B结点是A结点的子结点;子结点也称为孩子结点
- 兄弟结点(sibling):具有同意父结点的各结点是彼此的兄弟结点
- 路径和路径长度:从结点(n_1)到(n_k)的路径为一个结点序列(n_1,n_2,...,n_k),(n_i)是(n_{i+1})的父结点。路径所包含的边的个数为路径的长度。
- 祖先结点(ancestor):沿树根到某一结点路径上的所有结点都是这个结点的祖先结点。
- 子孙结点(descendant):某一结点的子树中的所有结点是这个结点的子孙
- 结点的层次(level):规定根结点在1层,其他任意结点的层数是其父结点的层数加1。
- 树的深度(depth):树中所有结点中最大层次是这棵树的深度。
二叉树及其存储结构
定义
二叉树T:一个有穷的结点组合
- 这个集合可以为空
- 若不为空,则它是由根结点和称为其左子树(T_L)和右子树(T_R)的两个不相交的二叉树组成。
特殊的二叉树
性质
- 一个二叉树第i层的最大结点数为:(2^{i-1},i geq 1.).
- 深度为K的二叉树有最大结点总数为(2^K-1,K geq 1.).
- 对于任何非空二叉树T,若(n_0)表示叶结点的个数(没有子树)、(n_2)是度为2的非叶结点个数(有左右两个子树),那么二者满足关系(n_0=n_2+1)
(二叉树的结点总数为(n_0+n_1+n_2))
抽象数据类型定义
类型名称:二叉树
数据对象集:一个有穷的结点集合。若不为空,则由根结点和其左、右二叉子树组成。
操作集:(BT in BinTree),(Item in ElementType),重要操作有:
Boolean IsEmpty( BinTree BT); //判别BT是否为空
void Traversal( BinTree BT ); //遍历,按某顺序访问每一个结点
BinTree CreatBinTree();//创建一个二叉树
/* 常见的遍历方法 */
void PreOrderTraversal( BinTree BT ); //先序:根-左-右
void InOrderTraversal( BinTree BT ); //中序:左-根-右
void PostOrderTraversal( BinTree BT ); //后序:左-右-根
void LevelOrderTraversal( BinTree BT ); //层次遍历:从上到下,从左到右
存储结构
顺序存储结构
完全二叉树:按从上到下、从左到右顺序存储n个结点的完全二叉树的结点父子关系:
- 非根结点(序号i>1)的父结点的序号是(i/2)。
- 结点(序号i)的左孩子结点序号是2i(需2i<=n,否则没有左孩子)
- 结点(序号i)的右孩子结点序号是2i+1(需2i+1<=n,否则没有右孩子)
链表存储结构
typedef struct TreeNode *Position
typedef Position BinTree
struct TreeNode
{
ElementTyoe Data;
BinTree Left;
BinTree right;
}
二叉树的遍历
递归遍历
(1) 先序遍历
遍历过程:
- 访问根结点
- 先序遍历其左子树
- 先序遍历其右子树
void PreOrderTraversal( BinTree BT )
{
if ( BT )
{
printf("%d", BT->Data); //根结点
PreOrderTraversal( BT->Left ); //左子树
PreOrderTraversal( BT->Right ); //右子树
}
}
(2) 中序遍历
遍历过程:
- 中序遍历其左子树
- 访问根结点
- 中序遍历其右子树
void InOrderTraversal( BinTree BT )
{
if ( BT )
{
InOrderTraversal( BT->Left ); //左子树
printf("%d", BT->Data); //根结点
InOrderTraversal( BT->Right ); //右子树
}
}
(3) 后序遍历
遍历过程:
- 后序遍历其左子树
- 后序遍历其右子树
- 访问根结点
void PostOrderTraversal( BinTree BT )
{
if ( BT )
{
PostOrderTraversal( BT->Left ); //左子树
PostOrderTraversal( BT->Right ); //右子树
printf("%d", BT->Data); //根结点
}
}
非递归遍历
先序、中序和后序遍历过程中结果结点的路线是一样的,只是访问各结点的时机不同。
- 先序遍历是第一次"遇到"该结点时访问
- 中序遍历是第二次"遇到"该结点(此时该结点从左子树返回)时访问
- 后序遍历是第三次"遇到"该结点(此时该结点从右子树返回)时访问
非递归算法实现的基本思路:使用堆栈
(1) 中序遍历
- 遇到一个结点,就把它压栈,并去遍历它的左子树;
- 当这个左子树遍历结束后,从栈顶弹出这个结点并访问它;
- 然后按其右指针再去中序遍历该结点的右子树;
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); //结点弹出堆栈
printf("%5d", T->Data); //打印结点
T = T->Right; //转向右子树
}
}
}
(2) 先序遍历
void PreOrderTraversal( BinTree BT )
{
BinTree T = BT;
Stack S = CreateStack(); // 创建并初始化堆栈 S
while(T || !IsEmpty(S)) // 当树不为空或堆栈不空
{
while(T)
{
Push(S,T); // 压栈,第一次遇到该结点
printf("%d",T->Data); // 访问结点
T = T->Left; // 遍历左子树
}
if(!IsEmpty(S)) // 当堆栈不空
{
T = Pop(S); // 出栈,第二次遇到该结点
T = T->Right; // 访问右结点
}
}
}
(3) 后序遍历
先序的访问顺序是root, left, right 假设将先序左右对调,则顺序变成root, right, left,暂定称之为“反序”。后序遍历的访问顺序为left, right,root ,刚好是“反序”结果的逆向输出。
- 反序遍历二叉树,具体方法为:将先序遍历代码中的left 和right 对调即可。数据存在堆栈S中。
- 在先序遍历过程中,每次Push节点后紧接着print结点。对应的,在反序遍历时,将print结点改为把当前结点Push到堆栈N中。
- 反序遍历完成后,堆栈N的压栈顺序即为反序遍历的输出结果。此时再将堆栈N中的结果pop并print,即为“反序”结果的逆向,也就是后序遍历的结果。
void PostOrderTraversal( BinTree BT )
{
BinTree T = BT;
Stack S = CreatStack();
Stack N = CreatStack(); //创建并初始化栈N
wihile ( T || !IsEmpty(S) )
{
while(T) /*一直向右并将沿途结点压入堆栈*/
{
Push(S,T);
Push(N,T); //将遍历到的结点压栈,用于反向
T = T->Right; //这里是Right
}
if ( !IsEmpty(S) )
{
T = Pop(S);
T = T->Left;
}
}
while ( !IsEmpty(N) )
{
T = Pop(N);
printf("%5d", T->Data); //将 N 栈中的数据依次弹出并打印
}
}
层次遍历
队列实现:
- 根结点入队;
- 从队列中取出一个元素,并访问该元素;
- 若该元素所指结点的左右孩子结点非空,则将其左右孩子的指针入队。
void LevelTraversal( BinTree BT )
{
if ( !BT )
return; //如果是空树直接返回
BinTree T;
Queue Q = CreatQueue(MaxSize); //创建并初始化队列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 );
}
}
举例
(1) 求二叉树的高度
int PostOrderGetHeight( BinTree BT )
{
int HL, HR, MaxH;
if ( BT )
{
HL = PostOrderGetHeight( BT->Left );
HR = PostOrderGetHeight( BT->Right );
MaxH = (HL>HR) ? HL :HR; // 取左右子树较大的深度
return ( MaxH+1 );
}
else
return 0; // 空树深度为0
}
(2) 根据先序和中序遍历来确定一棵二叉树
分析:
- 根据先序遍历序列的第一个结点确定根结点;
- 根据根结点在中序遍历序列中分割出左右两个子序列;
- 对左子树和右子树分别递归使用相同的方法继续分解;
例如:
前序:ABCDEFG
中序:CBDAFEG
先序遍历为"根左右",则 A 是根,对应可以划分出中序中:(CBD)A(FEG),CBD 为左子树,FEG 为右子树,
再根据前序的 BCD,B 为根,划分出中序中(C(B)D)A(FEG),则 C D 分别是 B 的左右子树…最后可得树为:
A
/ B E
/ / C D F G
二叉树的同构
题意理解
给定两棵树T1和T2。如果T1可以通过若干次左右孩子互换就变成T2,则我们称这两棵树是“同构”的。
输入格式:
- 现在一行中给出概述的结点数,随后N行
- 第i行对应编号第i个结点,给出该结点中存储的字母、其左孩子结点的编号、右孩子结点的编号
- 如果孩子结点为空,则在相应的位置上给出“-”。
输入样例:
程序框架搭建
int main()
{
Tree R1, R2; // 利用结构数组表示二叉树,静态链表
R1 = BuildTree(T1); // 建立二叉树1
R2 = BuildTree(T2); // 建立二叉树2
if ( Isomorphic(R1, R2) ) // 判别是否同构并输出
printf("Yes
");
else
printf("No
");
return 0;
}
程序实现
#include <stdio.h>
#include <stdlib.h> //调用malloc()和free()
#include <malloc.h> //调用malloc()和free()
#include <windows.h> //windows.h里定义了关于创建窗口,消息循环等函数S
#define MaxTree 10
#define Null -1
struct TreeNode
{
char item;
int Left;
int Right; // 注:这里的left和right分别为输入数据的左右子结点索引
} T1[MaxTree], T2[MaxTree]; // 结构数组表示二叉树
/* 创建树并返回根结点 */
int BuildTree(struct TreeNode T[])
{
int i, N;
int Root = 0;
int check[MaxTree];
char cl, cr;
scanf("%d", &N);
if ( !N )
return Null;
for (i = 0; i < N; i++)
check[i] = 0;
for (i = 0; i < N; i++)
{
printf("%d: ",i);
scanf("%c %c %c", &T[i].item, &cl, &cr); // 注意输入时不要有空格,且字母要小写,eg.a12
fflush(stdin); // 微软系统中清空缓冲区的功能函数
// 注意‘’和“”的区别,双引号括起来的是字符串指针,单引号括起来的才是字符
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;
return Root;
}
/* 判断二叉树是否重构 */
int Ismorphic(int R1, int R2)
{
if ((R1 == Null) && (R2 == Null)) // 均为空
return 1;
if (((R1 == Null) && (R2 != Null)) || ((R1 != Null) && (R2 == Null))) // 其中一个为空
return 0;
if ((T1[R1].Left == Null) && (T2[R1].Left == Null)) // 根结点均没有左子树
return Ismorphic(T1[R1].Right, T2[R2].Right);
if (((T1[R1].Left != Null) && (T2[R2].Left != Null)) && ((T1[T1[R1].Left].item) == (T2[T2[R2].Left].item)))
return (Ismorphic(T1[R1].Left, T2[R2].Left) && Ismorphic(T1[R1].Right, T2[R2].Right));
else
return (Ismorphic(T1[R1].Left, T2[R2].Right) && Ismorphic(T1[R1].Right, T2[R2].Left));
}
int main()
{
int R1, R2; // 利用结构数组表示二叉树,静态链表
printf("T1:
");
R1 = BuildTree(T1); // 建立二叉树1
printf("T2:
");
R2 = BuildTree(T2); // 建立二叉树2
if (Ismorphic(R1, R2)) // 判别是否同构并输出
printf("Yes
");
else
printf("No
");
system("pause"); //程序暂停,显示按下任意键继续
return 0;
}
以上是关于浙大《数据结构》第三章:树(上)的主要内容,如果未能解决你的问题,请参考以下文章