DS第6章学习小结
一、图
(一) 数学语言:G= (V, E) G:graph V:vertex E:edge
(二) 基本术语:
- 无向完全图vs有向完全图
完全图:两两相连
顶点vex
边edge
弧arc
无向完全图
n
n(n-1) /2
/
有向完全图
n
/
n(n-1)
- 权&网——带权图称为网
- 度、入度、出度——度(degree: the amount) 入度(in-degree) 出度(out-degree)——与顶点关联
- 连通、(强)连通图、(强)连通分量
连通
有路径
连通图*区别完全图
无向图中任两个顶点连通
连通分量
无向图中的极大连通子图;连通图的连通分量是其本身
强-
有向
(三) 存储结构:
- 邻接矩阵【二维数组】
1) 存储方法AM
a) 一维数组:顶点信息
b) 二维数组:邻接关系 注:若有向图,出度看行,入度看列,度=行+列
2)
1 #define MaxVnum 100 //顶点数最大值 2 typedef char VexType; //顶点的数据类型,根据需要定义 3 typedef int EdgeType; //边上权值的数据类型,若不带权值的图,则为0或1 4 typedef struct{ 5 VexType Vex[MaxVnum]; 6 EdgeType Edge[MaxVnum][MaxVnum]; 7 int vexnum,edgenum; //顶点数,边数 8 }AMGragh;
- 边集数组
1) 存储方法e[N*N]:通过数组存储每条边的起点和终点。如果是网,则增加一个权值域。
2)
1 struct Edge { 2 int u; 3 int v; 4 int w; 5 }e[N*N];
- 邻接表【链式存储】
1) 存储方法AL
a) 顶点:顶点信息、指向第一个邻接点的指针
b) 邻接点:邻接点下标、指向下一个邻接点的指针
2)
1 const int MaxVnum=100;//顶点数最大值 2 3 typedef char VexType;//顶点的数据类型为字符型 4 typedef struct AdjNode{ //定义邻接点类型 5 int v; //邻接点下标 6 struct AdjNode *next; //指向下一个邻接点 7 }AdjNode; 8 9 typedef struct VexNode{ //定义顶点类型 10 VexType data; // VexType为顶点的数据类型,根据需要定义 11 AdjNode *first; //指向第一个邻接点 12 }VexNode; 13 14 typedef struct{//定义邻接表类型 15 VexNode Vex[MaxVnum]; 16 int vexnum,edgenum; //顶点数,边数 17 }ALGragh;
二、图的应用
(一) DFS(深度优先搜索)
有点类似于树的前序遍历,即从一个选定的点出发,选定与其直接相连且未被访问过的点走过去,然后再从这个当前点,找与其直接相连且未被访问过的点访问,每次访问的点都标记为“已访问”,就这么一条道走到黑,直到没法再走为止。无路可走时就从当前点原路返回,看是否存在与这个点直接相连且未被访问的点。重复上述步骤,直到没有未被访问的点为止。
基于上述,DFS很适合使用递归,其代码如下:
1 #define MaxVnum 100 //顶点数最大值 2 bool visited[MaxVnum]; //访问标志数组,其初值为"false" 3 void DFS_AM(AMGragh G,int v)//基于邻接矩阵的深度优先遍历 4 { 5 int w; 6 cout<<G.Vex[v]<<"\t"; 7 visited[v]=true; 8 for(w=0;w<G.vexnum;w++)//依次检查v的所有邻接点 9 if(G.Edge[v][w]&&!visited[w])//v、w邻接而且w未被访问 10 DFS_AM(G,w);//从w顶点开始递归深度优先遍历 11 }
1 #define MaxVnum 100;//顶点数最大值 2 bool visited[MaxVnum]; //访问标志数组,其初值为"false" 3 void DFS_AL(ALGragh G,int v)//基于邻接表的深度优先遍历 4 { 5 int w; 6 AdjNode *p; 7 cout<<G.Vex[v].data<<"\t"; 8 visited[v]=true; 9 p=G.Vex[v].first; 10 while(p)//依次检查v的所有邻接点 11 { 12 w=p->v;//w为v的邻接点 13 if(!visited[w])//w未被访问 14 DFS_AL(G,w);//从w出发,递归深度优先遍历 15 p=p->next; 16 } 17 } 18 19 void DFS_AL(ALGragh G)//非连通图,基于邻接表的深度优先遍历 20 { 21 for(int i=0;i<G.vexnum;i++)//非连通图需要查漏点,检查未被访问的顶点 22 if(!visited[i])//i未被访问,以i为起点再次广度优先遍历 23 DFS_AL(G,i); 24 }
(二) BFS(广度优先搜索)
有点类似于树的层序遍历,即从一个选定的点出发,将与其直接相连的点都收入囊中,然后依次对这些点去收与其直接相连的点。重复到所有点都被访问然后结束。
基于上述,BFS可以通过一个队列来实现,其代码如下:
1 #define MaxVnum 100 //顶点数最大值 2 bool visited[MaxVnum]; //访问标志数组,其初值为"false" 3 void BFS_AM(AMGragh G,int v)//基于邻接矩阵的广度优先遍历 4 { 5 int u,w; 6 queue<int>Q; //创建一个普通队列(先进先出),里面存放int类型 7 cout<<G.Vex[v]<<"\t"; 8 visited[v]=true; 9 Q.push(v); //源点v入队 10 while(!Q.empty()) //如果队列不空 11 { 12 u=Q.front();//取出队头元素赋值给u 13 Q.pop(); //队头元素出队 14 for(w=0;w<G.vexnum;w++)//依次检查u的所有邻接点 15 { 16 if(G.Edge[u][w]&&!visited[w])//u、w邻接而且w未被访问 17 { 18 cout<<G.Vex[w]<<"\t"; 19 visited[w]=true; 20 Q.push(w); 21 } 22 } 23 } 24 }
1 #define MaxVnum 100;//顶点数最大值 2 bool visited[MaxVnum];//访问标志数组,其初值为"false" 3 void BFS_AL(ALGragh G,int v)//基于邻接表的广度优先遍历 4 { 5 int u,w; 6 AdjNode *p; 7 queue<int>Q; //创建一个普通队列(先进先出),里面存放int类型 8 cout<<G.Vex[v].data<<"\t"; 9 visited[v]=true; 10 Q.push(v); //源点v入队 11 while(!Q.empty()) //如果队列不空 12 { 13 u=Q.front();//取出队头元素赋值给u 14 Q.pop(); //队头元素出队 15 p=G.Vex[u].first; 16 while(p)//依次检查u的所有邻接点 17 { 18 w=p->v;//w为u的邻接点 19 if(!visited[w])//w未被访问 20 { 21 cout<<G.Vex[w].data<<"\t"; 22 visited[w]=true; 23 Q.push(w); 24 } 25 p=p->next; 26 } 27 } 28 } 29 30 void BFS_AL(ALGragh G)//非连通图,基于邻接表的广度优先遍历 31 { 32 for(int i=0;i<G.vexnum;i++)//非连通图需要查漏点,检查未被访问的顶点 33 if(!visited[i])//i未被访问,以i为起点再次广度优先遍历 34 BFS_AL(G,i); 35 }
遍历方式 |
存储结构 |
空间复杂度 |
时间复杂度 |
DFS |
AM |
O(V) |
O(V2) |
AL |
O(V+E) |
||
BFS |
AM |
O(V2) |
|
AL |
O(V+E) |
(三) 最短路径【Dijkstra算法】
- 单源最短路径问题:给定起点(源点),求到任意终点的最短路径
- 多源最短路径问题:起点(源点)不确定,求任意两个顶点的最短路径
- 对有权图而言,即是找权重和最小的路径
- 对无权图而言,即是找边最少的路径
Dijkstra算法(注:不适用于存在负值圈的图)有点类似于DFS算法,从源点开始将顶点一个一个往集合S里收(原则:在候选集合V-S里寻找离源点最近的点),保证从源点到该顶点“i”的当前最短路径是确定的,然后更新与“i”直接相连的点的最短路径。
1 const int INF=1e7; // 无穷大10000000 2 int dist[MaxVnum],p[MaxVnum];//最短距离和前驱数组 3 bool flag[MaxVnum]; //如果s[i]等于true,说明顶点i已经加入到集合S;否则顶点i属于集合V-S 4 5 void Dijkstra(AMGragh G,int u) 6 { 7 for(int i=0;i<G.vexnum;i++) 8 { 9 dist[i]=G.Edge[u][i]; //初始化源点u到其他各个顶点的最短路径长度 10 flag[i]=false; 11 if(dist[i]==INF) 12 p[i]=-1; //源点u到该顶点的路径长度为无穷大,说明顶点i与源点u不相邻 13 else 14 p[i]=u; //说明顶点i与源点u相邻,设置顶点i的前驱p[i]=u 15 } 16 dist[u]=0; 17 flag[u]=true; //初始时,集合S中只有一个元素:源点u 18 for(int i=0;i<G.vexnum;i++) 19 { 20 int temp=INF,t=u; 21 for(int j=0;j<G.vexnum;j++) //在集合V-S中寻找距离源点u最近的顶点t 22 if(!flag[j]&&dist[j]<temp) 23 { 24 t=j; 25 temp=dist[j]; 26 } 27 if(t==u) return ; //找不到t,跳出循环 28 flag[t]=true; //否则,将t加入集合 29 for(int j=0;j<G.vexnum;j++)//更新与t相邻接的顶点到源点u的距离 30 if(!flag[j]&&G.Edge[t][j]<INF) 31 if(dist[j]>(dist[t]+G.Edge[t][j])) 32 { 33 dist[j]=dist[t]+G.Edge[t][j]; 34 p[j]=t; 35 } 36 } 37 }
(四) 最小生成树【生成树:所有顶点均由边连接在一起,但不存在回路的图】
- 生成树的顶点数=图的顶点数
- 生成树是图的极小连通子图
- n个顶点的连通图的生成树有n-1条边
- Prim算法:以点为对象,寻找与点相连的最短边来构成最小生成树。
1 const int INF=1e7; // 无穷大10000000 2 const int N=100; 3 bool s[N]; 4 int c[N][N],closest[N],lowcost[N]; 5 void Prim(int n, int u0, int c[N][N]) 6 { //顶点个数n、开始顶点u0、带权邻接矩阵C[n][n] 7 //如果s[i]=true,说明顶点i已加入最小生成树 8 //的顶点集合U;否则顶点i属于集合V-U 9 //将最后的相关的最小权值传递到数组lowcost 10 s[u0]=true; //初始时,集合中U只有一个元素,即顶点u0 11 int i,j; 12 for(i=1;i<=n;i++) 13 { 14 if(i!=u0) 15 { 16 lowcost[i]=c[u0][i]; 17 closest[i]=u0; 18 s[i]=false; 19 } 20 else 21 lowcost[i]=0; 22 } 23 for(i=1;i<=n;i++) //在集合中V-u中寻找距离集合U最近的顶点t 24 { 25 int temp=INF; 26 int t=u0; 27 for(j=1;j<=n;j++) 28 { 29 if((!s[j])&&(lowcost[j]<temp)) 30 { 31 t=j; 32 temp=lowcost[j]; 33 } 34 } 35 if(t==u0) 36 break; //找不到t,跳出循环 37 s[t]=true; //否则,讲t加入集合U 38 for(j=1;j<=n;j++) //更新lowcost和closest 39 { 40 if((!s[j])&&(c[t][j]<lowcost[j])) 41 { 42 lowcost[j]=c[t][j]; 43 closest[j]=t; 44 } 45 } 46 } 47 }
- Kruskal算法:以边为对象,不断加入新的不构成回路的最短边来构成最小生成树。