树与二叉树
树(上)
引子
查找:根据某个给定关键字 K,从集合 R 中找出关键字与 K 相同的记录
静态查找:集合中记录是固定的。没有插入和删除操作,只有查找
顺序查找:(通过设置哨兵,进行判断是否找到元素或者是否到达表末尾)
1
2
3
4
5
6
7
8
9
10
11
12int SequentialSearch(list Tbl, ElementType k){
int i;
Tb1->Element[0] = k; //建立哨兵,最后函数返回的时候如果值为0,则没找到
for(i = Tab->Length; Tb1->Element[i]!=k; i--);
return i;
}
typedef struct LNode *List;
struct LNode{
ElementType Element[MAXSIZE];
int Length;
};顺序查找的时间复杂度为 O(n)
二分查找(Binary Search)
假设 n 个数据元素的关键字满足有序(比如:小到大)
并且是连续存放(数组),那么可以进行二分查找。
1
2
3
4
5
6
7
8
9
10
11
12
13int BinarySearch(List Tal, ElementType 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 NotFound; /*查找不成功,返回-1*/
}11个元素的二分查找判定树
平均成功查找次数:
ASL = (4*4 + 4*3 + 2*2 + 1)/11 = 3
- 动态查找:集合中记录是动态变化的,除查找,还可能发生插入和删除
树形结构
- 二叉树
- 概念:
- 定义
- 存储结构
- 操作:
- 三种遍历
- 线索二叉树
- 应用:
- 排序二叉树—平衡二叉树
- 哈夫曼树
- 概念:
- 树和森林:
- 概念:
- 定义
- 存储结构
- 操作:
- 与二叉树的转换
- 遍历
- 应用:并查集
- 概念:
树的定义
树:n(n≥0)个结点构成的有限集合
- 当 n=0时,称为空树。
对于任何一棵非空树应满足:
- 树中有且仅有一个特定的称为”根”(root)的特殊结点,用 r 表示;
- 其余结点可分为 m(m≥0) 个互不相交的有限集T1,T2,…,Tm,其中每个集合本身又是一棵树,称为原来树的”子树”(SubTree)
- 树的定义是递归的,是一种递归的数据结构。同时也是一种分层结构,具有以下特点:
- 树的根节点没有前驱结点,除根节点外所有结点有且只有一个前驱结点
- 树中所有结点可以有零个或多个后继结点
树与非树
- 子树是不相交的
- 除了根结点外,每个节点有且仅有一个父节点
- 一棵N个结点的树有 N-1条边
树的基本术语
- 结点的度(Degree):结点的子树个数(也就是当前辈分为孩子的个数,不包括孙子辈)
- 树的度:树的所有结点中最大的度数(也就是所生个数最多的同亲一代的个数)
- 叶结点(Leaf):度为 0 的结点(也叫做终端结点,没有孩子)
- 父结点(Child):若 A 结点是 B 结点的父结点,则称 B 结点是 A 结点的子节点;子结点也称孩子结点
- 兄弟结点(Sibling):具有同一父结点的各结点彼此是兄弟结点
- 路径和路径长度:从结点 n1到 nk 的路径为一个结点序列n1,n2,…nk,ni是 ni+1的父结点。路径所包含边的个数为路径的长度
- 祖先结点(Ancestor):沿树根到某一结点路径上所有结点都是这个结点的祖先结点
- 子孙结点(Descendant):某一结点的子树中所有结点是这个结点的子孙
- 结点的层次(Level):规定根结点在1层,其他任一结点的层次数是其父结点的层次数加1
- 树的深度(Depth):树中所有的结点中的最大层次是这棵树的深度
树的性质
- 非空树的结点总数等于树中所有结点的度之和加1(也就是所有的孩子加上祖宗)
- 度为 K 的非空树的第 i 层最多有 $k^{i-1}$ 个结点(i≥1)
- 深度为 h 的 k 叉树最多有$\frac{k^{h}-1}{k-1}$个结点
- 具有 n 个结点的 K 叉树的最小深度为 [$log_k(n(k-1)+1)$]
树的表示
儿子-兄弟表示法
二叉树的定义
特殊的二叉树
斜二叉树(相当于列表)
完美二叉树(满二叉树)
完全二叉树
二叉树的几个重要性质
一个二叉树第 i 层的最大结点数为:,i≥1.
深度为 K 的二叉树有最大结点总数为:,k≥1.(完美二叉树即为这样)
对任何非空二叉树(至少有一个结点的二叉树叫做非空二叉树)T,若表示叶子结点的个数、是度为 2 的非叶子结点个数,那么二者之间满足关系:
二叉树的抽象数据类型定义
二叉树的存储结构
顺序存储结构
完全二叉树: 按从上至下、从左到右的顺序存储
n个结点的完全二叉树的结点父子关系:
性质:
- 非根结点(序号 i > 1)的父结点的序号是[i/2];
- 结点(序号 i )的左孩子的结点的序号是2i;(要2i ≤ n,否则没有左孩子)
- 结点(序号 i)的右孩子结点的序号是 2i+1;(要 2i+1≤n,否则没有右孩子)
一般二叉树: 也可以使用上述的完全二叉树的结构,但会造成空间浪费
链表存储
1 | typedef struct node{ |
二叉树的遍历
先序遍历 -> 根左右
遍历过程:
- 访问根结点
- 先序遍历其左子树
- 先序遍历其右子树
1 | void PreOrderTraversal (BinTree BT){ |
先序遍历=> A B D F E C G H I
中序遍历 -> 左根右
遍历过程:
- 中序遍历其左子树
- 访问根结点
- 中序遍历其右子树
1 | void InOrderTraversal (BinTree BT){ |
中序遍历=> D B E F A G H C I
后序遍历 -> 左右根
遍历过程:
- 后序遍历其左子树
- 后序遍历其右子树
- 访问根结点
1 | void PostOrderTraversal(BinTree Bt){ |
后序遍历=> D E F B H G I C A
总结
- 先序、中序和后续遍历过程:遍历过程中经过结点的路线一样,只是访问各结点的时机不同
二叉树的非递归遍历
中序遍历非递归遍历算法
非递归算法实现的基本路线:使用堆栈
算法:
- 遇到一个结点,就把它压栈,并去遍历它的左子树;
- 当左子树遍历结束后,从栈顶弹出这个结点并访问它;
- 然后按其右指针再去中序遍历该结点的右子树
1 | void InOrderTraversal(BinTree BT){ |
1 |
|
先序遍历的非递归算法
1 | void PreOrderTraversal(BinTree BT){ |
1 |
|
后序遍历非递归算法
算法:
- 先遍历其左子树
- 再遍历其右子树
- 最后访问该结点
注意:
- 当是从左子树返回时,不能访问其根结点;
- 当是从右子树返回时,才能访问其根结点
设置标识位的后序遍历非递归算法
所以为了表明某结点是否可以被访问,引入一个标志变量 flag:
- 当 flag = 0,表示其从左子树返回,不可访问其根结点;
- 当 flag = 1,表示其从右子树返回,可访问其根结点
1 |
|
层序遍历
队列实现:
遍历从根结点开始,首先将根结点入队,然后开始执行循环:结点出队、访问该结点、其左右儿子入队
层序基本过程:先根结点入队,然后:
- 从队列中取出一个元素;
- 访问该元素所指结点;
- 若该元素所指结点的左、右孩子结点非空,则将其左、右孩子的指针顺序入队
1 | void LevelOrderTraversal(BinTree BT){ |
例:遍历二叉树的应用:输出二叉树中的叶子结点
在二叉树的遍历算法中增加检测结点的”左右子树是否都为空”。
1 | void PreOrderPrintLeaves(BinTree BT){ |
例:求二叉树的高度
1 | int PostOrderGetHeight(BinTree BT){ |
例:二元运算表达树及其遍历
解决中序遍历中受运算符优先级影响问题,通过在左子树开始的时候加一个左括号”(“,当左子树结束的时候再添加一个右括号”)”
例:由两种遍历序列确定二叉树
已知三种遍历中的任意两种两种遍历序列,能否唯一确定一棵二叉树?
答案是:两种遍历中必须要包含有中序遍历才可以
先序和中序遍历序列来确定一棵二叉树
[分析]
- 根据先序遍历序列第一个结点来确定根结点
- 根据根结点在中序遍历序列中分割出左右两个子序列
- 对左子树和右子树分别递归使用相同的方法继续分解
[例]
类似的,后序和中序遍历序列也可以确定一棵二叉树
小白专场:树的同构
二叉树的表示
结构数组表示二叉树:静态链表
1 |
|
顺序可以不同
对于 B,其左儿子存在且为 C ,其下标为 4。其右儿子为 D,其下标为 3
查找根结点是谁?
分析:这四个结点放在0、1、3、4中,且在0、1、3、4 中,0、3、4都出现了,所以 1 为根结点
程序框架搭建
如何建二叉树
递归法
用单个字符表示二叉树中一个结点的数据,这里采用前序遍历算法,建立二叉树的过程:
- 输入一个根结点
- 若输入的是” “(空字符),则表明二叉树为空,置 T 为 NULL
- 若输入的不是” “(空字符),则将该字符赋给 T->data,然后,依次递归地建立它的左子树T->lchild,和右子树 T->rchild。
具体算法:
1 | void BUILDBT(BTREE &T){ |
「幕课算法」
1 | Tree BuildTree(struct TreeNode T[]){ |
完整代码
1 | Tree BuildTree(struct TreeNode T[]){ |
如何判别两二叉树同构
树(中)
什么是二叉搜索树
查找问题
- 静态查找和动态查找
- 针对动态查找,数据如何组织?
二叉搜索树(BST,Binary Search Tree),也称二叉排序树或二叉查找树
二叉搜索树:一棵二叉树,可以为空,如果不为空,满足以下性质:
- 非空左子树的所有键值小于其根结点的键值
- 非空右子树的所有键值大于其根结点的键值
- 左、右子树都是二叉搜索树
二叉搜索树操作的特别函数
- Position Find(ElementType X, BinTree BST):从二叉搜索树BST中查找元素X,返回其所在结点的地址
- Position FindMin(BinTree BST):从二叉搜索树BST中查找并返回最小元素坐在结点的地址
- Position FindMax(BinTree BST):从二叉搜索树BST中查找并返回最大元素所在结点的地址
- BinTree Insert(ElementType X, BinTree BST):二叉搜索树的插入操作
- BinTree Delete(ElementType X, BinTree BST):二叉搜索树的删除操作
二叉搜索树的查找操作:Find
思路:
- 查找从根结点开始,如果树为空,返回 NULL
- 若搜索树非空,则根结点关键字和X进行比较,并进行不同的处理:
- 若 X 小于根结点的键值,则需在左子树中继续搜索
- 若 X 大于根结点的键值,则需在右子树中继续搜索
- 若两者比较结果是相等,搜索完成,返回指向此结点的指针
1 | Position Find(ElementType X, BinTree BST){ |
注:上述递归均为尾递归,效率低
用非递归函数的执行效率高,可将”尾递归”函数改为迭代函数,用循环来做
1 | Position IterFind(ElementType X, BinTree BST){ |
二叉搜索树的查找最大值和最小值递归函数
思路:
- 最大值一定是最右边到底
- 最小值一定是最左边到底
最小值查找
1 | Position FindMin(BinTree BST){ |
最大值查找1
2
3
4
5
6
7Position FindMax(BinTree BST){
if(!BST) return NULL;
else if (!BST->Right)
return BST;
else
return FindMin(BST->Right);
}
二叉搜索树的插入
「分析」关键是要找到元素应该插入的位置,可以采用与 Find 类似的方法
「算法」
1 | BinTree Insert (ElementType X, BinTree BST){ |
「例题」
注:按照字母先后进行排序
二叉搜索树的删除
考虑三种情况:
1. 要删除的是叶结点:直接删除,并再修改其父结点指针—置为 NULL
2. 要删除的结点只有一个孩子结点:
将其父结点的指针指向要删除结点的孩子结点
3. 要删除结点有左、右两棵子树:
用另一结点代替被删除结点:取右子树的最小元素代替之,或者取左子树的最大元素代替之。
以上原则还是符合二叉搜索树的构成要求:左子树键值小于根结点键值,右子树键值大于根结点键值
「算法」
1 | BinTree Delete(ElementType X, BinTree BST){ |
平衡二叉树
“平衡因子(Balance Factor,简称BF)”:BF(T)= $h_L- h_R$ ,其中$h_L- h_R$ 分别为 T 左、右子树的高度。
平衡二叉树(Balanced Binary Tree)(AVL树):
- 空树
- 或者,任一结点左、右子树高度差的绝对值不超过1,即|BF(T)|≤ 1
平衡二叉树的高度能达到$log_2 n$?
平衡二叉树的调整
四种调整结构
- RR 旋转
- LL 旋转
- LR 旋转
- RL 旋转
理解:对于 RR、LL 旋转,直接拎起左孩子,或者右孩子; 对于 LR 、RR 旋转,需要找到被破坏者,比较从被破坏者作为根节点的距离破坏者节点最近的最小不平衡树(三个节点),将其中平衡因子为中间的节点重新作为根节点。
注意:⚠️因为是平衡搜索树,一定需要满足左边小,右边大。
RR 旋转(右右旋转)具体例子:
判断按不同的插入序列是否可以得到相同的二叉搜索树
求解思路:
两个序列是否对应相同搜索树的判别:
- 分别建两棵搜索树的判别方法,进行递归判别每棵搜索树的每个节点的值是否一样
不建树的判别方法
建一棵树,在判别其他序列是否与该树一致
- 搜索树表示
- 建搜索树T
- 判别一序列是否与搜索树T一致
搜索树的表示:
1 | typedef struct TreeNode *Tree; |
程序框架的搭建
1 | int main(){ |
需要设计的主要函数:
- 读数据建搜索树T
- 判别一序列是否与 T 构成一样的搜索树
1 | int main(){ |
1 | Tree MakeTree(int N){ |
1 | Tree NewNode(int V){ |
1 | Tree Insert(Tree T, int V){ |
如何判别
1 | int check(Tree T, int V){ |
1 | int Judge(Tree T, int N){ /*有 bug 版本*/ |
1 | void ResetT(Tree T) /*清除 T 中各结点的 flag 标记*/ |
1 | void FreeTree(Tree T){ |
堆
优先队列(Priority Queue)
特殊的“队列”,取出元素的顺序是依照元素的优先权(关键字)大小,而不是元素进入队列的先后顺序。
采用数组或链表实现优先队列
采用二叉树存储结构
注意:从根结点到任意结点路径上结点序列的有序性,即由大到小
最大堆的操作
最大堆的创建:
1 | typedef struct HeapStruct *MaxHeap; |
最大堆的插入
算法实现: 将新增结点插入到从其父结点到根结点的有序序列中
哨兵的设置,使得,当比较到哨兵时,不需要进行交换。存储到下标为 1 为止。
最大堆的删除
取出根结点(最大值)元素,同时删除堆的一个结点
删除算法
1 | ElementType DeleteMax(MasHeap H){ |
最大堆的建立
建立最大堆:将已经存在的N个元素按最大堆的要求存放在一个数组中
方法一:通过插入操作,将 N 个元素一个个相继插入到一个初始为空的堆中去,其时间代价最大为 $O(NlogN)$
方法二:在线性时间复杂度下建立最大堆
(1)将 N 个元素按输入顺序存入,先满足完全二叉树的结构特性
(2)调整各结点位置,以满足最大堆的有序特性。
哈夫曼树与哈夫曼编码
哈夫曼树的定义
带权路径长度(WPL):设二叉树有 n 个叶子结点,每个叶子结点带有权值 $Wk$,从根结点到每个叶子结点的长度为 $l_k$ ,则每个叶子结点的带权路径长度之和就是:$$WPL=\sum{k=1}^{n}{W{k}L{k}}$$
最优二叉树或者哈夫曼树:WPL最小的二叉树
【例子】有五个叶子结点,它们的权值为{1,2,3,4,5},用此权值序列可以构造出形状不同的多个二叉树。
哈夫曼树的构造
每次把权值最小的两颗二叉树
如何选取两个最小的?(利用堆)(直接排序效率不如堆)
1 | typedef struct TreeNode *HuffmanTree; |
整体的时间复杂度为$O(N logN)$
哈夫曼树的特点:
没有度为 1 的结点;
n 个叶子结点的哈夫曼树共有 2n-1 个结点
哈夫曼树的任意非叶结点的左右子树交换后仍是哈夫曼树
对于同一组权值{w1, w2, ……, wn},是否存在不同构的两颗哈夫曼树呢?
哈夫曼编码
给定一段字符串,如何对自负进行编码,可以使得该字符串的编码存储空间最少?
当所有的字符都在叶结点上的时候,就不会出现前缀码是另外一个字符的前缀。
当编码中的一个结点出现在另一个结点的前面的时候—即在树的非叶结点上的时候,就会出现前缀。
利用哈夫曼树进行编码
集合的表示
集合运算
- 查找某个元素所在的集合(用根结点表示)
1 | int Find(SetType S[], ElementType X){ |
集合的并运算
- 分别找到 X1 和 X2 两个元素所在集合树的根结点
如果它们不同根,则将其中一个根结点的福结点指针设置成另一个根结点的数组下标
1
2
3
4
5
6
7
8
9
10void Union(SetType S[], ElementType X1, ElementType X2)
{
int Root1, Root2;
Root1 = Find(S, X1);
Root2 = Find(S, X2);
if(Root1 != Root2)
S[Root2].Parent = Root1;
}
堆中的路径
堆的表示及其操作
1 |
|