其他二叉树知识!二叉树知识汇总
目录
一、求树的深度
树的深度=max(左子树深度,右子树深度)+1
struct Node{
int data;
Node* leftchild;
Node* rightchild;
};
int deep(Node* t){
if (t==NULL) return 0;
int l=deep(t->leftchild);
int r=deep(t->rightchild);
return max(l,r)+1;
}
二、查找二叉树中的一个节点
在二叉树中查找一个结点,如果找到返回结点地址,如果没找到,返回NULL。
查找策略:优先左,如果是空树,返回NULL;如果查找的是根,直接返回根的地址,先去左子树中找,如果找到了,返回结果,如果左子树也没有找到,再去找右子树。
注意:查找过程是按照先序遍历写的,对于左子树的返回值第一时间进行判断,这样查找到结果就可以提前退出。
Node* Search(Node* t,int x){
if (t==NULL) return NULL;
if (t->data==x) return t;
Node* l=Search(t->leftchild,x);
if (l!=NULL) return l;
Node* r=Search(t->rightchild,x);
if (r!=NULL) return r;
return NULL;
}
三、最近公共祖先LCA
1.递归法 (链式存储)
此方法前提是你已经建好了一棵用链式存储的树。
查找两个节点的最近公共祖先,分三种情况:
- 如果两个节点恰好在某个节点 t 的两边,那么最近公共祖先就是 t 。
- 如果两个节点都在 t 的左边,那么说明 t 不是最近的公共祖先,需要再往下找,也就是把 t 更新为 t 的左孩子,再递归。
- 如果两个节点都在 t 的右边,同理,把 t 更新为 t 的右孩子,再递归。
怎么判断一个节点是否在 t 的左边或者右边?
深度优先遍历二叉树,一旦找到了两个节点其中的一个,就将这个点返回给上一层,上一层节点通过其左右子树的返回值是否==NULL,来判断有几个节点以及在哪边。
写法一: 传入参数是所求节点的指针
Node* FindLCA(Node* t,Node *p,Node* q){
if (t==NULL) return NULL;
if ((t==p)||(t==q)) return t;
Node* l=FindLCA(t->leftchild,p,q);
Node* r=FindLCA(t->rightchild,p,q);
if (l==NULL && r==NULL) return NULL;
if (l==NULL && r!=NULL) return r;
if (l!=NULL && r==NULL) return l;
return t;
}
写法二:传入的是节点的值
int Find_LCA(Node* t,int x,int y){
if (t==NULL) return 0;
if (t->data==x) return x;
if (t->data==y) return y;
int l=Find_LCA(t->left,x,y);
int r=Find_LCA(t->right,x,y);
if (l==0 && r==0) return 0;
if (l==0 && r!=0) return r;
if (l!=0 && r==0) return l;
if (l!=0 && r!=0) return t->data;
}
代码解释:
上述两种写法基本一样,只是传入参数不同。下面讲解按写法二来说。
现在我们分二种情况来讨论代码的运行。
情况一:LCA是他们的公共祖先,而不是两个节点中的某一个。求如图节点5 8的LCA
在这种情况下,我们的递归会一直从上层往下层走,在最左下角的7,l 和 r 都是0,说明没有找到两个节点中的任何一个,所以返回0.
在8这个位置,我们判断出了8是我们要求的某一个节点,所以返回8.
那么对于7 8节点的父节点4来说,他的 l=0 , r=8, 说明在他的‘管辖’之内出现了一个8,所以他需要返回8。
对于节点2来说,他的左孩子 l 的值已经求出来了,再去求r。恰好右孩子5是我们所求的,所以r=5。那么节点2的左右孩子都不是0,说明节点2就是LCA了。
这里要注意,到此为止程序还没结束,因为还需要把递归结果传回去,对于节点1来说,他只知道他的左孩子是2,l=2,是答案,还是仅仅是找到了一个节点,它不知道。
所以后续还会继续执行1的右孩子,最终算出r=0,然后返回的是l。
情况二:LCA是他们其中的一个。还是那个图,求节点2,9的最近公共祖先。
这种情况下1的左孩子是节点2找到后,2的后续节点都不会访问。只会再去找1的右孩子。
总结:此方法并不会把整个图遍历,递归的层数与所求两个节点位置有关。
2.普通的模拟法(邻接表建树)
建立用邻接表存储的一棵树,然后通过一遍 dfs 预处理出每个节点的深度和每个节点的父亲。
接下来就直接模拟:先把两个节点提升至同一高度,然后再一起向上移动,直至两个节点相遇就找到了LCA。
#include<iostream>
using namespace std;
const int MaxN=500050;
struct Edge{
int v;
int next;
};
Edge e[MaxN];
int last[MaxN];
int n,m,root,tot;
int deep[MaxN];
int f[MaxN];
void build(int x,int y){
tot++;
e[tot].v=y;
e[tot].next=last[x];
last[x]=tot;
}
//编号为x的节点,父亲是fa
void dfs(int x,int fa){
f[x]=fa;
deep[x]=deep[fa]+1;
for (int j=last[x]; j!=0; j=e[j].next){
int y=e[j].v;
if (y!=fa) dfs(y,x);
}
}
int LCA(int x,int y){
//默认x在更下面
if (deep[x]<deep[y]) swap(x,y);
//提升至同一高度
while (deep[x]>deep[y]) x=f[x];
//同步向上走
while (x!=y){
x=f[x];
y=f[y];
}
return x;
}
int main(){
cin>>n>>m>>root;
for (int i=1; i<=n-1; i++){
int x,y;
cin>>x>>y;
build(x,y);
build(y,x);
}
dfs(root,0);
for (int i=1; i<=m; i++){
int x,y;
cin>>x>>y;
cout<<LCA(x,y)<<endl;
}
}
3.倍增法求LCA
普通的模拟方法在查询量很大的时候容易超时(虽然模拟法比方法一的递归法快),所以有了现在的倍增法。
在朴素的模拟中,时间主要花费在了跳跃上,不管是为了提升至同一深度,还是共同跳跃,都只能一步一步跳,因为对于一个节点来说,我们只知道他的父亲节点,所以导致我们只能一步一步跳跃。
那么可不可以预处理出一个节点向上跳跃 x 步之后的节点是哪一个呢? 当然可以!
假设我们设一个数组 f[i][j] 表示节点 i 跳跃 j 步之后的节点,那么要预处理出这个数组,需要一个N*N的存储空间,并且预处理也要花费非常多的时间。
因为任何一个数都可以用二进制表示出来,所以我们把 f 数组的定义改为 f[i][j] 表示节点 i 跳跃 2^j 步之后的节点,这样预处理只需要花N*log(N)的时间,达到了我们想要的目的,速度也非常快。
现在假设一棵树有50w个节点,log(500000)=18.9<19,也就是说,50w个节点的树,我们预处理的 f 数组第二维也才是19而已,很小。
预处理 f 数组,是一个动态规划的过程,因为 f[i][j] 表示节点 i 跳跃 2^j 步之后的节点,那么 f [ i ][ j ] = f [ f[ i ][ j - 1 ] ][ j - 1 ],翻译一下就是跳 2^j 步,也就是先跳 2^(j-1) 步,再跳 2^(j-1) 步。
使用 f 数组: 提升至同一高度的阶段,可以看做把一个数直接二进制分解了。
共同跳跃的阶段,因为我们要避免跳过了的情况,所以判断条件是 (f[x][i]!=f[y][i]) ,最终只让他们跳到LCA的下面,返回父亲就是答案。
#include<iostream>
using namespace std;
const int MaxN=500050;
struct Edge{
int v;
int next;
};
Edge e[MaxN*2];
int last[MaxN];
int n,m,root,tot;
int deep[MaxN];
int f[MaxN][25]; //第i个节点向上跳跃2^j步是f[i][j]节点。
void build(int x,int y){
tot++;
e[tot].v=y;
e[tot].next=last[x];
last[x]=tot;
}
//编号为x的节点,父亲是fa
void dfs(int x,int fa){
deep[x]=deep[fa]+1;
f[x][0]=fa;
for(int i=1;i<=20;i++)
f[x][i]=f[ f[x][i-1] ][i-1];
for (int j=last[x]; j!=0; j=e[j].next){
int y=e[j].v;
if (y!=fa) dfs(y,x);
}
}
int LCA(int x,int y){
//默认x在更下面
if (deep[x]<deep[y]) swap(x,y);
//提升至同一高度
for (int i=20; i>=0; i--)
if (deep[x]-deep[y]>=(1<<i)) x=f[x][i];
//特判下x,y是否相同
if (x==y) return x;
//同步向上走
for (int i=20; i>=0; i--){
if (f[x][i]!=f[y][i]){
x=f[x][i];
y=f[y][i];
}
}
return f[x][0];
}
int main(){
cin>>n>>m>>root;
for (int i=1; i<=n-1; i++){
int x,y;
cin>>x>>y;
build(x,y);
build(y,x);
}
dfs(root,0);
for (int i=1; i<=m; i++){
int x,y;
cin>>x>>y;
cout<<LCA(x,y)<<endl;
}
}
以上方法中,我们用了3个从0到20的for循环,用来实现倍增,预处理的时候,很明显是先求小的,再求大的;使用时,要尽量先用大的,这跟快速幂先处理最左边的1是一个道理。
3个for循环我们也可以对其进行常数优化,预处理出一个lg数组,lg[i]=log(i)+1,个人感觉一般用不着,所以不赘述。
四、打印二叉树中的所有路径
在先序遍历的基础上改进:参数增加一个s,表示当前的和为s,每当到达一个新的节点时,将节点的值加入vector,保存路径,再去递归他的左右孩子,在完成对这个节点的遍历时,回溯,也就是pop_back();
另外在遇到一个新的节点时,特判是不是叶子节点,是的话就输出路径。
注意,在确定一个节点是叶子节点之后,也不能提前return,要让他去访问左右的空孩子,然后回溯之后(也就是pop_back这个节点),自然结束。提前return会导致这个检查完毕应该pop的节点没有pop。
vector<int> q;
void Print_Path(Node* t,int s){
if (t==NULL) return;
int x=t->data;
q.push_back(x);
if (t->leftchild==NULL &&t->rightchild==NULL){
cout<<"sum="<<s+x<<endl;
for (int i=0; i<q.size(); i++)
cout<<q[i]<<" ";
cout<<endl;
}
Print_Path(t->leftchild,s+x);
Print_Path(t->rightchild,s+x);
q.pop_back();
}
五、求和为某值的所有路径
与上述相同,只是增加一个参数为所求的值,特判输出即可。
vector<int> q;
void Find_Path(Node* t,int s,int sum){
if (t==NULL) return;
int x=t->data;
q.push_back(x);
if (t->leftchild==NULL &&t->rightchild==NULL){
if (s+x==sum){
for (int i=0; i<q.size(); i++)
cout<<q[i]<<" ";
cout<<endl;
}
}
Find_Path(t->leftchild,s+x,sum);
Find_Path(t->rightchild,s+x,sum);
q.pop_back();
}