本文是笔者的一个数据结构实验,仅供初学者参考。
目录
一、图的储存结构
对于图的储存,我们可以用邻接矩阵或者邻接表的形式进行储存,邻接矩阵容易实现图的操作,如:求某顶点的度、判断顶点之间是否有边、找顶点的邻接点等等,但其空间效率为.。对稀疏图而言尤其浪费空间;反之,邻接表的空间效率高,为,容易找到顶点的邻接点,但是判断两顶点间是否有边或
弧,需搜索两结点对应的单链表,没有邻接矩阵方便。
因此,对于无向图的储存,我们有如下两种储存方式:
//邻接矩阵形式存储
typedef struct graph{
int v;//顶点数
int e;//边数
int data[MAXVertexnum];//顶点数据
int weight[MAXVertexnum][MAXVertexnum];//权重
}graph,*Mgraph;
typedef struct edge{
int v1;
int v2;
int weight;//v1到v2,权重为weight
}*Edge;
//邻接表形式储存
typedef struct edgenode{
int weight;
struct edgenode *next;//指向边结点
int local;//边指向的顶点
}edgenode;
typedef struct vnode{
int data;
edgenode *firstvertnex;//指向边
}vnode,AdjList[MAXVertexnum];
typedef struct Adjgraph{
int v;
int e;
AdjList List;
}Adjgraph;
二、图的创建
2.1 邻接矩阵形式
要想创建出邻接矩阵,需要知道顶点和边的个数以及各个边的权重信息,于是,我们可以根据边的信息不断添加向图里添加边从而实现图的创建。对此,需要一个初始化函数和边添加函数,如下所示:
//初始化函数,把所有权重赋0
void Init_graph(Mgraph g,int vertnex)
{
g->v = vertnex;
g->e = 0;
for(int i=0;i<vertnex;i++)
{
for(int j=0;j<vertnex;j++)
{
g->weight[i][j]=0;
}
}
}
//边添加函数
void Insert_edge(Mgraph g,Edge e)
{
g->weight[e->v1][e->v2] = e->weight;
g->weight[e->v2][e->v1] = e->weight;
}
//创建邻接矩阵
Mgraph Create_graph()
{
int vertnex;
printf("请输入图的顶点数:\n");
scanf("%d",&vertnex);
Mgraph g;
g = (Mgraph)malloc(sizeof(graph));
Init_graph(g,vertnex);
printf("请输入边数:\n");
int n;
scanf("%d",&n);
if(n != 0)
{
printf("请输入顶点和权重(空格分隔):\n");
for(int i=0;i<n;i++)
{
Edge e;
e = (Edge)malloc(sizeof(edge));
scanf("%d %d %d",&e->v1,&e->v2,&e->weight);
Insert_edge(g,e);
}
printf("请输入顶点数据:(如果没有,输入-1)\n");//这个模块可以忽略
for(int j=0;j<vertnex;j++)
{
scanf("%d",&g->data[j]);
if(g->data[0] == -1)
{
break;
}
}
}
return g;
}
2.2 邻接表形式
要创建邻接表,也可采用顶点和边输入的形式,每输入一条边的信息,就对边关联的两个顶点链表添加对应顶点信息以及边权重(无向图),如下所示:
//初始化邻接表
void Init_Adjgraph(Adjgraph *g)
{
printf("请分别输入顶点数和边数:\n");
scanf("%d %d",&g->v,&g->e);
printf("请输入顶点数据:\n");
for(int i=0;i<g->v;i++)
{
scanf("%d",&g->List[i].data);
g->List[i].firstvertnex = NULL;//初始化收入顶点数据并把指针域置空
}
getchar();
}
void Create_Adjgraph(Adjgraph *g)//用相当于入栈的头插法对边链表添加
{
printf("一共有%d条边,请输入每条边的两个顶点以及边权重:\n",g->e);
int w,v,weight;
for(int i=0;i<g->e;i++)
{
scanf("%d %d %d",&w,&v,&weight);
edgenode *p = (edgenode *)malloc(sizeof(edgenode));
p->local = v;
p->weight = weight;
p->next = g->List[w].firstvertnex;
g->List[w].firstvertnex = p;
edgenode *q = (edgenode *)malloc(sizeof(edgenode));
q->local = w;
q->weight = weight;
q->next = g->List[v].firstvertnex;
g->List[v].firstvertnex = q;
}
}
三、图的遍历
3.1 深度优先遍历(DFS)
本文主要基于非函数递归方式来实现深度优先遍历,而一提起递归,自然就会想到栈,故用栈来实现递归操作,下面将先讲深度优先遍历的原理:
深度遍历是从一个给定的顶点开始,“摸索”出一条路径,然后从路径的末端开始依次往前退回来摸索其他路径,有点类似于树的先序遍历。比如下面这张图:
从顶点2出发,先有一条路径2->1->3->5,这些元素全部入栈,然后发现从5来看没有未访问过的顶点与之相邻(顶点2,3与其相邻但被访问过),故5出栈,同理3也出栈,此时栈顶元素为1,对1遍历发现还有顶点4与其相邻且未被访问,故4入栈,此时栈顶元素为4,再对4分析同理有6入栈,此时栈内元素为2,1,4,6,然后对6来看没有未被访问过的邻接顶点,故6出栈,同样的可以得到4,1,2也分别出栈,此时栈为空,遍历结束,最终有DFS遍历结果为:2->1->3->5->4->6。
由上面分析不难看出,我们需要一个栈和辅助访问数组来完成深度优先遍历操作,其中,要先找到一条路径,让路径的顶点入栈来对栈进行初始化,然后进行判断,如果栈顶元素没有未访问过的邻接顶点就出栈,反之就将新的顶点入栈,直到栈为空时遍历结束,代码如下所示:
void dfs_graph(Mgraph g,int i)//深度优先遍历,从顶点i开始
{
printf("\n下面是深度优先遍历结果:\n");
struct stack{//临时栈,用来储存顶点
int vec[100];
int top;
};
int visited[MAXVertexnum]={0};//数组访问标记,1表示已访问
struct stack q;
q.top = 0;
q.vec[q.top++] = i;
for(int m=0;m<g->v;i=(i+1)%g->v,m++)
{
if(!visited[i])
{
visited[i]=1;
printf("%d->",i);
for(int j=0; ;j++)
{
if(g->weight[i][j] != 0 && !visited[j])
{
visited[j] = 1;
q.vec[q.top++] = j;
printf("%d->",j);
i=j;
j=0;//由J开始遍历下一个邻接点
}
if(j == g->v)//j遍历完不存在邻接点
{
if(q.top == 0)
break;
q.vec[q.top--] = 0;
i = q.vec[q.top];
j=0;//返回上一个邻接点
}
}
}
}
}
有点要注意的是,我在遍历中用到了两个for循环嵌套,提高了时间复杂度,这样做是因为内层的for循环是正常从顶点i出发的一个深度优先遍历,此时for里面没有结束条件而是用break退出循环,而外层的循环是为了防止出现非连通图的遍历漏元素的情况,想象这样一个情形:如果一个图是由两个连通片组成的,而遍历起点是从一个连通片中的某个顶点开始,那么由于不存在从这个连通片到另一个连通片的路径,那么就永远不会遍历到另一个连通片中,这时我们可以采取把每个顶点都执行一次得到最终结果。
3.2 广度优先遍历(BFS)
对于广度优先遍历,就类似与树的层序遍历了,先找起始顶点的所有邻接顶点,然后再找所有邻接顶点的邻接顶点,这样一层一层的往外遍历。
还以上面的图为例,由于广度优先遍历需要不断回退接着遍历,故可以使用队列来实现,从2出发,2先入队,与2邻接的顶点为1和5,分别入队,然后2出队,接着分析1,与1邻接的为3和4,分别入队然后1出队,3无邻接且未访问顶点直接出队,下面就是4,6与其邻接入队,4出队,最后6再出队,队为空遍历结束,遍历结果为:2->1->5->3->4->6。
由上面分析,我们可以知道,需要一个队列和辅助访问数组来实现广度优先遍历,从起始点开始分析,将未访问过的邻接点分别入队并让起始点出队,然后不断分析队头元素,反复执行上述操作直至队列为空,当然,同上面深度优先遍历一样,为了防止多连通片的情况,在外面再嵌套一个循环,如下所示:
void bfs_graph(Mgraph g,int i)//广度优先遍历
{
printf("\n下面是广度优先遍历结果:\n");
int visited[MAXVertexnum]={0};
int queue[MAXVertexnum];//采用队列
int bottom = 0;
int t = 0;
for(int m=0;m<g->v;m++,i=(i+1)%g->v)
{
if(!visited[i])
{
printf("%d->",i);
visited[i] = 1;
queue[bottom++]=i;
for(int j=0; ;j++)
{
for(int k=0;k<g->v;k++)
{
if(g->weight[i][k] != 0 && !visited[k])
{
queue[bottom++] = k;
visited[k] = 1;
printf("%d->",k);
}
}
if(j == g->v)
{
if(t == g->v-1)
break;
i=queue[++t];//t+1来实现出队并取下一数据操作
j = 0;
}
}
}
}
}
四、最小生成树
在这里我采用的是Prim算法实现最小生成树的构造,输入为图,输出为最小生成树的边信息(关联顶点和权重)。首先要理解Prim算法的思想,即把图分为两部分,一部分是已找到最短边的顶点集合,记为红点集,另一部分是其余顶点集合,记为白点集;从起始顶点出发,利用贪心算法的思想,找到连接起始顶点和剩余顶点集合的最短边,将该边以及关联的顶点添加到红点集当中,不断重复上述操作即可,值得注意的是,每次更新红点集后就需要利用贪心算法找到白点集到红点集各个点最短边来实现边更新,示意图如下:
代码如下所示:
void Prim_mintree(Mgraph g)
{
printf("\n下面是最小生成树的边信息:\n");
int min,i,j,k;
int adjvex[MAXVertexnum];//相关顶点下标,表示第i个顶点前驱为adjvex[i]
int lowcost[MAXVertexnum];//保存权值,表示第i个顶点与前驱之间权值
lowcost[0]=0;
adjvex[0]=0;//初始化
for(i=1;i<g->v;i++)
{
if(g->weight[0][i] == 0)
{
lowcost[i] = INFINITY;
}
else
{
lowcost[i] = g->weight[0][i];
}
adjvex[i] = 0;//初始化都以0为前驱,并存入对应权重
}
for(i=1;i<g->v;i++)
{
min = INFINITY;
j = 1;
k = 0;
while(j<g->v)//找到和已有顶点集的最短的边
{
if(lowcost[j] != 0 && lowcost[j] < min)
{
min = lowcost[j];
k = j;//将当前最小下标储存到k中
}
j++;
}
printf("%d %d %d\n",adjvex[k],k,g->weight[adjvex[k]][k]);
lowcost[k] = 0;//清零表示该顶点已完成任务
for(j=1;j<g->v;j++)
{
if(lowcost[j] != 0 && g->weight[k][j] < lowcost[j] && g->weight[k][j] != 0)//确保不重复访问
{
lowcost[j] = g->weight[k][j];
adjvex[j] = k;
}
}
}
}
五、最短路径
本题要处理的是单源最短路径问题,故采用Dijstra算法,我们需要三个数组来实现,s[n]记录顶点是否已被确定为最短距离,d[n]来表示最短路径的值,p[n]表示最短路径的前驱,具体实现方式可以根据下面这张图(有向图)来确定:
从起始点出发,对d[n]和p[n]进行初始化赋值,若存在从起始点到其他点的路径,则直接将边权重存入d[n]中并将相应p[n]修改为起始点表示该路径是从起始点出发得来的,然后选择d[n]中的最小值的边所关联的顶点修改s[n]并基于新入红点集的点修改d[n]和p[n],这样一步步执行下去就能得到起始点到各个点的最短路径长度及路径(如果有的话),具体代码如下所示:
int Find_mindist(int *dist,int *s,int v)//找到最小的路径并返回顶点
{
int i;
int loc;
int min = INFINITY+1;
for(i=0;i<v;i++)
{
if(s[i] == 0)
{
if(dist[i]<min)
{
min = dist[i];
loc = i;
}
}
}
return loc;
}
void Dijkstra(Mgraph g,int start)
{
int i,j,num;
int min;
int dist[MAXVertexnum];//最短路径长度数组
int path[MAXVertexnum];//最短路径数组
int s[MAXVertexnum];//表示是否处理过
for(i=0;i<g->v;i++)//初始化数组
{
if(g->weight[start][i] == 0)
{
dist[i] = INFINITY;
}
else
{
dist[i]=g->weight[start][i];
}
if(dist[i]!=INFINITY)
{
path[i]=start;
}
else
{
path[i]=-1;
}
}
for(i=0;i<g->v;i++)
{
s[i] = 0;
}
s[start] = 1;
num = 1;
while(num<g->v)
{
min = Find_mindist(dist,s,g->v);
s[min] = 1;//把最小的路径点添加进s后对dist进行更新
for(i=0;i<g->v;i++)
{
if(s[i] == 0 && (dist[i]>dist[min]+g->weight[min][i]) && g->weight[min][i] != 0)
{
dist[i] = dist[min]+g->weight[min][i];
path[i] = min;
}
}
num++;
}
print_path(dist,path,g,start);
}
六、代码执行
整个实验代码如下所示:
#include<stdio.h>
#include<stdlib.h>
#define MAXVertexnum 100
#define INFINITY 65525
typedef struct graph{//邻接矩阵形式存储
int v;//顶点数
int e;//边数
int data[MAXVertexnum];//顶点数据
int weight[MAXVertexnum][MAXVertexnum];//权重
}graph,*Mgraph;
typedef struct edge{
int v1;
int v2;
int weight;//v1到v2,权重为weight
}*Edge;
typedef struct edgenode{
int weight;
struct edgenode *next;//指向边结点
int local;//边指向的顶点
}edgenode;
typedef struct vnode{
int data;
edgenode *firstvertnex;//指向边
}vnode,AdjList[MAXVertexnum];
typedef struct Adjgraph{
int v;
int e;
AdjList List;
}Adjgraph;
//函数声明模块
void Init_Adjgraph(Adjgraph *g);
void Create_Adjgraph(Adjgraph *g);
void Print_Adjgraph(Adjgraph *g);
void Init_graph(Mgraph g,int vertnex);
void Insert_edge(Mgraph g,Edge e);
Mgraph Create_graph();
void Print_graph(Mgraph g);
void dfs_graph(Mgraph g,int i);
void bfs_graph(Mgraph g,int i);
void Prim_mintree(Mgraph g);
int Find_mindist(int *dist,int *s,int v);
void Dijkstra(Mgraph g,int start);
void print_path(int *dist,int *path,Mgraph g,int start);
void Init_Adjgraph(Adjgraph *g)
{
printf("请分别输入顶点数和边数:\n");
scanf("%d %d",&g->v,&g->e);
printf("请输入顶点数据:\n");
for(int i=0;i<g->v;i++)
{
scanf("%d",&g->List[i].data);
g->List[i].firstvertnex = NULL;//初始化收入顶点数据并把指针域置空
}
getchar();
}
void Create_Adjgraph(Adjgraph *g)//用相当于入栈的头插法对边链表添加
{
printf("一共有%d条边,请输入每条边的两个顶点以及边权重:\n",g->e);
int w,v,weight;
for(int i=0;i<g->e;i++)
{
scanf("%d %d %d",&w,&v,&weight);
edgenode *p = (edgenode *)malloc(sizeof(edgenode));
p->local = v;
p->weight = weight;
p->next = g->List[w].firstvertnex;
g->List[w].firstvertnex = p;
edgenode *q = (edgenode *)malloc(sizeof(edgenode));
q->local = w;
q->weight = weight;
q->next = g->List[v].firstvertnex;
g->List[v].firstvertnex = q;
}
}
void Print_Adjgraph(Adjgraph *g)
{
printf("下面为图的邻接表示意:\n");
for(int i=0;i<g->v;i++)
{
printf("%-3d->",i);
while(g->List[i].firstvertnex != NULL)
{
printf("%-3d---weight:%-3d->",g->List[i].firstvertnex->local,g->List[i].firstvertnex->weight);
g->List[i].firstvertnex = g->List[i].firstvertnex->next;
}
if(g->List[i].firstvertnex == NULL)
printf("!\n");
}
}
void Init_graph(Mgraph g,int vertnex)
{
g->v = vertnex;
g->e = 0;
for(int i=0;i<vertnex;i++)
{
for(int j=0;j<vertnex;j++)
{
g->weight[i][j]=0;
}
}
}
void Insert_edge(Mgraph g,Edge e)
{
g->weight[e->v1][e->v2] = e->weight;
g->weight[e->v2][e->v1] = e->weight;
}
Mgraph Create_graph()
{
int vertnex;
printf("请输入图的顶点数:\n");
scanf("%d",&vertnex);
Mgraph g;
g = (Mgraph)malloc(sizeof(graph));
Init_graph(g,vertnex);
printf("请输入边数:\n");
int n;
scanf("%d",&n);
if(n != 0)
{
printf("请输入顶点和权重(空格分隔):\n");
for(int i=0;i<n;i++)
{
Edge e;
e = (Edge)malloc(sizeof(edge));
scanf("%d %d %d",&e->v1,&e->v2,&e->weight);
Insert_edge(g,e);
}
printf("请输入顶点数据:(如果没有,输入-1)\n");
for(int j=0;j<vertnex;j++)
{
scanf("%d",&g->data[j]);
if(g->data[0] == -1)
{
break;
}
}
}
return g;
}
void Print_graph(Mgraph g)
{
printf("下面是邻接矩阵:\n");
for(int i=0;i<g->v;i++)
{
for(int j=0;j<g->v;j++)
{
printf("%5d",g->weight[i][j]);
}
printf("\n");
}
if(g->data[0] != -1)
{
printf("下面是顶点数据:\n");
for(int k=0;k<g->v;k++)
{
printf("%3d",g->data[k]);
}
printf("\n");
}
}
void dfs_graph(Mgraph g,int i)//深度优先遍历,从顶点i开始
{
printf("\n下面是深度优先遍历结果:\n");
struct stack{//临时栈,用来储存顶点
int vec[100];
int top;
};
int visited[MAXVertexnum]={0};//数组访问标记,1表示已访问
struct stack q;
q.top = 0;
q.vec[q.top++] = i;
for(int m=0;m<g->v;i=(i+1)%g->v,m++)
{
if(!visited[i])
{
visited[i]=1;
printf("%d->",i);
for(int j=0; ;j++)
{
if(g->weight[i][j] != 0 && !visited[j])
{
visited[j] = 1;
q.vec[q.top++] = j;
printf("%d->",j);
i=j;
j=0;//由J开始遍历下一个邻接点
}
if(j == g->v)//j遍历完不存在邻接点
{
if(q.top == 0)
break;
q.vec[q.top--] = 0;
i = q.vec[q.top];
j=0;//返回上一个邻接点
}
}
}
}
}
void bfs_graph(Mgraph g,int i)//广度优先遍历
{
printf("\n下面是广度优先遍历结果:\n");
int visited[MAXVertexnum]={0};
int queue[MAXVertexnum];//采用队列
int bottom = 0;
int t = 0;
for(int m=0;m<g->v;m++,i=(i+1)%g->v)
{
if(!visited[i])
{
printf("%d->",i);
visited[i] = 1;
queue[bottom++]=i;
for(int j=0; ;j++)
{
for(int k=0;k<g->v;k++)
{
if(g->weight[i][k] != 0 && !visited[k])
{
queue[bottom++] = k;
visited[k] = 1;
printf("%d->",k);
}
}
if(j == g->v)
{
if(t == g->v-1)
break;
i=queue[++t];//t+1来实现出队并取下一数据操作
j = 0;
}
}
}
}
}
void Prim_mintree(Mgraph g)
{
printf("\n下面是最小生成树的边信息:\n");
int min,i,j,k;
int adjvex[MAXVertexnum];//相关顶点下标,表示第i个顶点前驱为adjvex[i]
int lowcost[MAXVertexnum];//保存权值,表示第i个顶点与前驱之间权值
lowcost[0]=0;
adjvex[0]=0;//初始化
for(i=1;i<g->v;i++)
{
if(g->weight[0][i] == 0)
{
lowcost[i] = INFINITY;
}
else
{
lowcost[i] = g->weight[0][i];
}
adjvex[i] = 0;//初始化都以0为前驱,并存入对应权重
}
for(i=1;i<g->v;i++)
{
min = INFINITY;
j = 1;
k = 0;
while(j<g->v)//找到和已有顶点集的最短的边
{
if(lowcost[j] != 0 && lowcost[j] < min)
{
min = lowcost[j];
k = j;//将当前最小下标储存到k中
}
j++;
}
printf("%d %d %d\n",adjvex[k],k,g->weight[adjvex[k]][k]);
lowcost[k] = 0;//清零表示该顶点已完成任务
for(j=1;j<g->v;j++)
{
if(lowcost[j] != 0 && g->weight[k][j] < lowcost[j] && g->weight[k][j] != 0)//确保不重复访问
{
lowcost[j] = g->weight[k][j];
adjvex[j] = k;
}
}
}
}
int Find_mindist(int *dist,int *s,int v)//找到最小的路径并返回顶点
{
int i;
int loc;
int min = INFINITY+1;
for(i=0;i<v;i++)
{
if(s[i] == 0)
{
if(dist[i]<min)
{
min = dist[i];
loc = i;
}
}
}
return loc;
}
void Dijkstra(Mgraph g,int start)
{
int i,j,num;
int min;
int dist[MAXVertexnum];//最短路径长度数组
int path[MAXVertexnum];//最短路径数组
int s[MAXVertexnum];//表示是否处理过
for(i=0;i<g->v;i++)//初始化数组
{
if(g->weight[start][i] == 0)
{
dist[i] = INFINITY;
}
else
{
dist[i]=g->weight[start][i];
}
if(dist[i]!=INFINITY)
{
path[i]=start;
}
else
{
path[i]=-1;
}
}
for(i=0;i<g->v;i++)
{
s[i] = 0;
}
s[start] = 1;
num = 1;
while(num<g->v)
{
min = Find_mindist(dist,s,g->v);
s[min] = 1;//把最小的路径点添加进s后对dist进行更新
for(i=0;i<g->v;i++)
{
if(s[i] == 0 && (dist[i]>dist[min]+g->weight[min][i]) && g->weight[min][i] != 0)
{
dist[i] = dist[min]+g->weight[min][i];
path[i] = min;
}
}
num++;
}
print_path(dist,path,g,start);
}
void print_path(int *dist,int *path,Mgraph g,int start)
{
int temp[MAXVertexnum];//临时数组存路径顶点
int loc;
int i,j,k;
printf("\n最短路径如下所示:\n");
for(i=0;i<g->v;i++)
{
loc = i;
j = 0;
while(loc != -1)
{
temp[j] = loc;
loc = path[loc];//更新为前驱顶点
j++;
}
if(j-1 == 0 && temp[j-1] == start)
{
printf("vertex:%d 起始点!\n",i);
}
else if(j-1 == 0 && temp[j-1] != start)
{
printf("vertex:%d 不可达!\n",i);
}
else
{
printf("vertex:%d dist:%d path:",i,dist[i]);
for(j=j-1;j>0;j--)
{
printf("%d->",temp[j]);
}
printf("%d\n",temp[j]);
}
}
}
int main()
{
int start;
int t=1;
int temp;
while(t)
{
printf("请输入,1为创建邻接矩阵,2为创建邻接表,3为退出。\n");
scanf("%d",&temp);
switch (temp) {
case 1:
{
Mgraph g = (Mgraph)malloc(sizeof(graph));
g = Create_graph();
Print_graph(g);
dfs_graph(g,0);
bfs_graph(g,0);
Prim_mintree(g);
printf("请输入最短路径起始点:\n");
scanf("%d",&start);
Dijkstra(g,start);
break;
}
case 2:
{
Adjgraph *g1 = (Adjgraph *)malloc(sizeof(Adjgraph));
Init_Adjgraph(g1);
Create_Adjgraph(g1);
Print_Adjgraph(g1);
break;
}
case 3:
t=0;
break;
default:
{
printf("无效输入!\n");
}
}
}
return 0;
}
运行实例如下: