图基础全解(概念,存储,遍历,最小生成树,最短路径,拓扑排序)
一、 图
1.1 图的基础概念
1、图的定义
图G由顶点集V和边集E组成,记为G=(V,E),V不能为空,E可以为空,顶点的个数,也叫做图G的阶。
2、有向图
当E是有向边(弧)的有限集合时,图G为有向图,弧是顶点的有序对,记为<v,w>,v和w是顶点,弧从v(弧尾)指向w(弧头)
3、无向图
当E是无向边的有限集合时,图G为无向图。边(v,w),既可以从v到w,也可以从w到v
4、简单图(数据结构仅讨论简单图)与多重图相对
①不存在重复边
②不存在顶点到自身的边
5、完全图(任意两点之间均存在边)
对于无向图,存在n*(n-1)/2条边,称为无向完全图
对于有向图,存在n*(n-1)条弧,称为有向完全图
6、子图
图G1的顶点和边都包含于图G2,则称G1是G2的子图
7、连通,连通图和连通分量(无向图)
在无向图中,若从顶点v到顶点w有路径存在,则称v和w是连通的。
若图G中任意两个顶点都是连通的,则称图G为连通图,否则称为非连通图。
无向图中的极大连通子图称为连通分量
若一个图中有n个顶点,并且边数小于n-1,则此图必是非连通图。
8、强连通图,强连通分量(有向图)
有向图中,若从顶点v到w和从w到v都有路径,则称这两个顶点是强连通的,则称此图为强连通图。
有向图中的极大强连通子图称为有向图的强连通分量。
9、顶点的度
顶点的度,以该顶点为一个端点的边的数目
无向图,全部顶点的度的和等于边数的2倍,因为每条边和两个顶点相连
有向图,顶点的v的度分为出度+入度,入度是指向顶点v的弧的数目,出度是以顶点v为起点的弧的数目。有向图的全部顶点的入度和出度之和相等,并且等于边数。因为每个有向边都有一个起点和终点
10、边的权和网
图中每条边都可以标上具有某种含义的数值,称为该边的权值。这种边上带有权值的图为带全图,也称网
11、稠密图和稀疏图
两者是相对而言的,一般当图G满足 |E|<|V|log|V| ,可以视作稀疏图
12、路径,路径长度,回路
顶点v到顶点p之间的一条路径是指顶点序列,当然关联的边也可以理解为路径的构成要素。路径上边的数目称为路径长度。第一个顶点和最后一个顶点相同的路径称为回路或环。
若一个图有n个顶点,并且有大于n-1条边,则此图一定有环。
13、简单路径,简单回路
在路径序列中,顶点不重复出现的路径称为简单路径。除第一个顶点和最后一个顶点外,其他顶点不重复出现的回路称为简单回路
1.2 图的存储与遍历
1.2.1 邻接矩阵
矩阵A如下:
A
=
(
0
1
0
1
1
0
1
0
0
1
0
1
1
0
1
0
)
A=\begin{gathered} \begin{pmatrix} 0&1&0& 1 \\ 1&0&1& 0\\ 0&1&0& 1\\ 1&0&1& 0 \end{pmatrix} \end{gathered}
A=⎝⎜⎜⎛0101101001011010⎠⎟⎟⎞
A[i][j]=1,代表顶点 i 与顶点 j 之间有路径,<i , j>是E中的一条边,反之则为0
无向图的邻接矩阵一般为对称矩阵,第i行非0元素的个数,正好是第i个顶点的度
有向带权图的邻接矩阵
A
=
(
∞
5
∞
3
∞
∞
8
∞
4
3
∞
6
8
∞
9
∞
)
A=\begin{gathered} \begin{pmatrix} ∞&5&∞& 3\\ ∞&∞&8&∞\\ 4&3&∞& 6\\ 8&∞&9& ∞ \end{pmatrix} \end{gathered}
A=⎝⎜⎜⎛∞∞485∞3∞∞8∞93∞6∞⎠⎟⎟⎞
对于有向带权图,邻接矩阵中,如果两个顶点i和j之间有边,则A[i][j]中存放相应权值,否则就是无穷大(当然在数组中存储时,不可能是真的存储无穷大,而是一个远大于所有权值的一个数)
第i行非0或非∞的元素的个数是该顶点 i 的出度,第i列非0或非∞的元素的个数是该顶点 i 的入度
图的邻接矩阵存储定义如下
#define MAX_SIZE 100
#define INF 999999
typedef struct{
int edge[MAX_SIZE][MAX_SIZE];
int vexnum,arcnum;
}MGraph;
1.2.1.1 图的邻接矩阵创建
//创建邻接矩阵
void CreateMGraph(MGraph *G){
int i,j,k,w;
printf("input the number of the vertex and the arc:");
scanf("%d,%d",&G->vexnum,&G->arcnum);
for(i = 0;i < G->vexnum;i++){
for(j = 0;j < G->vexnum;j++){
G->edge[i][j] = INF;
}
}
for(k = 0;k < G->arcnum;k++){
printf("input the arc:");
scanf("%d %d %d",&i,&j,&w);
G->edge[i][j] = w;
G->edge[j][i] = w;
}
}
1.2.1.2 图基于邻接矩阵的广度优先遍历
//BFS
void BFS(MGraph G,int v){
int visited[MAX_SIZE]; //标记数组
int queue[MAX_SIZE]; //队列
int front = 0,rear = 0; //队列头尾指针
int i;
for(i = 0;i < G.vexnum;i++){
visited[i] = 0;
}
int t = 1;
printf("%d ",v); //访问第一个顶点
visited[v] = 1; //标记为已访问
queue[rear++] = v;
while(front != rear){
v = queue[front++];
for(i = 0;i < G.vexnum;i++){
if(G.edge[v][i] != INF && visited[i] == 0){
printf("%d ",i);
visited[i] = 1;
queue[rear++] = i;
t++;
}
}
}
if(t < G.vexnum){ //如果t小于顶点数,说明图不连通
printf("the graph is not connected!");
}
}
1.2.1.3 图基于邻接矩阵的深度优先遍历
//DFS
int visited[MAX_SIZE];
int i;
for(i = 0;i < G.vexnum;i++){
visited[i] = 0;
}
void DFS(MGraph G,int v){
int i;
printf("%d ",v);
visited[v] = 1;
for(i = 0;i < G.vexnum;i++){
if(G.edge[v][i] != INF && visited[i] == 0){
DFS(G,i);
}
}
}
1.2.2 邻接表
当一个图为稀疏图时,使用邻接矩阵法显然要浪费大量的存储空间,而图的邻接表法结合了顺序存储和链式存储方法,大大减少了这种不必要的浪费。
所谓邻接表,是指对图G中的每个顶点v建立一个单链表,第i个单链表中的结点表示依附于顶点v的边(对于有向图则是以顶点v为尾的弧),这个单链表就称为顶点v的边表(对于有向图则称为出边表)。
边表的头指针和顶点的数据信息采用顺序存储(称为顶点表),所以在邻接表中存在两种结点:顶点表结点和边表结点
顶点结点:
顶点域 ↓ 边表头指针 ↓
data | firstarc |
---|
边表结点
邻接点域 ↓ 指针域 ↓
adjvex | nextarc |
---|
#define MAX_SIZE 100
typedef struct Arcnode{
int adjvex;
struct Arcnode *next;
}Arcnode,*ArcnodePtr;
typedef struct Vnode{
int data;
ArcnodePtr firstarc;
}Vnode,AdjList[MAX_SIZE];
typedef struct{
AdjList vertices;
int vexnum,arcnum;
}ALGraph;
①图的邻接表不是唯一的,因为临边的次序可以改变
②无向图G的邻接表存储空间为O(|V|+2|E|),每条边出现两次
有向图G的邻接表存储空间为O(|V|+|E|)
③对于稀疏图,邻接表可以极大的节省空间
3、十字链表(有机会再补)
4、邻接多重表(有机会再补)
1.2.2.1 图的邻接表的创建
//创建邻接表
void CreateALGraph(ALGraph *G){
int i,j,k;
ArcnodePtr p;
printf("input the number of the vertex and the arc:");
scanf("%d,%d",&G->vexnum,&G->arcnum);
for(i = 0;i < G->vexnum;i++){
printf("input the data of the vertex:");
scanf("%d",&G->vertices[i].data);
G->vertices[i].firstarc = NULL;
}
for(k = 0;k < G->arcnum;k++){
printf("input the arc:");
scanf("%d %d",&i,&j);
p = (ArcnodePtr)malloc(sizeof(Arcnode));
p->adjvex = j;
p->next = G->vertices[i].firstarc;
G->vertices[i].firstarc = p;
p = (ArcnodePtr)malloc(sizeof(Arcnode));
p->adjvex = i;
p->next = G->vertices[j].firstarc;
G->vertices[j].firstarc = p;
}
}
1.2.2.2 图基于邻接表的广度优先遍历
//BFS
void BFS(ALGraph G,int v){
int visited[MAX_SIZE]; //标记数组
int queue[MAX_SIZE]; //队列
int front = 0,rear = 0; //队列头尾指针
int i;
for(i = 0;i < G.vexnum;i++){
visited[i] = 0;
}
int t = 1;
printf("%d ",v); //访问第一个顶点
visited[v] = 1; //标记为已访问
queue[rear++] = v;
while(front != rear){
v = queue[front++];
ArcnodePtr p = G.vertices[v].firstarc;
while(p != NULL){
if(visited[p->adjvex] == 0){
printf("%d ",p->adjvex);
visited[p->adjvex] = 1;
queue[rear++] = p->adjvex;
}
p = p->next;
}
}
}
1.2.2.3 图基于邻接表的深度优先遍历
//DFS
int visited[MAX_SIZE];
int i;
for(i = 0;i < G.vexnum;i++){
visited[i] = 0;
}
void DFS(ALGraph G,int v){
printf("%d ",v);
visited[v] = 1;
ArcnodePtr p = G.vertices[v].firstarc;
while(p != NULL){
if(visited[p->adjvex] == 0){
DFS(G,p->adjvex);
}
p = p->next;
}
}
1.2.3 图的遍历(DFS与BFS)
图的遍历是指从图中的某一顶点出发,按照某种搜索方法沿着图中的边对图中的所有顶点访
问一次且仅访问一次。注意到树是一种特殊的图,所以树的遍历实际上也可视为一种特殊的图的
遍历。图的遍历算法是求解图的连通性问题、拓扑排序和求关键路径等算法的基础。
1.2.3.1 广度优先搜索(BFS)
类似于二叉树的层次遍历,需要用到队列进行辅助,算法的执行过程大概概括如下
①访问起始顶点v,将v入队,并标记已经访问
②当队列不为空时,循环执行:出队,一次检查出队顶点的所有邻接顶点,访问没有被访问过的邻接顶点并将其入队
③当队列为空时,跳出循环,广度优先搜索完成
BFS性能:
邻接表情况下,n个顶点均需入队一次,最坏的情况下,空间复杂度O(|V|),每个顶点均需搜索一次,每条边至少访问一次,所以总时间复杂度O(|V|+|E|)
邻接矩阵情况下,n个顶点均需入队一次,最坏的情况下,空间复杂度O(|V|),时间复杂度O(|V|2)
1.2.3.2 深度优先搜索(DFS)
DFS性能分析:
递归算法,需要借助空间栈,空间复杂度O(|V|);
使用邻接矩阵时的时间复杂度O(|V|2)
使用邻接表的时间复杂度O(|V|+|E|)
1.3 最小生成树
一个连通图的生成树,包含图的所有顶点,并且只含尽可能少的边。对于生成树来说。砍去一条边,则会使生成树变成非连通图,增加一条边,则会形成图中的一条回路
所有生成树权值之和最小的那棵称为最小生成树
①当图中权值互不相等时,最小生成树唯一;若无向连通图G的边数比顶点数少1,即G本身是一棵树的时候,最小生成树是G本身
②虽然最小生成树不唯一,但权值之和唯一,是最小的
③最小生成树的边数为顶点数减1
1.3.1 普里姆算法(prim)
执行过程如下
1)将V0到其他顶点的所有边当做后选边
2)重复以下步骤n-1遍,使得其他n-1个顶点被并入到树中
①从候选边中挑出权值最小的边输出,并将另一端相邻的顶点v并入树中
②检查所有剩余顶点vi,如果(v,vi)的权值小于lowcost[vi],则用(v,vi)的权值更新lowcost[vi];
//普利姆算法
void Prim(MGraph G){
int lowcost[MAX_SIZE]; //最小权值
int adjvex[MAX_SIZE]; //最小权值对应的顶点
int i,j,k,min;
int sum = 0;
adjvex[0] = 0;
lowcost[0] = 0;
for(i = 1;i < G.vexnum;i++){
lowcost[i] = G.edge[0][i];
adjvex[i] = 0;
}
for(i = 1;i < G.vexnum;i++){
min = INF;
j = 1;k = 0;
while(j < G.vexnum){
if(lowcost[j] != 0 && lowcost[j] < min){
min = lowcost[j];
k = j;
}
j++;
}
printf("(%d,%d)",adjvex[k],k);
sum += G.edge[adjvex[k]][k];
lowcost[k] = 0;
for(j = 1;j < G.vexnum;j++){
if(lowcost[j] != 0 && G.edge[k][j] < lowcost[j]){
lowcost[j] = G.edge[k][j];
adjvex[j] = k;
}
}
}
printf("the sum of the weight is %d",sum);
}
Prim时间复杂度分析,在邻接矩阵存储结构下,为O(n2)
1.3.2 克鲁斯卡尔算法(Kruskal)
执行过程:
1)对所有边的按照权值大小进行排序,从最小边进行扫描,并检测并入当前边是否构成回路,如不构成回路,将该边并入生成树
2)检查是否产生回路,用到并查集,检查该边的两个顶点是否属于同一棵树,即是否具有相同的根
//克鲁斯卡尔算法
typedef struct{ //边集数组
int begin;
int end;
int weight;
}Edge;
void swap(Edge *a,Edge *b){ //交换函数
Edge temp = *a;
*a = *b;
*b = temp;
}
void sort(Edge edges[],int n){ //冒泡排序
int i,j;
for(i = 0;i < n;i++){
for(j = i+1;j < n;j++){
if(edges[i].weight > edges[j].weight){
swap(&edges[i],&edges[j]);
}
}
}
}
int Find(int *parent,int f){ //查找连通分量
while(parent[f] > 0){
f = parent[f];
}
return f;
}
void Kruskal(MGraph G){ //克鲁斯卡尔算法
int parent[MAX_SIZE];
int i,j,n,sum=0;
Edge edges[MAX_SIZE];
n = 0;
for(i = 0;i < G.vexnum;i++){ //构造边集数组
for(j = i+1;j < G.vexnum;j++){
if(G.edge[i][j] != INF){
edges[n].begin = i;
edges[n].end = j;
edges[n].weight = G.edge[i][j];
n++;
}
}
}
sort(edges,n); //对边集数组排序
for(i = 0;i < G.vexnum;i++){ //初始化连通分量
parent[i] = 0;
}
for(i = 0;i < n;i++){ //循环每一条边
int a = Find(parent,edges[i].begin);
int b = Find(parent,edges[i].end);
if(a != b){
parent[a] = b;
printf("(%d,%d)",edges[i].begin,edges[i].end);
sum += edges[i].weight;
}
}
printf("the sum of the weight is %d",sum);
}
1.4 最短路径
1.4.1 迪杰斯特拉算法(dijkstra)(单源最短路径)
引入三个数组:
dist[vi]:表示当前已找到的从v0到每个终点vi的最短路径的长度。初始化为,v0到vi有边,则dist[vi]为权值,否则为∞(一个远大于各边权值的数)
path[vi]:保存从v0到vi最短路径上的vi的前一个顶点。初始化为,v0到vi有边,path[vi]=v0,否则为-1;
set[vi]:标记数组,0表示vi没有并入最短路径,1表示已经并入。初始化为,set[v0]=1,其余为0;
执行过程:
1)从当前dist[ ]选出最小值,假设为vk,将set[vk]=1,表示当前新并入的顶点为vk;
2)循环扫描图中顶点,检测是否并入,如果vj没有,则比较dist[vj]与dist[vk]+w,w为<vk,vj>的权值。意思是比较旧的最短路径与经过vk的新的最短路径的到达vj哪个小,如果新的小,则更新dist[vj],且path[vj]=vk;
3)循环执行n-1次(n为图中顶点个数),即可得到v0到其他所有顶点的最短路径
#define MAX_SIZE 100
#define INF 999999
typedef struct{
int edge[MAX_SIZE][MAX_SIZE];
int vexnum,arcnum;
}MGraph;
//创建邻接矩阵
void CreateMGraph(MGraph *G){
int i,j,k,w;
printf("input the number of the vertex and the arc:");
scanf("%d %d",&G->vexnum,&G->arcnum);
for(i = 0;i < G->vexnum;i++){
for(j = 0;j < G->vexnum;j++){
G->edge[i][j] = INF;
}
}
for(k = 0;k < G->arcnum;k++){
printf("input the arc:");
scanf("%d %d %d",&i,&j,&w);
G->edge[i][j] = w;
}
}
void Dijkstra(MGraph *g,int v,int dist[],int path[])
{
int set[MAX_SIZE];
int min,k;
for(int i=0;i<g->vexnum;i++)
{
dist[i]=g->edge[v][i]; //初始化dist[ ]数组
set[i]=0; //初始化set数组
if(g->edge[v][i]<INF) //初始化path数组
path[i]=v;
else
path[i]=-1;
}
set[v]=1; //标记已经并入最短路径
path[v]=-1; //没有前驱
for(int i=0;i<g->vexnum;i++)
{
min = INF;
for(int j=0;j<g->vexnum;j++)
{
if(set[j]==0&&dist[j]<min) //找出侯选边(邻接边)中权值最小的
{
min = dist[j];
k=j;
}
}
set[k]=1; //并入最短路径
for(int j=0;j<g->vexnum;j++)
{
if(set[j]==0&&dist[k]+g->edge[k][j]<dist[j]) //如果k作为中间顶点
{ //是否产生新的最短路径
dist[j]=dist[k]+g->edge[k][j]; //如果有,更新dist数组,path数组
path[j]=k;
}
}
}
}
void print(MGraph *g,int path[],int dist[])
{
for(int i=1;i<g->vexnum;i++) //循环打印0到其他所有顶点的最短路径
{
int j=i;
int stack[MAX_SIZE],top=-1; //利用栈进行顺序打印
while(path[j]!=-1)
{
stack[++top]=j;
j=path[j];
}
stack[++top]=j;
while(top!=0)
{
printf("%d-->",stack[top--]);
}
printf("%d\t\t",stack[top--]);
printf("dist=%d",dist[i]);
printf("\n");
}
}
int main()
{
int dist[MAX_SIZE],path[MAX_SIZE];
MGraph *g = (MGraph *)malloc(sizeof(MGraph));
CreateMGraph(g);
Dijkstra(g,0,dist,path);
print(g,path,dist);
}
运行截图:
1.4.2 弗洛伊德(Floyd)(一对顶点间的最短路径)
执行过程:引进矩阵A和矩阵Path
1)将图的邻接矩阵赋值给A,将矩阵Path中的元素全部设置为-1;
2)以顶点k为中间顶点,k取0~n-1,对图中所有的顶点对(i,j)进行如下检测
如果A[i][j]>A[i][k]+A[k][j],则更新A[i][j]的值,将Path[i][j]设置为k
#define MAX_SIZE 100
#define INF 999999
typedef struct{
int edge[MAX_SIZE][MAX_SIZE];
int vexnum,arcnum;
}MGraph;
//创建邻接矩阵
void CreateMGraph(MGraph *G){
int i,j,k,w;
printf("input the number of the vertex and the arc:");
scanf("%d %d",&G->vexnum,&G->arcnum);
for(i = 0;i < G->vexnum;i++){
for(j = 0;j < G->vexnum;j++){
G->edge[i][j] = INF;
}
}
for(k = 0;k < G->arcnum;k++){
printf("input the arc:");
scanf("%d %d %d",&i,&j,&w);
G->edge[i][j] = w;
}
}
//弗洛伊德算法
void Floyd(MGraph *g,int dist[][MAX_SIZE],int path[][MAX_SIZE])
{
int i,j,k;
for(i=0;i<g->vexnum;i++)
{
for(j=0;j<g->vexnum;j++)
{
dist[i][j]=g->edge[i][j];
path[i][j]=-1;
}
}
for(k=0;k<g->vexnum;k++)
{
for(i=0;i<g->vexnum;i++)
{
for(j=0;j<g->vexnum;j++)
{
if(dist[i][k]+dist[k][j]<dist[i][j])
{
dist[i][j]=dist[i][k]+dist[k][j];
path[i][j]=k;
}
}
}
}
}
//打印路径
void print(int path[][MAX_SIZE],int i,int j)
{
if(path[i][j]==-1)
{
printf("%d->%d",i,j);
}
else
{
print(path,i,path[i][j]);
printf("->%d",j);
}
}
int main()
{
MGraph g;
int dist[MAX_SIZE][MAX_SIZE];
int path[MAX_SIZE][MAX_SIZE];
CreateMGraph(&g);
Floyd(&g,dist,path);
int i,j;
for(i=0;i<g.vexnum;i++)
{
for(j=0;j<g.vexnum;j++)
{
printf("%d ",dist[i][j]);
}
printf("\n");
}
for(i=0;i<g.vexnum;i++)
{
for(j=0;j<g.vexnum;j++)
{
print(path,i,j);
printf("\n");
}
}
return 0;
}
1.5 拓扑排序
拓扑排序不唯一,当过程中存在多个入度为0的点时,取决于邻接表的输入顺序
#include<iostream>
#define MaxNum 10
using namespace std;
typedef struct ArcNode
{
int adjvex;
struct ArcNode *nextarc;
int info;
}ArcNode;
typedef struct VNode
{
int data;
int count;
ArcNode *firstarc;
}VNode,AdjList[MaxNum];
typedef struct
{
AdjList vextices;
int vexnum,arcnum;
}ALGraph;
void CreatALGraph(ALGraph *g)
{
int v1,v2;
int l,m;
ArcNode *p;
cin>>g->vexnum>>g->arcnum; //顶点数,边数
for(int i=0;i<g->vexnum;i++)
{
g->vextices[i].data=i;
cin>>g->vextices[i].count; //输入顶点的入度
g->vextices[i].firstarc=NULL;
}
for(int i=0;i<g->arcnum;i++)
{
cin>>v1>>v2; //先输入弧尾顶点,再输入弧头顶点
p = new ArcNode();
p->adjvex = v2;
p->nextarc = g->vextices[v1].firstarc; //头插法建立邻接表
g->vextices[v1].firstarc = p;
}
}
int TopSort(ALGraph *g)
{
int stack[MaxNum],top=-1;
int k,j,n=0;
ArcNode *p;
for(int i=0;i<g->vexnum;i++)
{
if(g->vextices[i].count==0) //查找入度为0的点入栈
stack[++top]=i;
}
while(top!=-1)
{
k=stack[top--];
n++; //统计当前顶点
cout<<k<<" ";
p=new ArcNode();
p=g->vextices[k].firstarc; //查找邻边
while(p!=NULL)
{
j=p->adjvex; //将临边的指向的顶点的入度减1
(g->vextices[j].count)--;
if(g->vextices[j].count==0) //为0则入栈
stack[++top]=j;
p=p->nextarc; //去下一条临边
}
}
if(n==g->vexnum) //计算器等于顶点数,则排序成功
return 1;
else
return 0;
}
int main()
{
ALGraph *g = new ALGraph();
CreatALGraph(g);
int k=TopSort(g);
cout<<endl<<k;
}
结束语
写了两整天,算是自己复习的一个过程,查漏补缺,尽量在写的过程中,将自己曾经有过的困惑表达清楚,就这样。