(本文为其他四篇文章的汇总)
树专栏https://blog.csdn.net/weixin_70468627/category_12737639.html
c++树笔记(一)定义,遍历https://blog.csdn.net/weixin_70468627/article/details/140456636?spm=1001.2014.3001.5501C++树笔记(二)【直径,中心】
https://blog.csdn.net/weixin_70468627/article/details/140485416c++树笔记(三)重心
https://blog.csdn.net/weixin_70468627/article/details/140512840C++树笔记(四)二叉树
https://blog.csdn.net/weixin_70468627/article/details/135934020
目录
证明:假设直径s-t外存在一点p相连->s-t-p st+tp>st
性质3:如果一棵树有多条直径,那么它们必然相交,且有极长连续段(可以是一个点,交点为树的中心)
性质4:树T1的直径为x,y,树T2的直径为s,t。现有一边u,v与两颗树相连,新树的直径端点一点是x,y,s,t中的两个
性质2:树的中心可以有一个(单中心),也可以有两个(双中心)
性质3:如果一棵树有多条直径,那么它们必然相交,且有极长连续段
性质4:两颗树加一条边相连,构成的新树直径端点一点是x,y,s,t中的两个
性质2:非空树有且仅有1-2个重心。当有两个重心时,树定有偶数个节点,且两个重心相邻。
性质3:树中所有点到重心的距离和最小,反过来距离和最小的点一定是重心。
性质4:往树上增加或减少一个叶子,如果原节点数是奇数,那么重心可能增加一个,原重心仍是重心;如果原节点数是偶数,重心可能减少一个,另一个重心仍是重心。
性质5:把两棵树通过一条边相连得到一棵新的树,则新的重心在较大的一棵树一侧的连接点与原重心之间的简单路径上。如果两棵树大小一样,则重心就是两个连接点。
树的定义
树(Tree)是n(n≥0)个结点的有限集。n=0时称为空树。在任意一颗非空树中:①有且仅有一个特定的称为根(Root)的结点;②当n>1时,其余结点可分为m(m>0)个互不相交的有限集T 1 {T}_{1}T 1 、T 2 {T}_{2}T 2 、… 、T m {T}_{m}T m ,其中每一个集合本身又是一棵树,并且称为根的子树(Sub Tree)。
树的基本术语
- 节点的度:一个节点含有的子树的个数称为该节点的度;
- 叶节点或终端节点:度为0的节点称为叶节点;
- 非终端节点或分支节点:度不为0的节点;
- 双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;
- 孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;
- 兄弟节点:具有相同父节点的节点互称为兄弟节点;
- 树的度:一棵树中,最大的节点的度称为树的度;
- 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
- 树的高度或深度:树中节点的最大层次;
- 堂兄弟节点:双亲在同一层的节点互为堂兄弟;
- 节点的祖先:从根到该节点所经分支上的所有节点;
- 子孙:以某节点为根的子树中任一节点都称为该节点的子孙。
- 森林:由m(m>=0)棵互不相交的树的集合称为森林;
树的初始起点:我们定义为根
递归树中,都只能从父节点走到子节点。
我们只需要记录每个父节点有哪些子节点,那么就可以遍历整个递归树。
树的层次:
节点高度:指从这个节点到叶子节点的距离(一共经历了几个节点)
节点深度:指从当前节点到根节点的距离(一共经历了几个节点)
树的高度:指所有节点高度的最大值
树的深度:指所有节点深度的最大值
节点的层:从根节点开始,假设根节点为第1层,根节点的子节点为第2层,依此类推
树的定义:
有n(n>=0)个节点,且任意两个点有且仅有一条路径连通的图形。若n=0,此时为空树。
树的性质
性质1:
n个节点,保证任意两点有且仅有一条路径,树中有且仅有n-1条边。
证明:除第一个节点外,连接一个其他节点,至少增加一条边,所以n个点至少要用n-1条边才能保证所有节点连通。若此时再增加一条非重边,任意两点间是否还存在一条唯一路径。
性质2:
树的根结点没有前驱(父节点),除根结点外的所有结点有且只有一个前驱。树中所有结点可以有零个或多个后继(子节点)。
证明:同上。
树形结构存储的两种思路
1.用结构体把每个节点的信息进行封装。这样的优点在于节点信息非常独立,但是所占空间稍大。2.用多个数组,分别描述每个节点的对应信息。这种方式的有点在于速度稍快,写起来简单。
树的遍历模板
通常来讲,树的边都是双向的我们在遍历的时候不希望一个点遍历多次。
方法1.用vis数组进行标记。
方法2.dfs中记录由父亲节点(来向),这样可以阻止走回去。
树上信息统计方式1-自顶向下统计
操作方法:在进入dfs之前进行信息统计。
如求链长:树上两个节点必然有且仅有一条路径,我们可以把该路径看成一条链。路径上的边权和为两点的链长。
树上信息统计方式2-自底向上统计
操作方法:在dfs回溯之时进行信息统计。
如求树的节点个数:当前树上共有多少个节点。
子树的概念:
抹除当前根节点以及所有与根节点的连边后,产生的树都是当前根节点的子树。
如当前根节点1的子树有,以2、3、4为根的子树。
总结:![](https://i-blog.csdnimg.cn/direct/0791e7ff0a4c4d44802fa2de539f05c2.png)
树的直径:
定义:树的直径是树上两点间距离的最大值。即树中最远的两个节点之间的距离被称为树的直径,连接这两点的路径被称为树的最长链。
链1) 5- 7 - 8 -3
链2) 1- 4 - 7 - 8 - 3
直径为17
树的直径的性质:
性质1:直径的端点一定是叶子节点
证明:假设直径s-t外存在一点p相连->s-t-p st+tp>st<sp 不成立
性质2:任意点的最长链端点一定是直径端点。
证明:
性质3:如果一棵树有多条直径,那么它们必然相交,且有极长连续段(可以是一个点,交点为树的中心)
证明:
性质4:树T1的直径为x,y,树T2的直径为s,t。现有一边u,v与两颗树相连,新树的直径端点一点是x,y,s,t中的两个
证明:
性质4分析:
uv连接后有两种情况
1.新直径不过uv,即现直径为st或为xy。2.新直径过uv,则现直径为
max(vs,vt)+max(ux,uy)+uv。
这两种情况都能保证新直径端点为x,y,s,t中的任意两个。新直径为以上三个中最大值。
连边uv求新树直径最小:
引理性质4可知:
st与xy不变,此时只能减下过uv的直径大小。以max(vs,vt)为例,要使该值最小,则v应当在树的中心位置,这样vs与vt越均衡。
同理u也应该在T2的树的中心位置。
连边uv求新树直径最大:
与前面一致,以max(vs,vt)为例,要使得该值最大,则v应当选择直径端点位置。
因此uv选择各自直径的端点位置时,直径最大。
树的直径求解方法:
引理性质2:任意点的最长链端点一定是直径端点。
方法:随意找一个点x,进行dfs找到最长链的端点s,再以端点s做第二遍dfs,此时可以找到直径的第二个端点t。此时端点s到t的距离就是树的直径。
树的直径的端点
通过记录父亲节点的方式能够把直径上的所有点全部记录下来。
在树中,直径端点是常用点(假设端点为s,t),我们树上任意一点p所能到的最大距离,只有可能是到ps或pt
找到所有点到两个直径端点的距离方法
法一(朴素方法):
求出直径端点后,以每个点为根做dfs,找到根节点到端点的距离。
复杂度O(N2)。
法二(优化方法):
第一次从任意点出发,必然能到达直径的一个端点s。
第二次从s点进行dfs找到端点t,此时记录所有点到s的距离。
第三次从t点进行dfs,记录所有点到t的距离。
复杂度:O(n)
树的中心
概念:以树的中心为根时,从该根到每个叶子节点的最长路径最短,使得路径和平衡。
树的中心求解:
我们现在已经知道求解任意一点到两端点的距离,即根据性质2可很轻松得到每个点能到的最长路径。求出每个点后的路径后,一次遍历便可知树的中心点。
树的中心两个性质:
性质1:树的中心一定在直径上,且趋向于中点位置
性质2:树的中心可以有一个(单中心),也可以有两个(双中心)
证明:
引理性质2,若树的中心p不在直径st上,st上有一点q与直径联通。中心点能到的最远距离为:
max(qs,qt)+pq,若要使得该值最小,pq应当为0,因此p在直径上。
同时为了让max(qs,qt)更小,树的中心要在直径中点处。
直径公共点证明与求解方法
以当一颗树存在多条直径时,引理性质3,公共边一定连续,因此可以直接对公共点/边进行求解
公共点公共边的求法:
找到直径左右端点s,t,从左往右遍历直径上的点进行
dfs,如果某点r在直径外找到一点与到右端点t距离相同,点r右边的点一定不是公共点。
同理,从右往左遍历直径上的点进行dfs,如果某点l在直径外找到一点与到左端点s距离相同,l左边的点一定不是公共点。此时,l->r就是我们直径的公共点。
因此我们只需要找到公共点边界l,r即可。使得l尽可能靠右,r尽可能靠左。
总结:
一、树的直径的四个基础性质
性质1:直径的端点一定是叶子节点
性质2:任意点的最长链端点一定是直径端点
性质3:如果一棵树有多条直径,那么它们必然相交,且有极长连续段
性质4:两颗树加一条边相连,构成的新树直径端点一点是x,y,s,t中的两个
二、树的直径相关求解问题
1. 树的直径以及所有点到直径左右端点距离的解法。
2. 树的中心的含义以及求解方法。
3. 直径的公共点/边的求解方法。
4. 两颗树连边后求新树的最小/大直径方法。
重心的基础概念
定义:使最大子树大小最小的点叫做树的重心
在线性的序列[1,n]中,我们在考虑用分治 思想处理问题时,需对问题进行划分。 在划分问题时若要更加均匀,我们选择中 点mid可以更加高效。 这样得到[1,mid],[mid+1,n]两个子序列, 因为子序列中元素的个数
思考:在树中我们同样可以选择一个点,将该点及该点连 边删除后得到一些子树,那么要使得问题更加均匀的划分, 选的点应具备什么要的性质?
树的重心求解方式
要求树的重心,我们可以枚举出每个点为断点时,所产生的最大子树大小。 某断点求当前最大子树大小的方法:对该点进行dfs,找到以i为根节点的子树的大小记录到sz[i]中,接着在该点的儿子中 找si最大的一个。复杂度为O(n2)
仔细分析:一次dfs,可以将每个节点x的子树分为两种:
1.根节点将要去的子树。(dfs回溯时一定能获取类型1子树大小)
2.来时的子树,此时无法再递归回去。(假设断开来边,来时子树大小=总节点数-接下来获得的sz[x],即等于n-sz[x])意味着一次dfs能够求出所有点作为重心能得到的最大子树大小mss。我们取最小值即可,复杂度O(n)
例题:
给定一棵树,树中包含 n(n)个结点(编号1~n)和 n−1 条无向边,找出树的重心若重心不止一个, 则输出编号较小的),以及当前重心下的最大子树大小。
#include<bits/stdc++.h>
using namespace std;
int n;
const int N=1e5+5;
vector<int>v[N];
int sz[N]; //当前节点的子树大小
int jie,minn=N;//jie是重心,minn为重心下的最大子树大小
void dfs(int x,int fa){
sz[x]=1;
int res=0;//开始找到当前x为根的最大子树
for(int i=0;i<v[x].size();i++){
int y=v[x][i];
if(y==fa) continue;
dfs(y,x);
sz[x]+=sz[y];
res=max(sz[y],res); //在几个子树中找最大值
}
res=max(res,n-sz[x]);//去的子树算完后,剩下来时子树
if(minn>res)minn=res,jie=x;
}
int main(){
cin>>n;
for(int i=1;i<n;i++){
int x,y;
cin>>x>>y;
v[x].push_back(y);
v[y].push_back(x);
}
dfs(1,0);
cout<<jie<<" "<<minn;
}
重心的性质
树的重心与直径端点,树的中心一样,在解决树上问题时,我们经常会用到。当边权为1时,树的重心存在以下性质。设mss(u)表示以u为重心的最大子树,s0(u)表示以u为根的子树大小,su(v)表示以u为根的的子树v大小。
性质1:重心点的最大子树大小不大于整棵树大小的一半。
性质1证明:
设u为重心,v为u的最大子树。
可以得出:s0[u]-su[v]>=su[v] ,即 su[v]0[u]/2
在整颗树中,存在s [u]=n0,所以su[v]
以某点为根,最大子树大小不超过n/2的都是树的重心
性质1的常用推导
推导1:
以某点为根,最大子树大小不超过n/2的一定是树的重心。
推导2:
以root为根的有根树中,树的重心一定在其最大的一颗子树内。
具体来讲,假设y为root的最大子树的儿子,那么重心一定在tp[y]->root的这一条链中(tp[y]表示子树y的重心)
性质2:非空树有且仅有1-2个重心。当有两个重心时,树定有偶数个节点,且两个重心相邻。
性质2证明:
假设u、v为树上两个重心,u,v分别为对方最长链上的点。
此时:mss[u]=mss[v]
又设k为两个重心之间存在的点数。
由mss[u]=su[v]+k,mss[v]=sv[u]+k,推出
在k个点中选择中点p,此时,
mss[p]=max(su[v]+k/2,sv[u]+k/2) >=su[v]+k,当且仅当k=0时,不等式成立。
重心u、v之间必不可能有点。
所以若有两个重心,则重心必然相邻。
性质3:树中所有点到重心的距离和最小,反过来距离和最小的点一定是重心。
当前重心为u。mss[u]=s [v]u。
假设重心从u移动到v,mss[v]=sv[u],可得1类节点到重心的距离加1,2类节点到重心的距离减少1,因此当增加部分sv[u] 小于 减少部分 sv[u]时,距离和减少
所以当s [v]>s [u]时,重心移动,得到mss更小。反之若当前mss已经最小,则无法再产生一个更小距离和uv。
树的重心的一些拓展性质
性质4:往树上增加或减少一个叶子,如果原节点数是奇数,那么重心可能增加一个,原重心仍是重心;如果原节点数是偶数,重心可能减少一个,另一个重心仍是重心。
性质5:把两棵树通过一条边相连得到一棵新的树,则新的重心在较大的一棵树一侧的连接点与原重心之间的简单路径上。如果两棵树大小一样,则重心就是两个连接点。
总结回顾:
一、重心的基础概念与求解方法
概念:找到一个点,使得最大子树的值最小。
求解方法:一次dfs记录子树的大小,来边通过n-sz[]解决,可求出当前点的最大子树大小,记录能产生最大子树值最小的点。
二、树的重心的三个基础性质
性质1:重心点的最大子树大小不大于整棵树大小的一半。
性质2:非空树有且仅有1-2个重心。当有两个重心时,树定有偶数个节点,且两个重心相邻。性质3:树中所有点到重心的距离和最小,反过来距离和最小的点一定是重心。
二叉树的定义:
二叉树(binary tree)是指树中节点的度不大于2的有序树,它是一种最简单且最重要的树。二叉树的递归定义为:二叉树是一棵空树,或者是一棵由一个根节点和两棵互不相交的,分别称作根的左子树和右子树组成的非空树;左子树和右子树又同样都是二叉树【节选自百度】
节点 | 左子树 | 右子树 |
0 | 子树 1 | 子树 7 |
1 | 子树 2 | 子树 6 |
2 | 无 | 无 |
链表也可以看作是一棵特殊的二叉树,因为链表每个结点只有一个子节点,满足二叉树的定义。二叉树与普通树的差别在于,普通树可以有 2 个以上的子节点,而二叉树的子节点不能超过 2 个。
链表 | 二叉树 | 树 | |
后继节点 | 1个 | 不超过 2个 | 任意个 |
定义关系 | 链表属于二叉树 | 二叉树属于树 |
二叉树的概念与性质
二叉树限定了孩子个数最多为2,而正是由于这种限定,使得二叉树拥有许多性质。
二叉树基本性质
性质1:在二叉树的第i层上最多有2i-1个结点(i>=1)。
性质2:深度为k的二叉树至多有2k–1个结点(k>=1)。
性质3:对任意一棵二叉树,如果其叶结点数为n0,度为2的结点数为n2,则一定满足:n0=n2+1。
归纳法证明性质3
假设初始为一个两个节点的二叉树。此时n0=n2+1,此时加点有两种方式:
1.加到叶节点,此时n0,n2不变
2.加到只有一个儿子的节点上,此时n0+1,n2+1。同样满足n0=n2+1
如果我们保证前面若干层都是满节点,那么我们就可以知道前面第i层确定的节点个数,这样的树将拥有更多的性质。
二叉树的节点数量
一棵高度为n的二叉树,最多包括2n−1个节点。树高为n,共有n 层,第一层有1 个根节点,根据二叉树的定义,后面每层节点的数量最多为上一层的2 倍,因此最多有:1+2+4+....+2n−1=2n−1个节点。
那么一棵高度为n的二叉树,最少有 n 个节点,是一个链表。
考虑前若干层是否为满节点,我们有两种特殊的二叉树:满二叉树和完全二叉树
满二叉树概念:
一棵深度为k且有2k–1个结点的二叉树 称为满二叉树。 通常来说我们对满二叉树的结点进行 连续编号,约定从根结点起,自上而 下,从左到右进行编号。
完全二叉树概念:
深度为k,有n个结点的二叉树当且 仅当其每一个结点都与深度为k的满 二叉树中编号从1到n的结点一一对 应时,称为完全二叉树。 特点:k-1层以前是满二叉树,最后 一层节点从左到右连续出现。
完全二叉树性质:
性质1:具有n个结点的完全二叉树的深度为:floor(log2n)+1
性质2:对于一棵n个结点的完全二叉树,对任一个结点(编号为i),有:
1) 节点i的左儿子为:i*2,节点i的右儿子为:i*2+1。
2) 若i*2+1>n,说明节点i无右儿子。若i*2>n,说明节点i无儿子。
3) 对于节点i,若i>1,则节点i的父亲为:i/2。
二叉树的存储
对于一颗普通的树,我们采取vector存放儿子,但是对于二叉树,我们只需要记录左右儿子即可。
有数组存放和结构体存放两种方式。结构体存放会更耗空间,因为结构体内部有不定长度的动态数组。 而存放二叉树,只需要用ls,rs来存放左右儿子即可。 空间占用与普通数组一样。对于复杂题目,信息较多的时候,变量命名更轻松,信息也独立。同时为了更快速调用,可以在结构体内进行define。
struct node{
int value;
vector<int> childs; //用来记录所有子节点的编号
}nodes[10000];
int root;
//root为根节点
也可以利用二叉树的特性,用左右子树的方式,来存储二叉树。
用结构体来表示二叉树的节点, 每个节点存储了当前节点的值, 以及左子树和右子树的编号。
struct node{
int value;
int left;
int right;
}nodes[10000];
int root; //root为根节点
二叉树的遍历
遍历二叉树的方法可以沿用遍历树的方法——递归。
DFS(当前节点u){
DFS(u.left);
DFS(u.right);
}
在此基础上,二叉树的遍历又分为以下三种:
先序遍历
遍历顺序规则为:根左右,遍历方法:
(1)访问根节点
(2)采用先序递归遍历左子树
(3)采用先序递归遍历右子树
中序遍历
遍历顺序规则为:左根右,遍历方法:
(1)采用中序遍历左子树
(2)访问根节点
(3)采用中序遍历右子树
二叉树遍历的总结
三种方法遍历过程中经过节点的路线一样,只是访问各个节点的顺序不同。
二叉树的先序、中序、后序遍历我们已经学会了, 那么给定其中任意两种遍历, 我们能否推出唯一的第三种遍历么?
答案:给定先序+中序可以推出后序。后序+中序可以推出先序。但是先序+后序是无法推出中序的。
这里我们只是以:知道先序遍历和中序遍历,推断后序遍历作为例子,其他组合方式原理是一样的。
二叉树有以下几种特性:
特性 A ,对于先序遍历,第一个肯定是根节点
特性 B ,对于后序遍历,最后一个肯定是根节点
特性 C ,利用先序或后序遍历,确定根节点,在中序遍历中,根节点的两边就可以分出左子树和右子树;
特性 D ,对左子树和右子树分别做前面 3 点的分析和拆分,相当于做递归,我们就可以重建出完整的二叉树。
所以我们可以靠保存先序+中序,或者后序+中序 2 个顺序,来保存整个二叉树的结构。
二叉树的遍历转化
一颗二叉树如果知道中序遍历,以及先序(或后序),我们就能清晰的知道树的结构
二叉树的特殊遍历方法
对于一棵二叉树,由于其特殊的性质,我们在树上信息统计的时候有其独特的方法。
二叉树的特殊遍历方法 前面所学并查集中提到,若没有查询某个节点,节点上的信息可以暂时不更新,等到需要的时候我们再去更新。在二叉树中,应用得也比较广泛,我们称之为”懒标记“。
如图:若某点值若只与父亲节点相关,修改7号节点的时候,只影响其本身的值,其他节点可以正常访问。当我们需要访问到该节点的时候,再进行更新。这样,我们可以将多次修改放到一起解决,从而降低问题的复杂度。
特殊的二叉树
二叉搜索树
二叉搜索树( Binary Search Tree )它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有节点的值均小于它的根节点的值; 若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值; 它的左、右子树也分别为二叉搜索树。
能够快速查找数据也是二叉搜索树被用于各类数据结构的原因,但这种快速是有限制的,即不会出现退化,假如二叉搜索树退化为链表,则无法做到快速查找数据的要求,所以又出现了平衡二叉树
平衡二叉树
平衡二叉树( Balanced Binary Tree )具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
平衡二叉树有效的限制了树高,使得二叉树不会退化为一个链表。
平衡二叉树如果同时也是一棵二叉搜索树,那么查找某个节点的效率是O(log(n))的。
很多数据结构,不能完美的做到平衡,但也能有效的控制树高,例如:红黑树。也被经常用于数据的快速查找。
满二叉数有2k-1个结点
样例代码
struct node{
char value;
int left,right;
}data[101];
int root=0,cnt;
char ch;
int buildTree(int bt){
cin>>ch;
if(ch=='.'){
bt==0;
return bt;
}else{
bt=++cnt;
data[bt].value=ch;
data[bt].left=data[bt].right=0;
data[bt].left=buildTree(bt);
data[bt].right=buildTree(bt);
}
return bt;
}
void postorder(int bt){
if(bt){
postorder(data[bt].left);
postorder(data[bt].right);
cout<<data[bt].value;
}
}
int main(){
root=0;
cnt=0;
root=buildTree(0);
postorder(root);
return 0;
}
总结:
一、二叉树的概念与性质
二叉树最多有两个儿子
二叉树的三个基本性质
完全二叉树与满二叉树的基本概念与性质
二、二叉树的储存与遍历
二叉树的独特存储与遍历方法
二叉树的前中后序遍历
二叉树遍历转化
二叉树中懒标记的应用