图和树数据结构

图的表示方式

图的表示方式

1.领接矩阵

    这是图表示的一种经典方式,当图越接近完全图时,领接矩阵的效率就越高,且使用领接矩阵可以在O(1)的时间复杂度内获得边的权值,缺点就是在稀疏图中由于开辟空间的时间复杂度是O(n^2)的,所以时间开销较大,除此之外,使用领接矩阵存储图数据,则其遍历图的时间复杂度一定是O(n ^2)的,下面就简单的以领接矩阵实现的图数据实现其迪杰斯塔拉算法。

code
#include<bits/stdc++.h>
using namespace std;
#define N 50
int main(){
	srand((unsigned int)time(NULL));
	int g[N][N];//以100个节点的数据为例
	memset(g,0,sizeof(g));//memset只能初始化为0或者-1; 
	int a,b,v;
	set<pair<int,int>> eset;//用来判断边是否重复 
	for(int i=0;i<N*10;i++){
		a=rand()%N;
		b=rand()%N;
		v=rand()%10+1;
		if(eset.count({a,b})==1)continue;//简单图 
		eset.emplace(a,b);
		eset.emplace(b,a);
		g[a][b]=v;
		g[b][a]=v;
	} 
 	priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>> que;//注意默认的堆是大根堆,我们要使用的是小根堆 
 	int dis[N];//记录单元最短路的长度 
 	int color[N];//标记已经遍历的顶点 
 	for(int i=0;i<N;i++)dis[i]=10000;
 	que.emplace(0,0);//优先级队列中存储的是到0的权值,以及顶点编号
	dis[0]=0; 
	for(;!que.empty();){
		pair<int,int> k=que.top();
		que.pop();//求单元最短路可以不再这里删除优先级队列的顶点,但是求prim算法是一定要在这里删除 
		if(color[k.second]==1)continue;
		color[k.second]=1;
		for(int i=0;i<N;i++){//领接矩阵这里需要遍历一行,时间复杂度为O(N) 
			if(g[k.second][i]>0){
				dis[i]=min(dis[i],dis[k.second]+g[k.second][i]);
				que.emplace(dis[i],i);
			}
		}
	} 
	for(int i=0;i<N;i++)cout<<dis[i]<<" ";
	return 0; 
	
}
领接链表

    领接链表是图数据的存储空间进行了优化,使用链表的思想来存储图数据,实际上编程中我们可以用动态数据来模拟链表,好处是遍历时间复杂度是O(V+E)的,其中V是顶点数,E是边数,对于稀疏图,遍历的时间复杂度是线性的,且空间复杂度也是线性的,缺点是由于使用链表的思想来存储节点数据的,所以其查询每一条边是否存在都是O(k)的,其中k是查询边的端点上链接的节点数量。
    当然这些都可以用哈希函数来克服,但是仍然需要开辟额外的空间,但好处就是在稀疏图中时间复杂度是O(V)的。
    下面以prim算法的实现来展示领接链表形式存储的图数据,这里用vector动态数组来模拟每个节点的领接链表。

code1

使用map存储边权值

