数据结构与算法 第三章 树以及相关遍历方法
一、树
1. 树的定义和术语
(1) 树的定义和术语
- 树是一种分层次组织结构,这种结构在管理上具有更高的效率
- 数据管理的基本操作之一:查找
- 空树:包含零个结点
- 根(root):用
r
表示。是每棵树(包括子树)的最上面的结点 - 子树(SubTree):根以外的结点可以分为若干互不相交的有限集,每个集合本身又是一棵树,称为原来树的子树(SubTree)
- 结点的度(Degree):结点的子树个数
- 树的度:树的所有结点中最大的度
- 叶节点(Leaf):度为0的结点。如
F
、L
等 - 父结点(Parent):有子树的结点是其子树的根结点的父结点。如
B
是G
的父结点 - 子结点(Child):也成为孩子结点。如
G
是B
的子节点 - 兄弟结点(Sibling):具有同一父结点的各结点彼此是兄弟结点。如
B
、C
、D
互为兄弟结点 - 路径:从结点 n1n_1n1 到 nkn_knk 的路径可以看做一个结点序列 n1,n2,...,nkn_1,n_2,...,n_kn1,n2,...,nk ,该序列中相邻的结点时父子结点的关系
- 路径的长度:路径所包含的边的个数
- 祖先结点(Ancestor):沿树根到某一结点路径上的所有结点都是这个结点的祖先结点
- 子孙结点(Descendant):某一结点的子树中所有结点时这个结点的子孙
- 结点的层次(Level):规定根结点在 1 层,其他任何结点的层数是其父结点的层数加 1。即结点所在的层数
- 树的深度(Length):树中所有结点中的最大层次是这棵树的深度
(2) 树的规则
- 在由查找方法导出的判定树中,树上的每个结点需要查找的次数刚好为该结点所在的层数
- 查找成功时,查找次数不会超过树的深度
- nnn 个结点的判定树的深度是 [log2n]+1[log_2n]+1[log2n]+1
- 平均查找次数(ASL):
若树的第 nin_ini 层有 mjm_jmj 个结点,且一共有 ppp 个元素,则平均查找次数如下
ASL=∑ijnimjpASL = \frac{\sum_{ij}n_im_j}{p}ASL=p∑ijnimj
(3) 查找
- 查找,指根据某个给定的关键字 KKK,从集合 RRR 中找出内容中与 KKK 相符的记录
- 查找的分类
- 静态查找:集合中的记录是固定的
不进行插入和删除操作,只是查找 - 动态查找:集合中的记录是变化的
有查找、插入和删除操作
- 静态查找:集合中的记录是固定的
- 子树互不相交
- 除了根结点外,每个结点有且仅有一个父结点。根结点没有父结点
- 一棵 NNN 个结点的树有 N−1N-1N−1 条边
(4) 静态查找
问题情境:在数组中查找某元素
方法1:顺序查找
顺序查找算法的时间复杂度为 O(n)O(n)O(n)
struct LNode {ElementType Element[MAXSIZE]; // 存放数组元素int length; // 数组中元素的个数
};
typedef struct LNode *List;
/* 功能:在Element[1]~Element[n]中查找关键字为K的数据元素* 输入:List Tbl 数组的首地址。下标为0的元素是哨兵,剩下的元素是数据元素* ElementType K 要查找的关键字* 输出:int i 查找的数据元素的下标。没有找到时返回 0*/
int SequentialSearch(List Tbl, ElementType K) {int i;Tbl->Element[0] = K; // 建立哨兵。使下面的for语句的判断中不需要对i>0进行判断// 从length开始,到1结束,查找数组里的元素for (i = Tbl->length; Tbl->Element[i] != K; i--) {};return i;
}
方法2:二分查找(Binary Search)
前提:nnn 个数据元素的关键字是有序的,且存放在连续存储结构中(如数组)
/* 功能:二分查找算法。在表Tbl中查找关键字为K的数据元素* 输入:List Tbl 数组首地址* ElementType K 要查找的关键字* 输出:int mid 查找到的元素下标。-1表示没有找到* 注意:数组中的元素按照从左到右,从小到大的顺序排列的*/
int BinarySearch(List Tbl, ElementType K) {int left, right, mid, NoFound = -1;left = 1; // 初始左边界right = Tbl->Length; // 初始右边界while (Tbl->length) {mid = left + (right - left) / 2; /* 计算中间元素下标,同时防止left和right太大导致mid溢出 */if (K < Tbl->Element[mid]) {right = mid - 1; // 调整右边界} else if (K > Tbl->Element[mid]) {left = mid + 1; // 调整左边界} else {return mid; // 查找成功,返回数据元素下标}}return NoFound;
}
2. 二叉树
树最好使用链表存储,且链表需要使用儿子-兄弟表示法构造结点
(1) 二叉树的定义
- 二叉树
T
:一个有穷的结点集合- 这个集合可以为空
- 若不为空,则它是由根结点和称为器左子树 TLT_LTL 和右子树 TRT_RTR 的两个互不相交的二叉树组成
- 二叉树的子树有左右顺序之分
- 二叉树
T
的五种基本形态:- 空树
- 只有一个结点
- 一个结点和对应的左子树,右子树为空
- 一个结点和对应的右子树,左子树为空
- 一个结点和对应的左右子树
(2) 特殊二叉树
- 斜二叉树(Skewed Binary Tree)
- 完美二叉树(Perfect Binary Tree)或满二叉树(Full Binary Tree)
- 完全二叉树(Complete Binary Tree)
- 按从上到下、从左到右的顺序存储 nnn 个结点的完全二叉树的结点父子关系
- 相当于满二叉树的叶结点那一层不完全,但缺少的那一层有特殊要求
上图的满二叉树中,若11
到15
删掉,则成为完全二叉树;若9
和后面的其他任意若干叶结点删掉,则不是完全二叉树
(3) 二叉树的重要性质
- 一个二叉树第 i 层的最大结点数为:2i−1,i≥12^{i-1},i≥12i−1,i≥1
- 深度为 k 的二叉树有最大结点总数:2k−1,k≥12^k-1,k≥12k−1,k≥1
- 对任何非空二叉树
T
,叶结点个数 = 度为 2 的非叶结点个数 + 1 - 非根结点(序号 i > 1)的父结点的序号为 [i/2][i/2][i/2]
- 结点(序号为 i)的左孩子结点的序号是 2i2i2i(若 2i≥2i≥2i≥总结点数,则没有左孩子)
- 结点(序号为 i)的右孩子结点的序号是 2i+12i +12i+1(若 2i+1≥2i+1≥2i+1≥总结点数,则没有右孩子)
2. 二叉树的抽象数据类型
类型名称:二叉树
数据对象集:一个有穷的结点集合。若不为空结点,则由根结点和其左、右二叉子树组成
操作集:BT ∈ BinTree,Item ∈ 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); // 层次遍历(或 层序遍历):从上到下,从左到右
3. 二叉树的存储结构
(1) 顺序存储结构
- 可以用数组存储二叉树
- 一般二叉树可以补全为完全二叉树,但造成了许多空间的浪费
(2) 链表存储
struct TreeNode {ElementType Data;BinTree Left;BinTree Right;
};
typedef struct TreeNode *BinTree;
typedef BinTree Position;
4. 链式存储结构二叉树的递归遍历
(1) 先序遍历
- 遍历过程为:
- 访问根结点
- 先序遍历其左子树
- 先序遍历其右子树
/* 功能:先序遍历二叉树* 输入:BinTree BT 要遍历的树的首地址* 输出:void*/
void PreOrderTraversal(BinTree BT) {if (BT) { // 若不是空树printf("%d", BT->Data); // 遍历根结点PreOrderTraversal(BT->Left); // 遍历左结点PreOrderTraversal(BT->Right); // 遍历右节点}
}
上图中黑底白字的数字表示遍历的先后顺序
(2) 中序遍历
- 遍历过程:
- 中序遍历其左子树
- 访问根结点
- 中序遍历其右子树
/* 功能:中序遍历二叉树* 输入:BinTree BT 要遍历的树的首地址* 输出:void*/
void InOrderTraversal(BinTree BT) {if (BT) {InOrderTraversal(Bt->Left);printf("%d", BT->Data);InOrderTraversal(Bt->Right);}
}
(3) 后序遍历
- 遍历过程:
- 后序遍历其左子树
- 后序遍历其右子树
- 访问根结点
/* 功能:后序遍历二叉树* 输入:BinTree BT 要遍历的树的首地址* 输出:void*/
void PostOrderTraversal(BinTree BT) {if (BT) {PostOrderTraversal(Bt->Left);PostOrderTraversal(Bt->Right);printf("%d", BT->Data);}
}
先序、中序和后序遍历过程中经过结点的路线一致,只是访问各结点(即各子树的根结点和叶结点)的时机不同
下图为结果结点的路线。可知,每一棵子树的根结点都经过了三次。
- 第一次经过根结点,就访问其中的数据,是先序遍历
- 第二次经过根结点,就访问其中的数据,是中序遍历
- 第三次经过根结点,就访问其中的数据,是后序遍历
5. 链式存储结构二叉树的非递归遍历
- 下面以中序遍历非递归遍历为例,关键是使用堆栈实现遍历
- 先序遍历、后序遍历同理
- 算法逻辑:
- 遇到一个结点,就把它压栈,并遍历其左子树
- 当左子树遍历结束后,从栈顶弹出这个结点,并访问它
- 然后按其右指针再去中序遍历该结点的右子树
/* 功能:链式存储结构二叉树的中序非递归遍历* 输入:要遍历的二叉树* 输出:void*/
void InOrderTraversal(BinTree BT) {BinTree T = BT; // T是临时变量Stack S = CreatStack(MaxSize); // 创建并初始化堆栈while (T || !IsEmpty(S)) { // 若树不空或堆栈不空/* 一直向左并将沿途结点压入堆栈 */while (T) { // 若堆栈不空Push(S, T); // 经过的结点入栈T = T->Left; // 当T=NULL时退出本循环}/* 已经定位到左子树的第一个要输出的结点,之后按照中序遍历的顺序输出 */if (!IsEmpty(S)) { // 若堆栈不空T = Pop(S); // 栈顶结点出栈。栈顶结点是二叉树中最左边的结点(简记为A)printf("%5d", T->Data); // 访问结点AT = T->Right; // 转向右子树。当T=NULL,但堆栈不为空时,最外层的大循环继续执行}}
}
6. 层序遍历
- 使用队列实现:遍历从根结点开始,先将根结点入队,然后开始执行循环:结点出队、访问该结点、其左右儿子结点入队
- 算法逻辑:先将根结点入队,然后执行下列操作
- 从队列中取出一个元素
- 访问该元素所指结点
- 若该元素所指结点的左、右孩子结点非空,则将其左、右孩子的指针顺序入队
/* 功能:队列实现层序遍历* 输入:BinTree BT 要遍历的目标树的首地址* 输出:void*/
void LevelOrderTraversal(BinTree BT) {Queue Q;BinTree T;if (!BT) { // 若目标对象是空树return; // 则直接退出函数}Q = CreatQueue(MaxSize); // 创建并初始化队列QAddQ(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); // 将右儿子结点添加到队列中}}
}
7. 遍历二叉树的应用
(1) 输出二叉树中的叶子结点
/* 功能:输出二叉树中的叶子结点* 输入:BinTree BT 叶子结点所在的树* 输出:void */
void PreOrderPrintLeaves(BinTree BT) {if (BT) {if (!BT->Left && !BT->Right) {printf("%d", BT->Data); // 某结点的左右结点为空,表明该结点是叶子结点}PreOrderPrintLeaves(BT->Left);PreOrderPrintLeaves(BT->Right);}
}
(2) 求二叉树的高度
/* 功能:求二叉树的高度(深度)* 输入:BinTree BT 求深度的树* 输出:int 树的深度*/
int PostOrderGetHeight(BinTree BT) {int HL, HR, MaxH;if (BT) {HL = PostOrderGetHeight(BT->Left); // 求左子树的深度HL = PostOrderGetHeight(BT->right); // 求右子树的深度MaxH = (HL > HR) ? HL : HR; // 取左右子树深度的最大值return (Max + 1); // 返回整棵树的深度} else {return 0; // 定义空树的深度为0}
}
(3) 运算表达式树及其遍历
- 叶子结点表示运算数或者是字母
- 根结点表示运算符
- 上图中,不同的遍历得到不同的表达式:
- 先序遍历得到前缀表达式:++a∗bc∗+∗defg++a*bc*+*defg++a∗bc∗+∗defg
- 中序遍历得到中缀表达式:a+b∗c+d∗e+f∗ga+b*c+d*e+f*ga+b∗c+d∗e+f∗g
- 由于中缀表达式需要考虑运算符的优先级,所以在遍历左右子树时,在遍历子树前加左括号,在遍历子树后加右括号
- 后序遍历得到后缀表达式:abc∗+de∗f+g∗+abc*+de*f+g*+abc∗+de∗f+g∗+
(4) 由先序、中序、后序遍历确定二叉树结构
- 方法是,从这三种遍历方式中,取中序遍历和其余两种中任何一种遍历方式,即可确定二叉树的结构
- 只根据先序和后序遍历无法准确确定二叉树的结构
情景1:先序和中序遍历序列确定一棵二叉树
- 方法
- 根据先序遍历序列第一个结点确定根结点;
- 根据根结点在中序遍历序列中分割出左右两个子序列;
- 对左子树和右子树分别递归,使用相同的方法继续分解
情景2:后序和中序遍历序列确定一棵二叉树
(5) 树的同构问题:判断某两棵树是否为同构的
- 同构:给定两棵树
T1
和T2
。若T1
可以通过若干次左右孩子互换就变成了T2
,则我们称两棵树是“同构”的
题目:输入两棵二叉树的信息,比较他们是否是同构
输入要求:
- 先在一行中给出该树的结点数
- 第
i
(从零开始计数)行对应编号第i
个结点,给出该结点中存储的字母、其左孩子结点的编号、右孩子结点的编号- 若孩子结点为空,则在相应位置上给出
-
- 输入样例如下:
关键:
- 二叉树的表示
- 建立二叉树
- 同构的判别
0x00
二叉树的表示
- 用链表结构表示
- 用数组结构表示
一般数组:将给定的二叉树看成完全二叉树存储
结构数组(物理存储结构是数组,组成思想是静态链表)
- 存储结构如下
- 第一行是该结点的信息,第二行是左结点,第三行是右结点,第二三行存储结点的编号。
-1
表示为空
由上图可知,一共有四个结点,分别用0、1、2、3表示,而表格中左右结点编号中只出现1、2、3,所以编号为0的结点就是根结点,即A是根结点
- 第一行是该结点的信息,第二行是左结点,第三行是右结点,第二三行存储结点的编号。
- 存储结构如下
#define MaxTree 10
#define ElementType char
#define Tree int
#define Null -1
struct TreeNode {ElementType Element;Tree Left; // 左结点编号Tree Right; // 右结点编号
} T1[MaxTree], T2[MaxTree];
0x01
程序框架搭建
int main(void) {Tree R1, R2;R1 = BuildTree(T1); // 建立二叉树T1R2 = BuildTree(T2); // 建立二叉树T2if (Isomorphic(R1, R2)) { // 判断是否同构printf("Yes\n");} else {printf("No\n");}return 0;
}
/* 功能:建立二叉树* 输入:struct TreeNode T[] 二叉树首地址* 输出:Tree Root 根结点的编号*/
Tree BuildTree(struct TreeNode T[]) {Tree Root, i;scanf("%d\n", &N); // 输入二叉树的结点个数if (N) {for (i = 0; i < N; i++) {check[i] = 0;}for (i = 0; i < N; i++) {scanf("%c %c% %c\n", &T[i].Element, &cl, &cr); // 输入各结点信息if (cl != '-') { // 若结点的左结点不为空T[i].Left = cl - '0'; // 将数字字符转换为对应的数值,表示现在处于的结点编号check[T[i].Left] = 1; // 当前结点的左结点不为空,表示当前结点不是根结点,标记为1} else { // 左结点为空T[i].Left = Null;}if (cr != '-') { // 若结点的右结点不为空T[i].Left = cr - '0'; // 将数字字符转换为对应的数值,表示现在处于的结点编号check[T[i].Right] = 1;} else { // 右结点为空T[i].Right = Null;}}for (i = 0; i < N; i++) { // 遍历check数组,i代表结点编号,check[i]表示结点是否为根结点,值为0时表示跟结点if (!check[i]) {break;}}Root = i; // 找到了根结点的编号,并返回}return Root;
}
/* 功能:判断两个二叉树是否同构* 输入:Tree R1 第1棵树当前的结点编号* Tree R2 第2棵树当前的结点编号* 输出:1 同构* 0 不同构*/
int Isomorphic(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) { // 两棵树的根结点不相等,则他们不同构return 0;}if ((T1[R1].Left == Null) && (T2[R2].Left == Null)) { // 若两棵树的当前节点都没有左子树return Isomorphic(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)) {// 若两棵树的当前节点的左结点相等且非空return (Isomorphic(T1[R1].Left, T2[R2].Left) && Isomorphic(T1[R1].Right, T2[R2].Right)); /* 目前遍历过的结点的位置和内容都相同,则继续判断 */} else {return (Isomorphic(T1[R1].Left, T2[R2].Right) && Isomorphic(T1[R1].Right, T2[R2].Left)); /* 判断是否可能是一棵树的左子树与另一棵树的右子树同构 */}
}
数据结构与算法 第三章 树以及相关遍历方法相关推荐
- 数据结构与算法(C#实现)系列---树
Heavenkiller(原创) 首先我们给树下一个定义: 树是一个有限的.非空的结点集, T={r} or T1 or T2 or-or Tn 它具有下列性质: 1.集合指定的结点r叫做树的根结点 ...
- 11_JavaScript数据结构与算法(十一)树
JavaScript 数据结构与算法(十一)树 树结构 什么是树? 真实的树: 树的特点: 树一般都有一个根,连接着根的是树干: 树干会发生分叉,形成许多树枝,树枝会继续分化成更小的树枝: 树枝的最后 ...
- Java算法--第三章--排序(14)概述
Java算法–第三章–排序(14)概述 排序算法的总结: 一.基础排序-----算法评估等级:O(n²) 1.冒泡 谁大谁上,每一轮都把最大的顶到天花板效率太低O(n2)–掌握swap 2.选择排序, ...
- 数据结构与算法(C++)– 树(Tree)
数据结构与算法(C++)– 树(Tree) 1.树的基础知识 树(tree): 一些节点的集合,可以为空集 子树(sub tree): 树的子集 根(root): 树的第一个节点 孩子和父亲(Chil ...
- 51NOD L4-第三章 树 刷题记录-zgw
第三章 树 2423 叶子节点的数量 AC 3.7 2282 树的深度 AC 3.7 2281 树的Size之和 AC 3.7 2627 树的深度及子树大小 AC 3.10 3039 叶子节点的路径 ...
- 51NOD L4-第三章 树 刷题记录-zyz
第三章 树 2423 叶子节点的数量 2282 树的深度 2281 树的Size之和 2627 树的深度及子树大小 3039 叶子节点的路径 2599 最近公共祖先(LCA) 2621 树上距离 26 ...
- 03_JavaScript数据结构与算法(三)栈
JavaScript 数据结构与算法(三)栈 数组是一个线性结构,并且可以在数组的任意位置插入和删除元素. 但是有时候,我们为了实现某些功能,必须对这种任意性加以限制. 栈和队列就是比较常见的受限的线 ...
- 比特数据结构与算法(第二章收尾)带头双向循环链表的实现
1.链表的分类 链表的分类 ① 单向或者双向 ② 带头或者不带头 ③ 循环或者非循环 常用的链表: 根据上面的分类我们可以细分出8种不同类型的链表,这么多链表我们一个个讲解这并没有意义.我们实际中最常 ...
- vpwm的控制变频_第三章 VVF控制与PWM方法.ppt
第三章 VVF控制与PWM方法 第三章 VVVF控制与PWM方法 3.1 VVVF变频调速原理 控制方法特点: 2.这是在忽略定子漏阻抗的影响得到的,在频率比较低时,这种忽略会带来偏小,电机力矩不够, ...
- 重读《学习JavaScript数据结构与算法-第三版》- 第6章 链表(一)
定场诗 伤情最是晚凉天,憔悴厮人不堪言: 邀酒摧肠三杯醉.寻香惊梦五更寒. 钗头凤斜卿有泪,荼蘼花了我无缘: 小楼寂寞新雨月.也难如钩也难圆. 前言 本章为重读<学习JavaScript数据结构 ...
最新文章
- arcgis api for flex 开发入门(二)map 的创建
- centos6.5_x64远程链接输入正确的账号密码无法登陆
- halcon的算子清点:Chapter 7 :Image
- argparse--解析命令行选项、用法以及说明
- arp 不同网段 相同vlan_三层交换机,相同的网段,不同的VLAN ,怎么通信?
- Division and Union CodeForces - 1101C (排序后处理)
- wkwebview 下移20像素_UITableView嵌套WKWebView的那些坑
- 扩展Asterisk1.8.7的CLI接口
- 开心开心开心开心开心开心哦哦哦
- Lua实现二进制串与Hex显示串的相互转换
- canvas width/height和style.width/style.height
- linux 找不到libaio.h,Linux上的POSIX AIO和libaio之间的区别?
- 本特利3300XL 25mm前置器 330780-50-CN
- PHP队列的实现,看完秒懂
- Win11搜索框恢复成放大镜
- 微信吸粉秘籍之人气论坛吸粉方法
- Visionpro从小白到大佬,第一章了解工具名称和用途
- 在html图片上方叠加一个半透明颜色层,并在半透明颜色层上叠加文字
- 世界四大顶级牛排,你都知道吗
- 先码后看 severlet开发基础 侵立删
热门文章
- 从零开始--系统深入学习android(实践-让我们开始写代码-Android框架学习-7.App Widgets)...
- vue.js devtools安装
- win10设置Python程序定时运行(设置计划任务)
- vue国际化高逼格多语言
- Android内核开发 Linux C编程调用内核模块设备驱动
- xcode6 使用MJRefresh
- 全宁对医药行业销售代表的介绍
- (转)PHP利用Curl、socket、file_get_contents POST数据
- 配置Apache支持
- Echo团队Alpha冲刺随笔 - 第八天