文章目录
图的概念
图的定义
图G是由顶点集合V(G)和边集合E(G)构成。
如果代表边的顶点对是无序的,则称G为无向图,用圆括号序偶表示无向边。
有序图用尖括号表示。
图的基本术语
1、端点和邻接点
无向图:若存在一条边(i,j),顶点i、j为端点,它们互为邻接点
有向图:存在一条边<i,j>,顶点i为起始端点,j为终止端点,它们互为邻接点。
2、顶点的度、入度和出度
无向图:以顶点i为端点的边数称为该顶点的度。
有向图:以顶点i为终点的入边的数目称为该顶点的入度。以顶点i为始点的出边的数目称为该顶点的出度。一个顶点的入度和出度和称为该顶点的度。
3、完全图
无向图:每两个顶点之间都存在一条边,称为完全无向图,包含有n(n-1)/2条边
有向图:每两个顶点间存在方向相反的两条边。包含有n(n-1)条边
4、稠密图、稀疏图
5、子图
设有两个图G=(V,E)和G’=(V’,E’),若V’是V的子集,E’是E的子集,则G’是G的子图。
6、路径和路径长度
路径长度是指一条路径上经过边的数目。
若一条路径上除开始点和结束点可以相同外,其余顶点均不相同,则称此路径为简单路径。
7、回路和环
若一条路径上的开始点与结束点为同一个顶点,即为回路或环。
开始点与结束点相同的简单路径被称为简单回路或简单环。
8、连通、连通图和连通分量
无向图:若从顶点i到顶点j有路径,则称顶点i和j是连通。
连通图:若图中任意两个顶点都连通。
连通分量:无向图G中的极大连通子图。任何连通图的连通分量只有一个,即本身,而非连通图有多个连通分量。
9、强连通图和强连通分量
有向图:若从顶点i到顶点j有路径,则称从顶点i到j是连通的
若图G中任意两个顶点i和j都连通,即从顶点i到j和从顶点j到i都存在路径,则图G是强连通图。
强连通分量:有向图G中极大强连通子图。强连通图只有一个强连通分量,即本身。非强连通图可能有多个。
在一个非强连通中找强连通分量:
1、在图中找有向环
2、扩展该有向环:如果某个顶点到该环中任一顶点有路径,并且该环中任一顶点到这个顶点也有路径,则加入这个顶点。
10、权和网
边上有权的图称为带权图,也称作网。
图的存储结构
邻接矩阵
邻接矩阵是表示顶点之间相邻关系的矩阵。设G=(V,E)是具有n个顶点的图。
G的邻接矩阵A是n阶方阵:
若G是无向图,则:A[i][j]=1:若(i,j)属于E(G)
若G是有向图,则:A[i][j]=1:若<i,j>属于E(G)
若G是带权无向图,则:A[i][j]=wij:若i不等于j且(i,j)属于E(G) 0:i=j 无穷:其他
若G是带权有向图,则:A[i][j]=wij:若i不等于j且<i,j>属于E(G) 0:i=j 无穷:其他
主要特点:
1、一个图的邻接矩阵表示唯一
2、特别适合稠密图的存储
存储空间为O(n^2)
图的邻接矩阵存储类型如下:
#define MAXV <最大顶点个数>
typedef struct
{ int no;//顶点编号
InfoType info;
}VertexType;
typedef struct
{ int edges[MAXV][MAXV];
int n,e;//顶点数,边数
VertexType vexs[MAXV];//存放顶点信息
}MGraph;
邻接表存储方法
1、对图中每个顶点i建立一个单链表,将顶点i的所有邻接点链起来。
2、每个单链表上添加一个表头节点。并将所有表头节点构成一个数组,下标为i的元素表示顶点i的表头节点。
特点:
1、邻接表表示不唯一
2、特别适合于稀疏图存储。邻接表的存储空间为O(n+e)
图的邻接表存储类型定义如下:
typedef struct ANode
{ int adjvex;//该边的终点编号
struct ANode *nextarc;//指向下一条边的指针
InfoType info;
}ArcNode;
typedef struct VNode
{ Vertex data;
ArcNode *firstarc;//指向第一条边
}VNode;
typedef struct
{ VNode adjlist[MAXV];//邻接表
int n,e;
}ALGraph;
图的遍历
深度优先遍历
算法设计思路:深度优先遍历体现后进先出:栈或递归。
邻接表DFS算法:
void DFS(ALGraph *G,int v)
{ ArcNode *p,int w;
visited[v]=1;
printf("%d ",v);//输出被访问顶点的编号
p=G->adjlist[v].firstarc;
while(p!=NULL)
{ w=p->adjvex;
if(visited[w]!=1)
DFS(G,w);
p=p->nextarc;
}
}
该算法复杂度为O(n+e)。
距离初始顶点越远越优先访问。
广度优先遍历
广度优先搜索遍历体现先进先出特点,用队列实现。
void BFS(ALGraph *G,int v)
{ ArcNode *p,int w,i;
int queue[MAXV],front=0,rear=0;//定义循环队列
int visited[MAXV];
for(i=0;i<G->n;i++)
visited[i]=0;
printf("%2d",v);
visited[v]=1;
rear=(rear+1)%MAXV;
queue[rear]=v;//v进队
while(front!=rear)
{ front=(front+1)%MAXV;
w=queue[front];//出队
p=G->adjlist[w].firstarc;
while(p!=NULL)
{ if(visited[p->adjvex]==0)
{ printf("%2d",p->adjvex);
visited[p->adjvex]=1;
rear=(rear+1)%MAXV;
queue[rear]=p->adjvex;
}
p=p->nextarc;//找下一个邻接顶点
}
}
}
距离初始点越近越优先访问。
非连通图的遍历
无向非连通图调用一次DFS或BFS只能访问到初始点所在连通分量中所有顶点,不可能访问到其他连通分量重的顶点。只要分别遍历每个连通分量,才能够访问到所有顶点。
void DFS1(ALGraph *G)
{ int i;
for(i=0;i<G->n;i++)
if(visited[i]==0)
BFS(G,i);
}
调用BFS次数恰好等于连通分量个数
生成树和最小生成树
生成树概念:一个连通图的生成树是一个极小连通子图,它含有图中全部n个顶点和构成一棵树的n-1条边。
可通过遍历方法得到生成树。
最小生成树概念:对于带权连通图G,权值之和最小的生成树称为图的最小生成树。
连通图:仅需调用遍历过程一次,从图中任一顶点出发,便可以遍历图中各顶点。
非连通图:需多次调用遍历过程。
Prim算法
构造性算法,构造最小生成树。
思路:
1、初始化U={v},v到其他顶点的所有边为侯选边
2、重复以下步骤n-1次,使得其他n-1个顶点被加入到U中:
(1)从侯选边中挑选权值最小的边输出,设该边在V-U中顶点是k,将k加入U中
(2)考察当前V-U中所有顶点j,修改侯选边:若(j,k)的权值小于原来和顶点k关联的侯选边,则用(k,j)取代后者作为侯选边。
Prim算法如下:
#define INF 32767 //INF表示无穷
void Prim(MGraph g,int v)
{ int lowcost[MAXV];
int min;
int closest[MAXV],i,j,k;
for(i=0;i<g.n;i++)
{ lowcost[i]=g.edges[v][i];
closest[i]=v;
}
for(i=1;i<g.n;i++)//输出n-1条边
{ min=INF;
for(j=0;j<g.n;j++) //在V-U中找出离U最近的顶点k
if(lowcost[j]!=0&&lowcost[j]<min)
{ min=lowcost[j];
k=j;//k记录最近顶点编号
}
printf("边(%d,%d)权为:%d\n",closest[k],k,min);
lowcost[k]=0;//标记k已经加入U
for(j=0;j<g.n;j++)//修改数组lowcost和closest
if(lowcost[j]!=0&&g.edges[k][j]<lowcost[j])
{ lowcost[j]=g.edges[k][j];
closest[j]=k;
}
}
}
局部最优+调整=全局最优
有两重for循环所以时间复杂度为O(n^2)
Kruskal算法
求带权无向图的最小生成树的构造性算法,按权值的递增次序选择合适的边来构造最小生成树。
算法过程:
1、置U的初值等于V,TE的初值为空寂
2、将G中边按权值从小到大顺序依次选取:
若选取的边未使生成树T形成回路,则加入TE
否则舍弃,直到TE中包含n-1条边为止
用数组E存放图G中所有边,类型如下:
typedef struct
{ int u;//边的起始顶点
int v;//边的终止顶点
int w;//权值
}Edge;
算法如下:
void Kruskal(MGraph g)
{ int i,j,u1,v1,sn1,sn2,k;
int vset[MAXV];
Edge E[MaxSize];//存放所有边
k=0;
for(i=0;i<g.n;i++)
for(j=0;j<g.n;j++)
if(g.edges[i][j]!=0&&g.edges[i][j]!=INF)
{ E[k].u=i;E[k].v=j;E[k].w=g.edges[i][j];
k++;
}
InsertSort(E,g.e);//直接插入排序对E数组按权值递增排序
for(i=0;i<g.n;i++)//初始化辅助数组
vset[i]=i;
k=1;j=0;
while(k<g.n)
{ u1=E[j].u;v1=E[j].v;
sn1=vset[u1];sn2=vset[v1];
if(sn1!=sn2)
{ printf("(%d,%d):%d\n",u1,v1,E[j].w);
k++;
for(i=0;i<g.n;i++)
if(vset[i]==sn2)//集合编号为sn2的改为sn1
}
j++;//扫描下一条边
}
}
上述不是最优,改进:堆排序、并查集
时间复杂度为O(elog2e)
最短路径
Dijkstra算法
设G=(V,E)是一个带权有向图,把图中顶点集合V分成两组:
1、已求出最短路径顶点集合
2、为求出最短路径顶点集合
void Dijkstra(MGraph g,int v)
{ int dist[MAXV],path[MAXV];
int s[MAXV];
int mindis,i,j,u;
for(i=0;i<g.n;i++)
{ dist[i]=g.edges[v][i];//距离初始化
s[i]=0;
if(g.edges[v][i]<INF)
path[i]=v;
else path[i]=-1;
}
s[v]=1;
for(i=0;i<g.n;i++)
{ mindis=INF;
for(j=0;j<g.n;j++)
if(s[j]==0&&dist[j]<mindis)
{ u=j;
mindis=dist[j];
}
s[u]=1;//顶点u加入S中
for(j=0;j<g.n;j++)//修改不在s中的顶点的距离
if(s[j]==0)
if(s[j]==0)
if(g.edges[u][j]<INF&&dist[u]+g.edges[u][j]<dist[j])
{ dist[j]=dist[u]+g.edges[u][j];
path[j]=u;
}
}
Dispath(dist.path,s,g.n,v);
}
时间复杂度:O(n^2)
特点:1、按顶点进入S的先后顺序,最短路径长度越来越长
2、一个顶点一旦进入S后,其最短路径长度不再改变
Floyd算法
迭代思路:采用邻接矩阵存储。设置一个二维数组A用于存放当前顶点之间最短路径长度。分量A[i][j]表示当前顶点i->j的最短路径长度。
初始时,A-1[i][j]=g.edges[i][j]
考虑从i->j的最短路径经过编号为k顶点的情况:
Ak[i,j]=MIN{Ak-1[i,j],Ak-1[i,k]+Ak-1[k,j]}
用二维数组A存储最短路径长度:
Ak[i][j]表示考虑顶点0-k后得出的i->j的最短路径长度。
An-1[i][j]表示最终的i->j的最短路径长度。
用二维数组path存放最短路径:
pathk[i][j]表示考虑顶点0-k后得出的i->j的最短路径
pathn-1[i][j]表示最终i->j的最短路径
算法如下:
void Floyd(MatGraph g)//求每对顶点之间的最短路径
{ int A[MAXVEX][MAXVEX];
int path[MAXVEX][MAXVEX];
int i,j,k;
for(i=0;i<g.n;i++)
for(j=0;j<g.n;j++)
{ A[i][j]=g.edges[i][j];
if(i!=j&&g.edges[i][j]<INF)
path[i][j]=i;//i和j顶点之间有一条边时
else path[i][j]=-1;
}
for(k=0;k<g.n;k++)//求Ak[i][j]
{ for(i=0;i<g.n;i++)
for(j=0;j<g.n;j++)
if(A[i][j]>A[i][k]+A[k][j])
{ A[i][j]=A[i][k]+A[k][j];
path[i][j]=path[k][j];
}
}
}
拓扑排序
步骤:
1、从有向图中选择一个没有前驱的顶点并且输出
2、从图中删去该顶点,并且删去从该顶点发出的全部有向边
3、重复上述两步,直到剩余的图中不再存在没有前驱的顶点
拓扑排序算法设计:
邻接表定义中VNode类型修改如下:
typedef struct
{ Vertex data;//顶点信息
int count;//存放顶点入度,用于查找入度为0的顶点
ArcNode *firstarc;//指向第一条边
}VNode;
void TopSort(VNode adj[],int n)
{ int i,j;int St[MAXV],top=-1;
ArcNode *p;
for(i=0;i<n;i++)
if(adj[i].count==0)
{top++;St[top]=i;}
while(top>-1)
{ i=St[top];top--;
printf("%d",i)
p=adj[i].firstarc;
while(p!=NULL)
{ j=p->adjvex;
adj[j].count--;
if(adj[j].count==0)
{top++;St[top]=j;}
p=p->nextarc;
}
}
}
```