#include<bits/stdc++.h>
using namespace std;
#define N 50
int main(){
	srand((unsigned int)time(NULL));
	vector<vector<int>> g(N); 
	int a,b,v;
	map<pair<int,int>,int> emap;//由于这里使用数组模拟的领接链表并没有开辟存储边权值的空间,所以使用map查询 
	for(int i=0;i<N*10;i++){
		a=rand()%N;
		b=rand()%N;
		v=rand()%10+1;
		if(emap.count({a,b})==1)continue;//简单图 
		emap[{a,b}]=v;
		emap[{b,a}]=v;
		g[a].push_back(b);
		g[b].push_back(a);
	} 
 	priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>> que;//注意默认的堆是大根堆,我们要使用的是小根堆 
 	int dis[N];//记录单元最短路的长度 
 	int color[N];//标记已经遍历的顶点 
 	for(int i=0;i<N;i++)dis[i]=10000;
 	que.emplace(0,0);//优先级队列中存储的是到0的权值,以及顶点编号
	dis[0]=0; 
	int ans=0;//这里只输出最小生成树的边权和。 
	for(;!que.empty();){
		pair<int,int> k=que.top();
		que.pop();//求单元最短路可以不再这里删除优先级队列的顶点,但是求prim算法是一定要在这里删除 
		if(color[k.second]==1)continue;
		color[k.second]=1;
		ans+=k.first; 
		for(int i=0;i<g[k.second].size();i++){//领接矩阵这里需要遍历一行,时间复杂度为O(N) 
			dis[g[k.second][i]]=min(dis[g[k.second][i]],emap[{k.second,g[k.second][i]}]);//注意这里与迪杰斯塔拉的区别,这里是i到局部最小生成树直接的最小距离 
			que.emplace(dis[g[k.second][i]],g[k.second][i]);
		}                    
	} 
	//for(int i=0;i<N;i++)ans+=dis[i];//dis数组中存储的即为最小生成树的边权值,也可以在循环中统计,根据每个节点只会被处理一次,可以在遍历之前统计。 
	cout<<ans<<endl;
	return 0; 
}
code2

额外开辟空间保存边权值。

#include<bits/stdc++.h>
using namespace std;
#define N 50
int main(){
	srand((unsigned int)time(NULL));
	vector<vector<pair<int,int>>> g(N); 
	int a,b,v;
	set<pair<int,int>> eset;//由于这里使用数组模拟的领接链表并没有开辟存储边权值的空间,所以使用map查询 
	for(int i=0;i<N*10;i++){
		a=rand()%N;
		b=rand()%N;
		v=rand()%10+1;
		if(eset.count({a,b})==1)continue;//简单图 
		g[a].push_back({b,v});
		g[b].push_back({a,v});
	} 
 	priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>> que;//注意默认的堆是大根堆,我们要使用的是小根堆 
 	int dis[N];//记录单元最短路的长度 
 	int color[N];//标记已经遍历的顶点 
 	for(int i=0;i<N;i++)dis[i]=10000;
 	que.emplace(0,0);//优先级队列中存储的是到0的权值,以及顶点编号
	dis[0]=0; 
	int ans=0;//这里只输出最小生成树的边权和。 
	for(;!que.empty();){
		pair<int,int> k=que.top();
		que.pop();//求单元最短路可以不再这里删除优先级队列的顶点,但是求prim算法是一定要在这里删除 
		if(color[k.second]==1)continue;
		color[k.second]=1;
		ans+=k.first; 
		for(int i=0;i<g[k.second].size();i++){//领接矩阵这里需要遍历一行,时间复杂度为O(N) 
			dis[g[k.second][i].first]=min(dis[g[k.second][i].first],g[k.second][i].second);//注意这里与迪杰斯塔拉的区别,这里是i到局部最小生成树直接的最小距离 
			que.emplace(dis[g[k.second][i].first],g[k.second][i].first);
		}                    
	} 
	cout<<ans<<endl;
	return 0; 
}

    上述实现方式相比于真正使用链表的方式来说,编程简单,模板性较强,但是由于vector动态开辟空间的时间复杂度是较高的,所以时间复杂度相对较高。

树的表示方式

    显然前面的两种表示方式都可以用来表示树,并且使用领接链表可以更好的表示数的结构,因为树是以个稀疏图,所以使用领接链表的时间复杂度相对较低。
    使用领接链表我们无法使用固定长度的数组来对图数据进行保存,所以需要使用vector(动态数组)来进行模拟。缺点是频繁的开辟空间会造成时间开销较大,所以对于稀疏图应当视数据情况来选择使用vector还是链表。
    但是除了上面两种表示形式外,还有很多存储树数据的结构,而有些则是为了减低时间复杂度而构造的树形结构。这些结构大多使用数组对数数据进行存储,最常见的如二叉树的存储,就可以使用数组进行模拟,这里介绍几种常见的树形结构。

