常见算法的复杂度及相应的分析

今天也是为了cc,努力奋斗的一天ヾ(≧▽≦*)o

貌似保研面试的时候会问一些这样的问题。。。那我就整理一下吧。反正刷《算法笔记》的时候看见时间复杂度就记到这里呗。

Bellman-Ford

时间复杂度

O(VE),其中V是顶点个数,E是边数。

关键代码

for(i = 0;i < n - 1;i++){	//执行n-1轮操作,其中n为顶点数 
	for(each edge u->v){	//每轮操作都遍历所有边 
		if(d[u] + length[u -> v] < d[v]){	//以u为中介点可以使d[v]更小 
			d[v] = d[u] + length[u -> v];	//松弛操作 
		}
	} 
} 

SPFA

时间复杂度

O(kE),其中E是图的边数,k是一个常数,在很多情况下k不超过2,可见这个算法在大部分数据时异常高效,并且经常性地优于堆优化的Dijkstra算法。但是如果图中有从源点可达的负环时,传统SPFA的时间复杂度会退化成O(VE)

关键代码

//主体部分
while(!Q.empty()){
	int u = Q.front();		//队首顶点编号为u
	Q.pop();				//出队
	inq[u] = false;			//设置u为不在队列中
	
	//遍历u的所有邻接边v
	for(int j=0;j<Adj[u].size();j++){
		int v = Adj[u][j].v;
		int dis = Adj[u][j].dis;
		
		//松弛操作
		if(d[u] + dis < d[v]){
			d[v] = d[u] + dis;
			if(!inq[v]){	//如果v不在队列中 
				Q.push(v);	//v入队
				inq[v] = true;	//设置v为在队列中
				num[v++];		//v的入队次数加一
				if(num[v] >= n){
					return false;	//有可达负环 
				} 
			}
		} 
	}	 
} 

Floyd

时间复杂度

O(n*n*n),其中n是顶点的个数。

关键代码

void Floyd(){
	for(int k=0;k<n;k++){
		for(int i=0;i<n;i++){
			for(int j=0;j<n;j++){
				if(dis[i][k] != INF && dis[k][j] != INF && dis[i][k] + dis[k][j] < dis[i][j]){
					dis[i][j] = dis[i][k] + dis[k][j];		//找到更短的路径 
				}
			}
		}
	}
} 

prim

时间复杂度

O(V*V),其中邻接表实现prim算法可以通过堆优化使时间复杂度降为 O ( V l o g V + E ) O(VlogV+E)
此外, O ( V 2 ) O(V^2) 的时间复杂度也说明,尽量在图的顶点数目较少而边数较多的情况下(即稠密图)上使用prim算法。至于为什么prim算法得到的生成树一定是最小生成树,可以参考《算法导论》的相关证明。

关键代码

int prim(){	//默认0号为初始点,函数返回最小生成树的边权之和 
	fill(d,d+MAXV,INF);			//fill函数将整个d数组赋值为INF(慎用memset)
	d[0] = 0;					//只有0号顶点到集合S的距离为0,其余为INF
	int ans = 0;				//存放最小生成树的边权之和
	for(int i=0;i<n;i++){		//循环n次 
		int u = -1,MIN = INF;	//u使d[u]最小,MIN存放该最小的d[u]
		for(int j=0;j<n;j++){	//找到未访问的顶点中d[]最小的 
			if(vis[j] == false && d[j] < MIN){
				u = j;
				MIN = d[j];
			}	
		}
		
		//找不到小于INF的d[u],则剩下的顶点和集合S不连通
		if(u == -1){
			return -1;
		}
		
		vis[u] = true;	//标记u为已访问
		ans += d[u];	//将与集合S距离最小的边加入到最小生成树
		for(int v=0;v<n;v++){
			//v未访问 && u能到达v && 以u为中介点可以使v离集合S更加近
			if(vis[v] == false && G[u][v] != INF && G[u][v] < d[v]){
				d[v] = G[u][v];	//将G[u][v]赋值给d[v] 
			} 
		} 		 
	}
} 

kruskal

时间复杂度

O ( E l o g E ) O(ElogE) E E 为图的边数。
显然kruskal适合顶点数较多、边数较少的情况,这和prim算法恰恰相反。于是可以根据题目所给的数据范围来选择合适的算法,即如果是稠密图(边多),则用prim算法;如果是稀疏图(边少),则用kruskal算法。

关键代码

int father[N];	//并查集数组
int findFather(int x){	//并查集查询函数 
	...
}

//kruskal函数返回最小生成树的边权之和,参数n为顶点个数,m为图的边数
int kruskal(int n,int m){
	//ans为所求边权之和,Num_Edge为当前生成树的边数
	int ans = 0;
	int Num_Edge = 0;
	for(int i=1;i <= n;i++){	//假设题目中顶点范围是[1,n] 
		father[i] = i;	//并查集初始化 
	}
	
	sort(E,E+m,cmp);	//所有边按边权从小到大排序
	for(int i=0;i<m;i++){	//枚举所有边 
		int faU = findFather(E[i].u);	//查询测试边两个端点所在集合的根结点
		int faV = findFather(E[i].v);
		if(faU != faV){	//如果不在一个集合中 
			father[faU] = faV;	//合并集合(即将测试边加入到最小生成树中)
			ans += E[i].cost;	//边权之和增加测试边的边权
			Num_Edge++;			//当前生成树的边数加1
			if(Num_Edge == n - 1){
				break;	//边数等于顶点数减1时结束算法 
			} 
		} 
	} 
	if(Num_Edge != n-1){
		return -1;	//无法连通时返回-1 
	}else{
		return ans;	//返回最小生成树的边权之和 
	}
} 

参考文档

算法笔记

发布了15 篇原创文章 · 获赞 0 · 访问量 214

猜你喜欢

转载自blog.csdn.net/yc_cy1999/article/details/103955768