数据结构--图
1. 术语
- V(Vertex): 顶点
- E(Edge): 边
- (v, w): v顶点与w顶点双向连接
- <v, w>: v顶点指向w顶点的单项连接
2. 图的表示
2.1 邻接矩阵法
G[i][j] = 1: <vi, vj>是G的边
=0: 无边
| 0 | 1 | ... | n | |
|---|---|---|---|---|
| 0 | 0 | 1 | 0 | |
| 1 | 1 | 0 | ||
| ... | 0 | 0 | ||
| n | ... | ... | ... | 0 |
-
无向图用全阵浪费一半空间
方案:用链表(单行)表示元素下表:(i*(i+1)/2+j)
ps:整个表看起来:对角线为0,对称
0 ... 0 ... ... 0 ... ... ... 0 ... ... ... ... 0 -
对于网络:值表示权重
网络:有权重的图
2.2 邻接表法
ps:用这种方法表示的图够稀疏才划算,且:图的表示并非只有这两种,要根据实际情况来设计
G[0]: 指针数组
G[0] --> |下标|指针域| --> |下标|指针域| --> ...
G[1] --> |下标|指针域| --> |下标|指针域| --> ...
...
G[n-1] --> |下标|指针域| --> |下标|指针域| --> ...
ps: 对于带权的网络,还要另加一个域表示权重
特点:
- 方便找临界点
- 节约稀疏图的空间(N个头指针+2E个节点)
- 计算“度”
- 无向图:方便
- 有向图:不方便,需构造“你邻接表”来计算“入度”
- 不方便检查对顶点是否存在边
3. 图的遍历
3.1 DFS
void DFS(Vertex V){
visited[V] = true; //点亮第一盏灯
for(v的每一个邻接点W){ //观察与v相连的灯
if(!visited[W]) //若未被点
DFS(W); //点亮,进入堆栈
}
}
对于N个顶点,E条边的图,时间复杂度:
- 邻接表:O(N+E)
- 邻接矩阵:O(N2)
自然语言描述:往下走,遇到路口挑第一个走,走到不通就往回退,退到起点遍历完成
3.2 BFS
类似树的BFS,树是特殊的图,借助队列来实现
void BFS(Vertex V){
visited[V] = true;//点灯
Enqueue(V, Q);//将与V相连的点入队
while(!IsEmpty(Q)){//队列不空
V = Dequeue(Q);//出队
for(V的每个邻接点W){
visited[w] = true;//电灯
Enqueue(W,Q);//入队
}
}
}
对于N个顶点,E条边的图,时间复杂度:
- 邻接表:O(N+E)
- 邻接矩阵:O(N2)
3.3 总结
-
为何需要两种遍历
每种遍历适用于不同情况,故两种皆有用
- BFS:优点是可以得到最优解,缺点是在树的层次较深并且子节点个数较多的情况下,消耗内存现象十分严重。因此,BFS适用于节点的子节点个数不多,并且树的层次不太深的情况。
- DFS:优点是消耗内存小,缺点是难以寻找最优解,仅仅只能寻找有解。
-
图不连通怎么办
//每一个DFS都是在把图的一个连通分量遍历(不连通的部分为一个连通分量) void ListComponent(Graph G){ for(each V in G)//每个连通分量处理一遍 if(!visited[V]) DFS(V); }
4. 最短路径
源点:起点
最短路径
- 单源最短路径问题(从某点到其他所有点的最短路径)
- 无权图单源
- 有权图单源
- 多源最短路径问题(求任意的顶点的最短路径)
4.1 无权单源
按递增(非递减)的顺序找出到各个顶点的最短路
类似BFS一样利用队列向外一圈一圈拓展开来,以计算和记录圈层和最短路径经过的顶点
v3 --> v1 --> v4
(无法上传图,只能创一个像链表一样的图)
0: v3
1: v1
2: v4
代码:
//dist[W] = S到W的最短路径
//dist[S] = 0
//path[W] = S到W的路上经过的某顶点
//与BFS类似,只不过这里不用点亮而是用记录的方式得到最短路径。
void Unweighted(Vertex S)
{
Enqueue(S,Q);
while(!Empty(Q)){
V = Dequeue(Q);
for(V的每个邻接点W)
if(dist[W] == -1) //如果dist[W]等于初始值-1
dist[W] = dist[V] + 1; //距离+1
path[W] = V; //记录路径上一个点,利用堆栈可以得到路径上所有的点
Enqueue(W,Q);
}
}
4.2 有权单源(Dijkstra算法)
按递增(非递减)的顺序找出到各个顶点的最短路
Dijkstra算法
- 令S = {源点s + 已经确定了最短路径的顶点vi}
- 对任一未收录的顶点v,定义dist[v]为s到v的最短路径长度,但该路径仅经过S中的顶点。即路径{s --> (vi in S) --> v 的最小长度}
- 若路径是按照递增(非递减)的顺序生成的,则
- 真正的最短路必须只经过S中的顶点
- 每次从为收录的顶点中选一个dist最小的记录(贪心算法的思想)
- 增加一个v进入S,可能影响另外一个w的dist值(新增的这个v一定在s到w的路径上,更即s是w的邻接点,v新增进S,只能影响v的邻接点的dist值,如果不是,那么dist值就不是S内的最短路了,而是全部点的最短路了,这冲突了)
- dist[w] = min
/* 应该将源点和第一圈层的顶点的值正确赋值(如源点的dist从初始值无穷大赋值为0后),然后将源点收录,再进入Dijkstra算法 */
void Dijkstra(Vertex s)
{
while(1){
V = 未收录顶点中dist最小者;
if( 这样的V不存在 )
break;
collected[V] = true; //表示V边已被收录
for( V的每个邻接点W )
if( collected[W] == false ) //未被收录的点
if( dist[V]+E<v,w> < dist[W] ){ //dist的初始值应该定义为无穷大
dist[W] = dist[V] + E<v,w>;
path[W] = V; //记录W的上一个点是V
}
}
}
/* 不能解决有负边的情况 */
//dist 初始值应该是无穷大
//path 初始值应该是-1
计算算法复杂度方法:
- 直接扫描所有未收录顶点--O(|V|)
- T = O(|V|2) + |E|)
- 对于稠密图效果好
- 稠密图(V 与 E 的数量不在同一个数量级的差别上)
- 将dist存在最小堆中--O(log|V|)
- 更新dist[w]的值 - O(log|V|)
- T = O(|V|log|V| + |E|log|V|) = O(|E|log|V|)
- 对于稀疏图效果好
4.3 多源最短路算法
-
方法一:直接将单源最短路算法调用|V|遍
- T = O(|V|3 + |E|*|V|) ——对于稀疏图效果好
-
方法二:Floyd算法
- T = O(|V|3) ——对于稠密图效果好
Folyd算法:每一条最短路是逐步成型的
- 定义一个矩阵,Dkij = 路径 {i -> { l <= k } -> j}的最小长度
- D0, D1, ..., D|V|-1 ij,慢慢一步一步的增加一条边,直到最后即给出了 i 到 j 的真正最短距离
- 最初的D-1是一个零阶矩阵,对角线是零,Dij 表示的是 i 到 j 的路径长度,如果两点没有直接边的话,必须初始化为正无穷
- 当 Dk-1 已经完成,递推到 Dk 时:
- 或者 k 不属于最短路径 { i -> { l <= k } -> j },则 Dk = Dk-1
- 或者 k 属于最短路径 { i -> { l <= k } -> j },则该路径必定由两段最短路径组成:Dkij = Dk-1 ij + Dk-1 kj
void Floyd(){
for(i = 0; i < N; i++)
for(j = 0; j < N; j++){
D[i][j] = G[i][j];
path[i][j] = -1; //path矩阵是用来记录最短路径的
}
for(k = 0; k < N; k++)
for(i = 0; i < N; i++)
for(j = 0; j < N; j++)
if(D[i][j]) + D[k][j] < D[i][j]){
D[i][j] = D[i][k] + D[k][j];
path[i][j] = k;
}
}
// T = O(|V|3次方)
5. 最小生成树问题
-
是一棵树
- 无回路
- |V|个顶点一定有|V|-1条边
-
是生成树
- 包含全部顶点
- |V|-1条边都在图里
-
边的权重和最小
ps:
- 向生成树中任加一条边都一定构成回路
- 最小生成树存在 <---> 图连通
贪心算法是解决这个问题的唯一算法,不管你是Prim算法还是什么别的,本质上都是一种贪心算法。
贪心算法
- 什么是“贪”:每一步都要最好的
- 什么是“好”:权重最小的边
- 需要约束:
- 只能用图里有的边
- 只能正好用掉|V|-1条边
- 不能有回路
5.1 Prim算法——让一棵小树长大
- 初始化:收入一个顶点
- 在与该顶点相连的边中选择一条权重最小的,把边对面的顶点收录进来
- 在与已经收录进来的顶点相邻的边中,选择权重最小的,收录进来,注意,每一步收录都要避免有回路生成
- 循环第三步,直至所有的顶点都收录进来
void Prim(){
MST = (s); //把s根节点收进生成树
while(1){
V = 未收录顶点中dist最小者; //令与生成树相邻的权重最小的点为V
if( 这样的V不存在 ) //已经没有这样连接的点,则退出循环
break;
将V收录进MST:dist[v] = 0; //为0表示这个点已经被收录进生成树里了
for( V 的每个邻接点 W ) //因为收录了V进去,所以要更新与V邻接的点到生成树的距离、权重
if(dist[W] != 0)
if(E(v,w) < dist[W]){ //如果原来W到树的距离大于收录后V到W的距离
dist[W] = E(v,w); //刷新W到数的距离为V到W的距离
parent[W] = V; //令W的父节点为V
}
}
if( MST中收的顶点不到|V|个 ) //有顶点与生成树不连接
Error("生成树不存在");//生成树不存在,因为没有把所有的点纳入进来
}
// dist[V] = E(s,V) 表示的是V节点到已经收录的图的距离
// dist[V] = 正无穷 表示的是节点没有直接与已经收录的图相连
// parent[s] = -1 表示s点是生成树的根节点,别的值则代表这个点的父节点
// T = O(|V|的平方)
这个算法适合稠密图
5.2 Kruskal算法——将森林合并成树
- 初始时,每一个顶点都是独立的,都看作一棵树
- 然后将全部的点中把距离最近的点都连上
- 然后继续连,当然,树不能成环
- 直到所有的点都连通起来,循环结束
void Kruskal(Graph G){
MST = { };
while( MST中不到 |V|-1 条边 && E 中还有边 ){
从 E 中取一条权重最小的边 E(V,W); //最小堆
将 E(V,W) 从 E 中删除;
if( E(V,W) 不在 MST 中构成回路) //并查集,检验如果V和W在同一棵树上,那么连接上就会构成回路
将 E(V,W) 加入 MST;
else
彻底无视 E(V,W);
}
if( MST 中不到 |V|-1 条边 )
Error("生成树不存在");
}
// T = O(|E| log|E|)
这个算法适合稀疏图

浙公网安备 33010602011771号