兄弟链法

    这是对稀疏图数据进行保存的一种不错的方法,它只保存边数据,以及一个额外的兄弟节点之间单向指向的关系,该关系用一个数组来表示,类似于BFS中为了重构最短路而生成的一树结构,该树中是子节点指向父节点,而兄弟链法,则是兄弟指向兄弟,构建的关系一定是一个偏序关系,也即表示成图的形式一定是有向无环图的形式。

code
#include<bits/stdc++.h>
using namespace std;
#define N 10
#define M 100//最大边数 
int edge[M][3];
int pre[N];//用pre数组实现一个兄弟节点之间的互连。 
int color[N]; 
int arr[N];//并查集
int id=0;
int find(int x){
	return x==arr[x]?x:arr[x]=find(arr[x]);
} 
void merge(int x,int y){
	x=find(x);
	y=find(y);
	arr[x]=y;
}
int DFS(int k){
	int num=0;
	for(int i=pre[k];i;i=edge[i][0]){
		if(color[edge[i][1]]==0){
			color[edge[i][1]]=1;
			num+=edge[i][2];
			num+=DFS(edge[i][1]); 
		}
	}
	return num;
} 
inline void add(int a,int b, int v){
	edge[++id][0]=pre[a];//注意这里一定要用++id,否则后面更新是错的 
	edge[id][1]=b;
	edge[id][2]=v;
	pre[a]=id;
	return;
}
int main(){
	srand((unsigned int)time(NULL));
	memset(pre,0,sizeof(pre)); 
	for(int i=0;i<N;i++)arr[i]=i; 
	int a,b,v;
	for(int i=0;i<N*10;i++){
		a=rand()%N;
		b=rand()%N;
		v=rand()%10+1;
		if(find(a)==find(b))continue;
		merge(a,b);
		add(a,b,v);
		add(b,a,v);
	} 
	memset(color,0,sizeof(color));
	color[0]=1;
	cout<<DFS(0)<<endl;
	return 0; 
}

    上面是一个简单的使用兄弟链法实现的图数据的遍历的算法,用于计算树结构的边权和,其中空间开销为顶点数的4倍,是一个常数级的,这样存储的一个好处就是,当我们知道边的数目是,就可以使用静态空间来存储树结构,而不必使用vector开辟动态数组,从而提高执行的效率。

树状数组

    用于查询区间和,或者构建区间和递增数组进而使用二分查找的结构,可以实现在O(logn)时间复杂度内的修改和查询,这里给出其模板实现;

code
#include<bits/stdc++.h>
using namespace std;
#define N 1000
#define maxnum 10
int arr[N+1];//需要从下标1开始,所以数组要增加一位 
int lowbit(int x){
	return x&(-x);
}
void update(int x,int v){
	for(int i=x;i<N+1;i+=lowbit(i)){
		arr[i]+=v;
	}
	return;
}
int search(int x){
	int ans=0;
	for(int i=x;i>0;i-=lowbit(i)){
		ans+=arr[i];
	}
	return ans;
}
int main(){
	memset(arr,0,sizeof(arr));
	srand((unsigned int)time(NULL));
	for(int i=0;i<N;i++){
		int a=rand()%maxnum+1;
		update(i+1,a);
	}
	for(int i=0;i<N;i++){
		cout<<search(i+1)<<" "; 
	}
	return 0;
	
} 

线段树

    树状数组可以实现的问题,线段树都可以实现,但是在常数级别上,树状数组的常数系数更小,且需要的空间更小,线段树的保存方式是采用完全二叉树的表示形式,所以理解起来相对简单,复合人的直观印象。通常线段树需要开辟原始数据四倍大小的空间用来存储线段树中所有节点,其中叶子节点存储原始数据中的每一个值,而分支节点则存储的是数组连续一段区间内的值。

code

递归实现,写起来简单,但是复杂度较迭代实现高一些,因为要开辟递归栈。

