图的存储
邻接矩阵存储
存储实现
#define MaxVertexNum 100 //结点数
typedef char VertexType;
typedef int ElemType;
typedef struct
{
VertexType Vex[MaxVertexNum];//顶点表
ElemType Edge[MaxVertexNum][MaxVertexNum];//邻接矩阵,边表
int vexnum,arcnum;//结点数 边数
}MGraph;
无向图的度
有向图的的度
邻接矩阵法,求结点的度/入度/出度的时间复杂度为O(|V|)
性能分析
空间复杂度为(n^2),只和结点个数有关,与边数无关,因此适合于稠密图
邻接矩阵法的性质
邻接表
存储
无向图
有向图
//边
typedef struct ArcNode
{
int adjvex; //边指向哪个结点
struct ArcNode *next;//指向下一个弧的指针
//InfoType info //边的权值
}ArcNode;
//顶点表
typedef struct VNode
{
VertexType data; //顶点信息
ArcNode *first; //第一条边
}VNode,AdjList[MaxVertexNum];
//邻接表
typedef struct
{
AdjList vertices;
int vexnum,arcnum;
}ALGraph;
无向图的度
遍历结点的边链表,有几个边结点,度就是几
有向图的度
出度,遍历结点的边链表。
入度,遍历结点数组的每个结点的边链表,找到指向某个结点的边。
图的邻接表表示方式不唯一
性能分析
无向图:
每个边结点实际上被存了两份,所以空间复杂度为O(|V|+2|E|)
有向图:
每个边结点实际上存一份,所以空间复杂度为O(|V|+|E|)
十字链表存储有向图
一图看懂十字链表
十字链表法只用于存储有向图
注意:弧头和弧尾的对应,如A->B 弧头是B弧尾是A
性能分析
有向图:空间复杂度O(|V|+|E|)
解决了邻接矩阵空间复杂度高,邻接表不方便找入边不方便的问题
邻接多重表存储无向图
一图看懂邻接多重表
性能分析:
无向图:空间复杂度O(|V|+|E|)
删除边,删除节点都很方便
解决了邻接矩阵空间复杂度高,邻接表删除边与节点不方便的问题。
图的操作
判断图G中是否存在边<x,y>或(x,y)
邻接矩阵:O(1)
bool Adjacent(MGraph G,x,y)
{
if(G.Edge[x][y] == 1)
return true;
return false;
}
邻接表:O(|E|)
bool Adjacent(AdjList G,int x,int y)
{
ArcNode *p = G[x].first;
while(p)
{
if(p->adjvex == y)
{
return true;
}
p=p->next;
}
return false;
}
列出图G中与结点x邻接的边
邻接矩阵:O(V)
void Neighbors(MGraph G,int x)
{
for(int i = 0;i < G.vexnum; i++)
{
if(G.Edge[x][i] == 1)
{
cout<<i<<endl;
}
}
}
邻接表:
无向图O(V)(O(1)~O(V))
有向图:找入边O(E) 找出边O(V)
void Neighbors(AdjList G,int x)
{
ArcNode *p = G[x].first;
while(p)
{
cout<<p->adjvex<<endl;
p=p->next;
}
}
在图G中插入顶点x
邻接矩阵:直接在顶点集中插入新节点,O(1)。
邻接表:直接插入新节点,O(1)
从图中删除顶点x
邻接矩阵:直接将要删除的结点对应的行与列全部设为0,O(V)
邻接表:
无向图,删除一个结点,并释放其边链表的内存,O(E)(O(1)~O(E))
有向图,删除入边时间复杂度为O(E),删除出边为O(V)(O(1)~O(V))
若无向边(x,y)或有向边<x,y>不存在,则添加这条边
邻接矩阵:O(1)
邻接表:使用头插法可以做到O(1),尾插法是O(V)
若无向边(x,y)或有向边<x,y>存在,则删除这条边
邻接矩阵:O(1)
邻接表:使用头插法可以做到O(1),尾插法是O(V)
求图G中顶点x的第一个临界点,若有则返回顶点号,若没有或图中不存在x,则返回-1
邻接矩阵:O(V)(O(1)~O(V))
邻接表:无向图O(1)
有向图 出边O(1),入边O(E)(O(1)~O(E))
假设图G中顶点y是顶点x的一个邻接点,返回除y之外顶点x的下一个邻接点顶点号,若y是x的最后一个邻接点,则返回-1
邻接矩阵:O(V)
邻接表:O(1)
获取图G中边(x,y)或<x,y>对应的权值
邻接矩阵O(1)
邻接表O(V) (O(1)~O(V))
设置图G中边(x,y)或<x,y>对应的权值。
图的遍历
广度优先搜索
辅助队列容量至少为V.
对于基于邻接矩阵实现的广度优先搜索:
时间复杂度为O(V^2),对于每个顶点,都需要遍历V个顶点
对于基于邻接表实现的广度优先搜索:
访问V个顶点需要O(V)的时间,查找各个顶点的邻接点共需要O(E)的时间,总的时间复杂度为O(V+E)
void BFS(Graph G,int v)//从顶底v开始除法,广度优先遍历G
{
visit(v);
vis[v]=true;
Enqueue(Q,v);
while(!isEmpty(Q))
{
DeQueue(Q,v);
for(w = FirstNeighbor(G,v);w>=0;w = NextNeighbor(G,v,w))
{
if(!vis[w])
{
visit(w);
visit[w] = true;
EnQueue(Q,w);
}
}
}
}
对于非连通图,上面的代码需要做一些改进:
bool vis[MAX_VERTEX_NUM];
void BFSTraverse(Graph G)
{
memset(vis,false,sizeof(vis));
InitQueue(Q);
for(int i = 0;i<G.vexnum;i++)
{
if(!vis[i])
BFS(G,i);
}
}
void BFS(Graph G,int v)//从顶底v开始除法,广度优先遍历G
{
visit(v);
vis[v]=true;
Enqueue(Q,v);
while(!isEmpty(Q))
{
DeQueue(Q,v);
for(w = FirstNeighbor(G,v);w>=0;w = NextNeighbor(G,v,w))
{
if(!vis[w])
{
visit(w);
visit[w] = true;
EnQueue(Q,w);
}
}
}
}
深度优先搜索
bool vis[MAX_VERTEX_NUM];
void DFS(Graph G,int v)
{
visit(v);
vis[v]=true;
for(w=FirstNeighbor(G,V);w>=0;w=NextNeighbor(G,v,w))
{
if(!vis[w])
{
DFS(G,w);
}
}
}
对于非连通图,上面的代码需要做一些改进:
bool vis[MAX_VERTEX_NUM];
void BFSTraverse(Graph G)
{
memset(vis,false,sizeof(vis));
InitQueue(Q);
for(int i = 0;i<G.vexnum;i++)
{
if(!vis[i])
BFS(G,i);
}
}
void DFS(Graph G,int v)
{
visit(v);
vis[v]=true;
for(w=FirstNeighbor(G,V);w>=0;w=NextNeighbor(G,v,w))
{
if(!vis[w])
{
DFS(G,w);
}
}
}
对于基于邻接矩阵实现的深度优先搜索:
时间复杂度为O(V^2),对于每个顶点,都需要遍历V-1个顶点
对于基于邻接表实现的深度优先搜索:
访问V个顶点需要O(V)的时间,查找各个顶点的邻接点共需要O(E)的时间,总的时间复杂度为O(V+E)
最短路问题
BFS处理单源无权图最短路
邻接矩阵:
时间复杂度为O(V^2)
邻接表
时间复杂度为O(V+E)
void BFS_MIN_Distance(Graph G,int u)
{
//d[i]表示从u到i结点的最短路径
for(int i = 1;i<G.vexnum;i++)
{
d[i]=INF;//初始化路径长度为无限大
path[i]=-1;//当前结点在最短路中的前驱结点
}
d[u]=0;
vis[u]=true;
EnQueue(Q,u);
while(!isEmpty(Q))
{
DeQueue(Q,u);
for(w=FirstNeighbor(G,u);w>=0;w=NextNeighbor(G,u,v))
{
if(!vis[w])
{
d[w]=d[u]+1;
path[w]=u;
vis[w]=true;
EnQueue(Q,w);
}
}
}
}
Dijkstra处理单源有权图最短路
时间复杂度为O(V^2)
Dijkstra也可以处理无权图,但不能处理带负权边的图
为什么无法处理带负权边的图呢?
Dijkstra的原理是每次选取一个到起始点最近的、还未被选过的点,把这个点到源点的距离定义为源点到该点的最短距离,但是如果存在负权边,这个最短距离就不一定是最短距离了。如下图所示
① 2是到1最近的点,标记2,更新1到4的距离为10
② 3是未标记点中到1最近的点,更新1到2的距离为4.
这里可以看到1到2的最短距离被更新了,因此1到4的最短距离也应该被更新,但是由于2被标记了,永远也无法再选到2去更新1到4的距离了,所以Dijkstra就失效了
int dis[MAX_VERTX];//存储每个结点到指定结点的最短路
int mp[MAX_VERTX][MAX_VERTX];//邻接矩阵存储图
int vis[MAX_VERTX];//标记数组,标记结点是否已经找到最短路
void Dijkstra(int s)
{
int i,j,u,minn;
memset(vis,0,sizeof(vis));
//初始化s到其他点的距离
for(i=1; i<=n; i++)
{
dis[i]=mp[s][i];
}
vis[s]=1;//第一个点已经找到最短路了(也就是它自己本身,长度为0)
//找到s到其他n-1个点的最短路
for(i=1; i<n; i++)
{
minn=INF;
//找到一个离s最近的点,并且这个点之前没有被确定最短路
for(j=1; j<=n; j++)
{
if(!vis[j]&&dis[j]<minn)
{
u=j;
minn=dis[j];
}
}
if(minn==INF)
break;
//标记这个点,即s已经找到到这个点的最短路
vis[u]=1;
//更新s到其他点的距离
for(j=1; j<=n; j++)
{
if(!vis[j]&&dis[j]>dis[u]+mp[u][j])
{
dis[j]=dis[u]+mp[u][j];
}
}
}
}
Floyd处理多源最短路
思路:对于两点之间的最短路,可以通过改变中转点或增加中转点的数量来找到更近的最短路。
Floyd实际上是枚举中转点,之后枚举图中每条路径之间的距离,不断地通过中转点来更新每条路径之间的最短路实现的。
实现:三层循环,第一层枚举中转点,第二层枚举路径起点,第三层枚举路径终点。设最外层循环执行的次数为i,则中转点可选取的数量为[1,i]。
时间复杂度为O(V^3)
可以处理带负权边的图,但不能处理带负权环的图。负权环的图没有最短路。
void Floyd(Graph G)
{
//以k作为中转
for(int k = 0;k<G.vexnum;k++)
{
//起始点为i
for(int i = 0;i<G.vexnum;i++)
{
//终点为j
for(int j = 0;j<G.vexnum;j++)
{
if(G.E[i][j]<G.E[i][k]+G.E[k][j])
{
G.E[i][j]=G.E[i][k]+G.E[k][j];
path[i][j]=k;//从i到j的中转点为k
}
}
}
}
}
拓扑排序
可以用栈、队列甚至数组来实现
邻接矩阵:
O(V^2)
邻接表:
O(V+E)
基于邻接表的拓扑排序:
#define MaxVertexNum 100
typedef struct ArcNode
{
int adjvex;
struct ArcNode *next;
}ArcNode;
typedef struct VNode
{
VertexType data;
ArcNode *fisrt;
}
typedef struct
{
AdjList vertices;
int vexnum,int arcnum;
}Graph;
int indegree[MaxVertexNum];
bool TopologicalSort(Graph G)
{
InitStack(S);
for(int i = 0;i<G.vexnum;i++)
{
if(indegree[i]==0)
Push(S,i); //将所有入度为0的顶点入栈
}
int count = 0;//记录已经输出的顶点数
while(!isEmpty(S))
{
Pop(S,i);
cout<<i<<endl;//输出顶点i
count++;
for(ArcNode p=G.vertices[i].firstarc;p;p=p->next)
{
//将所有i指向的顶点的入度减一,并将度减为零的点压入栈中
v=p->adjvex;
if(!(--indegree[v]))
{
Push(S,v);
}
}
}
if(count<G.vexnum)
{
return false;
}
else
return true;
}
其他代码
基于邻接表的DFS非递归遍历
void DFS(Graph G,int x)
{
for(int i = 0; i<G.vernum; i++)
{
if(!visited[i])
{
InitStack(S);
visited[x]=true;
Push(S,x);
while(!isEmpty(S))
{
x = Pop(S);//从栈中取出一个顶点
visit(x);//访问x
//把该顶点相连的、还未入过栈的顶点全部加入栈中。
for(w=FirstNeighbor(G,x);w>=0;w=NextNeighbor(G,x,w))
{
if(!visited[w])
{
visited[w]=true;
Push(S,w);
}
}
}
}
}
}