二叉树操作:求树的深度,求最近公共祖先LCA,求和为某值的所有路径,打印二叉树中的所有路径.

其他二叉树知识!二叉树知识汇总


目录

一、求树的深度

二、查找二叉树中的一个节点

三、最近公共祖先LCA

1.递归法 (链式存储)

2.普通的模拟法(邻接表建树)

3.倍增法求LCA

四、打印二叉树中的所有路径

五、求和为某值的所有路径


一、求树的深度

树的深度=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.递归法 (链式存储)

此方法前提是你已经建好了一棵用链式存储的树。

查找两个节点的最近公共祖先,分三种情况:

  1. 如果两个节点恰好在某个节点 t 的两边,那么最近公共祖先就是 t 。
  2. 如果两个节点都在 t 的左边,那么说明 t 不是最近的公共祖先,需要再往下找,也就是把 t 更新为 t 的左孩子,再递归。
  3. 如果两个节点都在 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();
} 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值