#include<bits/stdc++.h>
using namespace std;
#define N 100
int arr[N*4];
int lazy[N*4]; 
int data[N];
void up(int rt){
	arr[rt]=arr[rt<<1]+arr[rt<<1|1];
	return;
}
void down(int rt,int ln,int rn){//延时更新,使用惰性标记,惰性标记保存的孩子节点应该更新的值,本层已经被更新 
	if(lazy[rt]){
		lazy[rt<<1]+=lazy[rt];
		lazy[rt<<1|1]+=lazy[rt];
		arr[rt<<1]+=lazy[rt]*ln;
		arr[rt<<1|1]+=lazy[rt]*rn;
		lazy[rt]=0;
	}
	return;
}
void build(int l,int r,int rt){
	if(l==r){
		arr[rt]=data[l];
		return;
	}
	int m=(r+l)/2;
	build(l,m,rt<<1);
	build(m+1,r,rt<<1|1);
	up(rt);
	return;
}
void update(int x,int v,int l,int r,int rt){//点更新,只有点更新则无需惰性标记,有区间更新则需要惰性标记
	 if(l==r){
	 	arr[rt]+=v;
	 	return;
	 } 
	 int m=(r+l)/2;
	 down(rt,m-l+1,r-m);
	 if(x<=m)update(x,v,l,m,rt<<1);
	 else update(x,v,m+1,r,rt<<1|1);
	 up(rt);
	 return;
	
}
void update(int x,int y,int v,int l,int r,int rt){
	if(x<=l&&r<=y){
		arr[rt]+=v*(r-l+1);
		lazy[rt]+=v;
		return;
	}
	int m=(r+l)/2;
	down(rt,m-l+1,r-m);
	if(x<=m)update(x,y,v,l,m,rt<<1);
	if(y>m)update(x,y,v,m+1,r,rt<<1|1);
	up(rt);
	return;
}
int query(int x,int y,int l,int r,int rt){
	if(x<=l&&r<=y){
		return arr[rt];
	}
	int m=(l+r)/2;
	down(rt,m-l+1,r-m);
	int ans=0;
	if(x<=m)ans+=query(x,y,l,m,rt<<1);
	if(y>m)ans+=query(x,y,m+1,r,rt<<1|1);
	return ans;
}
int main(){
	std::ios::sync_with_stdio(false);
	srand((unsigned int)time(NULL));
	memset(arr,0,sizeof(arr));
	memset(lazy,0,sizeof(lazy));
	for(int i=0;i<N;i++){
		data[i]=rand()%10+1;
	}
	build(0,N-1,1);//注意根节点是1,从1开始建立线段树
	for(int i=0;i<N;i++)cout<<query(0,i,0,N-1,1)<<" ";
	cout<<endl;
	cout<<endl;
	for(int i=0;i<100;i++){
		update(i,rand()%100+1,0,N-1,1);
	} 
	for(int i=0;i<N;i++)cout<<query(0,i,0,N-1,1)<<" ";
	cout<<endl;
	cout<<endl;
	for(int i=1;i<100;i++){
		update(i-1,i,rand()%100+1,0,N-1,1);
	} 
	for(int i=0;i<N;i++)cout<<query(0,i,0,N-1,1)<<" ";
	return 0;
}
并查集

这是一种双亲表示法,用以合并结合,判断集合是否冲突。

code
#include<bits/stdc++.h>
using namespace std;
#define N 100
int arr[N];
int ran[N];
int find(int x){
	return x==arr[x]?x:arr[x]=find(arr[x]);
}
void merge(int x,int y){
	x=find(x);
	y=find(y);
	if(ran[x]<ran[y]){
		arr[x]=y;
	}else{
		arr[y]=x;
		if(ran[x]==ran[y]){
			ran[x]++;
		}
	}
	return;
}
int mian(){
	std::ios::sync_with_stdio(false);
	srand((unsigned int)time(NULL));
	for(int i=0;i<N;i++)arr[i]=i;
	memset(ran,0,sizeof(ran)); 
	for(int i=0;i<80;i++){
		int a=rand()%N;
		int b=rand()%N;
		if(find(a)==find(b))continue;
		merge(a,b);
	}
	int ans=0;
	unordered_map<int,int> hash;
	for(int i=0;i<N;i++){
		if(hash.count(find(i))==0){
			ans++;
			hash[find(i)]++;
		}
	}
	cout<<ans;
	return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值