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

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

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

时间复杂度概述

时间复杂度就是 问题规模 n n n 的函数, T ( n ) = O ( f ( n ) ) T(n) = O(f(n)) T(n)=O(f(n)),常用的时间复杂度比较为:
O ( 1 ) < O ( l o g n ) < O ( n ) < O ( n l o g n ) < O ( n 2 ) < O ( n 3 ) < O ( 2 n ) < O ( n ! ) < O ( n n ) O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) <O(2^n) < O(n!) < O(n^n) O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)

记忆方法:常对幂指阶

1. Dijkstra

时间复杂度

没有堆优化的为 O ( V 2 + E ) O(V^2+E) O(V2+E),堆优化过后的为 O ( V l o g V + E ) O(VlogV+E) O(VlogV+E)

关键代码

void Dijkstra(int s){	//s为起点 
	fill(d,d+MAXV,INF);		//fill函数将整个d数组赋值为INF(慎用memset) 
	d[s] = 0;	//起点s到达自身的距离为0
	for(int i=0;i<n;i++){
		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;//这个细节需要注意~~~
		vis[u] = true;	//标记u为已访问
		//只有下面这个for与邻接矩阵的写法不同
		for(int j=0;j<Adj[u].size();j++){
			int v = Adj[u][j].v;	//通过邻接表直接获得u能够到达的顶点v
			if(vis[v] == false && d[u] + Adj[u][j].dis < d[v]){
				//如果v未访问 && 以u为中介点可以使d[v]更优
				d[v] = d[u] + Adj[u][j].dis;	//优化d[v] 
			} 
		} 
	} 
} 

堆优化版本

	while(!q.empty()){
		//每次找最小的 
		int u =  q.top().v;
		int cost = q.top().cost;
		q.pop();
		
		//如果已经被访问过了,这里需要注意! 
		if(vis[u] == true){
			continue;
		}
		
		vis[u] = true;
	
		//接下来进行更新
		for(int j=0;j<adj[u].size();j++){
			int v = adj[u][j].v;
			int cost = adj[u][j].cost;
			if(vis[v] == false && d[u]+cost < d[v]){
				q.push(node(v,d[u]+cost));
				d[v] = d[u]+cost;
			}
		} 	 
	}

2. 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];	//松弛操作 
		}
	} 
} 

3. 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;	//有可达负环 
				} 
			}
		} 
	}	 
} 

4. Floyd

时间复杂度

O ( V 3 ) O(V^3) O(V3),其中 V V V是顶点的个数。

关键代码

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];		//找到更短的路径 
				}
			}
		}
	}
} 

5. prim

时间复杂度

O(V*V),其中邻接表实现prim算法可以通过堆优化使时间复杂度降为 O ( V l o g V + E ) O(VlogV+E) O(VlogV+E)
此外, O ( V 2 ) O(V^2) O(V2)的时间复杂度也说明,尽量在图的顶点数目较少而边数较多的情况下(即稠密图)上使用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] 
			} 
		} 		 
	}
} 

6. kruskal

时间复杂度

O ( E l o g E ) O(ElogE) O(ElogE) E 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;	//返回最小生成树的边权之和 
	}
} 

拓扑排序

时间复杂度

O ( V + E ) O(V+E) O(V+E)
如果DAG有 V V V个顶点, E E E条边,在拓扑排序的过程中,搜索入度为零的顶点所需的时间是 O ( V ) O(V) O(V),建立入度数组为 E E E。在正常情况下,每个顶点进一次队列,出一次队列,所需时间 O ( V ) O(V) O(V)。每个顶点入度减1的运算共执行了 E E E次。所以总的时间复杂为 O ( V + E ) O(V+E) O(V+E)

关键代码

bool topologicalSort(){
	//初始化
	int num = 0;	//记录加入拓扑序列的顶点数
	queue<int> q;
	for(int i=0;i<n;i++){
		if(inDegree[i] == 0){
			q.push(i);	//将所有入度为0的顶点入队 
		}
	}
	
	//循环体
	while(!q.empty()){
		int u = q.front();	//取队首顶点u
		//printf("%d",u);	//此处可输出顶点u,作为拓扑序列中的顶点
		q.pop();
		for(int i=0;i<G[u].size();i++){
			int v = G[u][i];	//u的后继结点v
			inDegree[v]--;		//顶点v的入度减1
			if(inDegree[v] == 0){	//顶点v的入度减为0则入队 
				q.push(v); 
			} 
		}
		G[u].clear();		//清空顶点u的所有出边(如无必要可不写)
		num++;				//加入拓扑序列的顶点数加1 
	}
	if(num == n) return true;	//加入拓扑序列的顶点数为n,说明拓扑排序成功
	else return false;			//加入拓扑排序的顶点数小于n,说明拓扑排序失败(即存在环) 
} 

动态规划

最大连续子序列和

时间复杂度

O ( n ) O(n) O(n)
不使用DP的话,使用暴力是 O ( n 3 ) O(n^3) O(n3)或者 O ( n 2 ) O(n^2) O(n2)

关键代码

//边界
dp[0] = A[0];

for(int i=1;i<n;++i){
	//状态转移矩阵
	dp[i] = max(A[i],A[i]+dp[i-1]); 
}

最长不下降子序列(LIS)

时间复杂度

O ( n 2 ) O(n^2) O(n2)
不使用DP的话,使用暴力是 O ( 2 n ) O(2^n) O(2n)

关键代码

int ans = -1;	//记录最大的dp[i]
for(int i=1;i <= n;++i){
	dp[i] = 1;	//边界初始条件(即先假设每个元素自成一个子序列)
	for(int j=1;j<i;++j){
		if(A[i] >= A[j] && (dp[j]+1>dp[i])){
			dp[i] = dp[j] + 1;	//状态转移方程,用以更新dp[i] 
		}
	}
	ans = max(ans,dp[i]); 
}

最长公共子序列

时间复杂度

O ( n m ) O(nm) O(nm)
不使用DP的话,使用暴力是 O ( 2 n + m ⋅ m a x ( n , m ) ) O(2^{n+m}·max(n,m)) O(2n+mmax(n,m))

关键代码

//边界
for(int i=0;i<=lenA;i++){
	dp[i][0] = 0;
}
for(int j=0;j<=lenB;j++){
	dp[0][j] = 0;
}
//状态转移方程
for(int i=1;i<=lenA;i++){
	for(int j=1;j<=lenB;j++){
		if(A[i] == B[j]){
			dp[i][j] = dp[i-1][j-1] + 1;
		}else{
			dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
		}
	}
}
//dp[lenA][lenB]是答案
printf("%d\n",dp[lenA][lenB]); 

01背包

时间复杂度

O ( n V ) O(nV) O(nV) V V V表示背包自身的容量。
不使用DP的话,使用暴力是 O ( 2 n ) O(2^n) O(2n)

空间复杂度

O ( V ) O(V) O(V)

关键代码

for(int i=1;i<=n;i++){
	for(v = V; v >= w[i] ;v--){		//逆序枚举v 
		dp[v] = max(dp[v],dp[v-w[i]]+c[i]);
	}
}

参考文档

算法笔记

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值