数据结构(5)—— 图
写在前面
图这一节,考察最多的依然是算法的实现过程,而不是代码本身。但实现还是要实现的。
本文代码的实现参考了数据结构与算法系列 目录 - 如果天空不死 - 博客园 (cnblogs.com)的博客与王道书上的逻辑。
按照惯例,上一节:数据结构(4)—— 树与二叉树
图的存储结构以及遍历
注意点
这块内容比较少,就都写在一块了。图的类型主要是有向图与无向图,图的存储主要是邻接表与邻接矩阵,遍历主要是深度优先遍历与广度优先遍历。这里的实现没有太多要注意的点,主要就是按照王道书和我大二初学时的代码加以改造而成的。主要实现了无向图的邻接矩阵的广度优先与深度优先遍历,由于邻接表的遍历实现类似,就没有重复写了。
代码
无向图
/*
* @Description: 无向图的相关算法实现
* @version: 1.0
* @Author: Liuge
* @Date: 2021-07-21 20:53:36
*/
#include<bits/stdc++.h>
// 顶点类型,由用户自己定义
typedef char VertexType;
// 边上的权值类型,由用户自己定义
typedef int EdgeType;
// 最大顶点数,由用户定义
#define MAX_VEX 20
// 用int的最大值来代表无穷大
#define INFINITY 65535
// 访问数组,用来标识结点是否被访问过
int visited[MAX_VEX]={0};
// 用邻接矩阵法表示无向图
typedef struct{
// 顶点数组
VertexType vexs[MAX_VEX];
// 邻接矩阵
EdgeType arc[MAX_VEX][MAX_VEX];
// 图中当前定点数和边数
int vexNum, arcNum;
}Graph;
// *******************队列相关定义开始*******************
// 定义结点的结构
typedef struct LinkNode{
int data;
struct LinkNode *next;
}LinkNode;
// 定义链式队列的结构
typedef struct{
LinkNode *front;
LinkNode *rear;
}LinkQueue;
// 初始化
void initQueue(LinkQueue &Q){
// 建立头结点
Q.front = Q.rear = (LinkNode *)malloc(sizeof(LinkNode));
Q.front->next = NULL;
}
// 判队空
bool isEmpty(LinkQueue Q){
return Q.front == Q.rear;
}
// 入队
void enQueue(LinkQueue &Q,int x){
LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode));
s->data = x;
s->next = NULL;
Q.rear->next = s;
// 队尾指针移动到新的尾部
Q.rear = s;
}
// 出队
bool deQueue(LinkQueue &Q,int &x){
if(isEmpty(Q)){
return false;
}
LinkNode *p = Q.front->next;
x = p->data;
Q.front->next = p->next;
// 如果原队列只有一个结点,删除后变空
if(Q.rear == p){
Q.rear = Q.front;
}
free(p);
return true;
}
// *******************队列相关定义结束*******************
// 定位顶点v在顶点数组的下标位置,不存在返回-1
int locateVex(Graph g, VertexType v)
{
int i = 0;
for (i = 0; i < g.vexNum; i++)
{
if (g.vexs[i]==v)
break;
}
if (i > g.vexNum)
{
printf("结点不存在!");
return -1;
}
return i;
}
// 用邻接矩阵表示法,构造无向网(图)g
void createUDN(Graph &g)
{
int i, j;
EdgeType w;
printf("输入顶点数和边数:\n");
scanf("%d,%d", &(g.vexNum), &(g.arcNum));
// 输入顶点
for(i = 0; i < g.vexNum; i++)
{
printf("顶点%d:", i + 1);
g.vexs[i]=getchar();
while(g.vexs[i]=='\n'){
g.vexs[i]=getchar();
}
}
getchar();
//初始化邻接矩阵
for (i = 0; i < g.arcNum; i++)
{
for (j = 0; j < g.arcNum; j++)
{
g.arc[i][j] = INFINITY;
}
}
printf("输入边(vi, vj)上的顶点vi vj和权值w\n");
for (i = 0; i < g.arcNum; i++)
{
char v1, v2;
scanf("%c %c %d", &v1, &v2, &w);
getchar();
int m = locateVex(g, v1);
int n = locateVex(g, v2);
if (m == -1 || n == -1)
{
printf("结点不存在!");
return;
}
g.arc[m][n] = w;
#if 1
// 按照对称直接赋值
g.arc[n][m] = g.arc[m][n];
#endif
}
}
// 打印图
void printGraph(Graph g)
{
int i=0,j=0;
printf("领接矩阵表为:\n");
for (i = 0; i < g.vexNum; i++)
{
for (j = 0; j < g.vexNum; j++)
{
printf("%6d", g.arc[i][j]);
}
putchar('\n');
}
}
// 图的深度优先遍历算法
void DFS_AM(Graph G,int v){
printf("%c ",G.vexs[v]);
visited[v] = 1;
int w;
for(w = 0;w < G.vexNum;w++){
if((G.arc[v][w] != INFINITY)&&(!visited[w])){
DFS_AM(G,w);
}
}
}
// 广度优先遍历算法
void BFSTraverse(Graph g)
{
LinkQueue q;
int v;
for (v = 0; v < g.vexNum; v++)
visited[v] = 0;
initQueue(q);
for (v = 0; v < g.vexNum; v++)
// 第v个顶点尚未访问
if (!visited[v])
{
visited[v] = 1;
// 打印顶点,也可以是其他操作
printf("%c ", g.vexs[v]);
// 第v个顶点入队列
enQueue(q, v);
while (!isEmpty(q))
{
int i,j=0;
deQueue(q, i);
for (j = 0; j < g.vexNum; j++)
// 判断其他顶点若与当前顶点存在边且未访问过
if (g.arc[i][j] > 0 && g.arc[i][j] != INFINITY && !visited[j])
{
// j为i尚未访问过的邻接顶点
visited[j] = 1;
printf("%c ", g.vexs[j]);
enQueue(q, j);
}
}
}
}
// 主函数测试
int main(){
Graph G;
createUDN(G);
printGraph(G);
printf("深度优先遍历结果为:\n");
DFS_AM(G,0);
printf("\n");
printf("广度优先遍历结果为:\n");
BFSTraverse(G);
return 0;
}
有向图
/*
* @Description: 有向图的相关算法实现
* @version: 1.0
* @Author: Liuge
* @Date: 2021-07-21 21:15:19
*/
#include<bits/stdc++.h>
// 顶点类型,由用户自己定义
typedef char VertexType;
// 边上的权值类型,由用户自己定义
typedef int EdgeType;
// 最大顶点数,由用户定义
#define MAX_VEX 20
// 使用邻接表法表示有向图
// 边表结点
typedef struct EdgeNode
{
// 邻接点域,存储该顶点在顶点表中的下标
int adjvex;
// 网图权值
EdgeType weight;
// 链域,指向下一个邻接点
struct EdgeNode *next;
}EdgeNode;
// 顶点表结点
typedef struct VertexNode
{
// 顶点域,存储顶点信息
VertexType data;
// 边表头指针
EdgeNode *firstedge;
}VertexNode, AdjList[MAX_VEX];
typedef struct
{
AdjList adjList;
// 图中当前顶点数和弧数
int vexNum, arcNum;
}GraphList;
// 定位顶点v在顶表的下标位置,不存在返回-1
int locateVex(GraphList g, VertexType v)
{
int i;
for (i = 0; i < g.vexNum; i++)
{
if (v == g.adjList[i].data )
break;
}
if (i > g.vexNum)
{
printf("结点不存在!");
return -1;
}
return i;
}
// 用邻接表表示法,构造有向网g
void createGraph(GraphList &g)
{
int i, j;
EdgeType w;
EdgeNode *e, *f;
printf("输入顶点数和边数:\n");
scanf("%d,%d", &(g.vexNum), &(g.arcNum));
//输入顶点
for (i = 0; i < g.vexNum; i++)
{
printf("顶点%d:", i+1);
g.adjList[i].data = getchar();
g.adjList[i].firstedge = NULL;
while(g.adjList[i].data == '\n')
{
g.adjList[i].data = getchar();
}
}
getchar();
// 建立边表
printf("输入边(vi, vj)上的顶点vi vj和权值w\n");
for (i = 0; i < g.arcNum; i++)
{
char v1, v2;
scanf("%c %c %d", &v1, &v2, &w);
getchar();
int m = locateVex(g, v1);
int n = locateVex(g, v2);
if (m == -1 || n == -1)
{
printf("结点不存在!");
return;
}
//向内存申请空间,生成边表结点
e = (EdgeNode*)malloc(sizeof(EdgeNode));
if(e == NULL)
{
printf("内存空间申请失败!");
return;
}
// 邻接序号为n
e->adjvex = n;
// 权值
e->weight = w;
// 单链表的头插法
e->next = g.adjList[m].firstedge;
g.adjList[m].firstedge = e;
}
}
void printGrapth(GraphList g)
{
int i;
printf("邻接表为:\n");
for (i = 0; i < g.vexNum; i++)
{
printf("顶点%c ", g.adjList[i].data) ;
EdgeNode *e = g.adjList[i].firstedge;
while (e != NULL)
{
printf("―> <%c, %c> 权值为: %d ", g.adjList[i].data, g.adjList[e->adjvex].data, e->weight);
e = e->next;
}
putchar('\n');
}
}
// 主函数测试
int main(){
GraphList G;
createGraph(G);
printGrapth(G);
return 0;
}
最小生成树——Prim算法
注意点
书上在这块有两种算法,Prim和Kruskal算法。我这里随便挑选了一种来实现,另一种的实现就不再写了。Prim算法实现的重点是要遍历找到权值最小的边,然后加入到顶点集合中。具体实现看代码好了。
代码
/*
* @Description: Prim算法的实现
* @version: 1.0
* @Author: Liuge
* @Date: 2021-07-24 19:41:35
*/
#include<bits/stdc++.h>
// 顶点类型,由用户自己定义
typedef char VertexType;
// 边上的权值类型,由用户自己定义
typedef int EdgeType;
// 最大顶点数,由用户定义
#define MAX_VEX 20
// 用int的最大值来代表无穷大
#define INFINITY 65535
// 访问数组,用来标识结点是否被访问过
int visited[MAX_VEX]={0};
// 用邻接矩阵法表示无向图
typedef struct{
// 顶点数组
VertexType vexs[MAX_VEX];
// 邻接矩阵
EdgeType arc[MAX_VEX][MAX_VEX];
// 图中当前定点数和边数
int vexNum, arcNum;
}Graph;
// 定位顶点v在顶点数组的下标位置,不存在返回-1
int locateVex(Graph g, VertexType v)
{
int i = 0;
for (i = 0; i < g.vexNum; i++)
{
if (g.vexs[i]==v)
break;
}
if (i > g.vexNum)
{
printf("结点不存在!");
return -1;
}
return i;
}
// 用邻接矩阵表示法,构造无向网(图)g
void createUDN(Graph &g)
{
int i, j;
EdgeType w;
printf("输入顶点数和边数:\n");
scanf("%d,%d", &(g.vexNum), &(g.arcNum));
// 输入顶点
for(i = 0; i < g.vexNum; i++)
{
printf("顶点%d:", i + 1);
g.vexs[i]=getchar();
while(g.vexs[i]=='\n'){
g.vexs[i]=getchar();
}
}
getchar();
//初始化邻接矩阵
for (i = 0; i < g.arcNum; i++)
{
for (j = 0; j < g.arcNum; j++)
{
g.arc[i][j] = INFINITY;
}
}
printf("输入边(vi, vj)上的顶点vi vj和权值w\n");
for (i = 0; i < g.arcNum; i++)
{
char v1, v2;
scanf("%c %c %d", &v1, &v2, &w);
getchar();
int m = locateVex(g, v1);
int n = locateVex(g, v2);
if (m == -1 || n == -1)
{
printf("结点不存在!");
return;
}
g.arc[m][n] = w;
#if 1
// 按照对称直接赋值
g.arc[n][m] = g.arc[m][n];
#endif
}
}
// prim 算法生成最小生成树
void prim(Graph G, int start)
{
int min,i,j,k,m,n,sum;
// 最小生成树的索引,即prims数组的索引
int index=0;
// 存储最小生成树的数组
char prims[MAX_VEX];
// 顶点间边的权值
int weights[MAX_VEX];
// prim最小生成树中第一个数是"图中第start个顶点",因为是从start开始的。
prims[index++] = G.vexs[start];
// 将每个顶点的权值初始化为"第start个顶点"到"该顶点"的权值。
for (i = 0; i < G.vexNum; i++){
weights[i] = G.arc[start][i];
}
// 将第start个顶点的权值初始化为0。
weights[start] = 0;
for (i = 0; i < G.vexNum; i++)
{
// 由于从start开始的,因此不需要再对第start个顶点进行处理。
if(start == i){
continue;
}
j = 0;
k = 0;
min = INFINITY;
// 在未被加入到最小生成树的顶点中,找出权值最小的顶点。
while (j < G.vexNum)
{
// 若weights[j]=0,意味着"第j个节点已经被加入到了最小生成树中"
if (weights[j] != 0 && weights[j] < min)
{
min = weights[j];
k = j;
}
j++;
}
// 经过上面的处理后,在未被加入到最小生成树的顶点中,权值最小的顶点是第k个顶点。
// 将第k个顶点加入到最小生成树的结果数组中
prims[index++] = G.vexs[k];
// 将"第k个顶点的权值"标记为0 代表第k个顶点已经加入到了结果中
weights[k] = 0;
// 当第k个顶点被加入到最小生成树的结果数组中之后,更新其它顶点的权值。
for (j = 0 ; j < G.vexNum; j++)
{
// 当第j个节点没有被处理,并且需要更新时才被更新。
if (weights[j] != 0 && G.arc[k][j] < weights[j]){
weights[j] = G.arc[k][j];
}
}
}
// 计算最小生成树的权值
sum = 0;
for (i = 1; i < index; i++)
{
min = INFINITY;
// 获取prims[i]在G中的位置
n = locateVex(G, prims[i]);
// 在vexs[0...i]中,找出到j的权值最小的顶点。
for (j = 0; j < i; j++)
{
m = locateVex(G, prims[j]);
if (G.arc[m][n]<min){
min = G.arc[m][n];
}
}
sum += min;
}
// 打印最小生成树
printf("PRIM(%c)=%d: ", G.vexs[start], sum);
for (i = 0; i < index; i++)
printf("%c ", prims[i]);
printf("\n");
}
// 主函数测试
int main(){
Graph G;
createUDN(G);
prim(G,0);
}
最短路径——Floyd算法
注意点
这里书上也是提出了两种算法,我选择了Floyd算法来进行实现,因为这种算法虽然时间复杂度更高但实现其实较为简单。在这里的实现中,重点就是如何更新A矩阵。直接看代码吧。其实就是一个三层循环,然后里面有
代码
/*
* @Description: Floyd算法的实现
* @version: 1.0
* @Author: Liuge
* @Date: 2021-07-24 21:05:30
*/
#include <bits/stdc++.h>
#define MAXV 100
#define INF 9999
typedef struct
{
int edges[MAXV][MAXV];
int n, e;
} MGraph;
void ppath(int path[][MAXV], int i, int j)
{
int k;
k = path[i][j];
if (k == -1)
return;
ppath(path, i, k);
printf("%d,", k);
ppath(path, k, j);
}
// 输出最短路径
void DisPath(int A[][MAXV], int path[][MAXV], int n)
{
int i, j;
for (i = 0; i < n; i++){
for (j = 0; j < n; j++){
if (A[i][j] != INF && i != j){
printf(" 从%d到%d路径为:", i, j);
printf("%d,", i);
ppath(path, i, j);
printf("%d", j);
printf("\t路径长度为:%d\n", A[i][j]);
}
}
}
}
// Floyd算法的实现
void Floyd(MGraph g)
{
int A[MAXV][MAXV], path[MAXV][MAXV];
int i, j, k, n = g.n;
// 给A数组置初值
for (i = 0; i < n; i++){
for (j = 0; j < n; j++){
A[i][j] = g.edges[i][j];
path[i][j] = -1;
}
}
// 计算Ak
for (k = 0; k < n; k++){
for (i = 0; i < n; i++){
for (j = 0; j < n; j++){
if (A[i][j] > (A[i][k] + A[k][j])){
A[i][j] = A[i][k] + A[k][j];
path[i][j] = k;
}
}
}
}
printf("\n输出最短路径:\n");
// 输出最短路径
DisPath(A, path, n);
}
//输出邻接矩阵g
void DispMat(MGraph g){
int i, j;
for (i = 0; i < g.n; i++{
for (j = 0; j < g.n; j++){
if (g.edges[i][j] == INF){
printf("%3s", "∞");
}else{
printf("%3d", g.edges[i][j]);
}
}
printf("\n");
}
}
int main(){
int i, j, n;
MGraph g;
printf("请输入图中节点的个数\n");
scanf("%d", &n);
int B[n][n];
g.n = n;
g.e = n;
printf("请输入邻接矩阵\n");
for (i = 0; i < n; i++){
for (j = 0; j < n; j++){
scanf("%d", &B[i][j]);
}
}
for (i = 0; i < g.n; i++){
for (j = 0; j < g.n; j++){
g.edges[i][j] = B[i][j];
}
}
//DispMat(g);
Floyd(g);
printf("\n");
return 0;
}
拓扑排序
注意点
这里是按照王道教材上的实现方法,对其进行了一部分改写,使用了栈进行实现。大概的思路很简单,每次都删掉入度为0的结点,然后输出结点,最后删完为止。具体实现看代码吧。
代码
/*
* @Description: 拓扑排序的邻接表实现
* @version: 1.0
* @Author: Liuge
* @Date: 2021-07-24 20:14:44
*/
#include<bits/stdc++.h>
// 顶点类型,由用户自己定义
typedef char VertexType;
// 边上的权值类型,由用户自己定义
typedef int EdgeType;
// 最大顶点数,由用户定义
#define MAX_VEX 20
#define maxSize 50
// 使用邻接表法表示有向图
// 边表结点
typedef struct EdgeNode
{
// 邻接点域,存储该顶点在顶点表中的下标
int adjvex;
// 网图权值
EdgeType weight;
// 链域,指向下一个邻接点
struct EdgeNode *next;
}EdgeNode;
// 顶点表结点
typedef struct VertexNode
{
// 顶点域,存储顶点信息
VertexType data;
// 边表头指针
EdgeNode *firstedge;
}VertexNode, AdjList[MAX_VEX];
typedef struct
{
AdjList adjList;
// 图中当前顶点数和弧数
int vexNum, arcNum;
}GraphList;
// ************顺序栈的相关实现************
// 顺序栈的存储结构
typedef struct{
int data[maxSize];
// 栈顶指针
int top;
}SqStack;
// 初始化栈
void initStack(SqStack &S){
// 这里把栈顶指针设为-1,也可以设为0
S.top = -1;
}
// 判空
bool isStackEmpty(SqStack S){
return S.top == -1;
}
// 进栈
bool push(SqStack &S, int e){
// 栈满
if(S.top == maxSize -1){
return false;
}
S.data[++S.top] = e;
return false;
}
// 出栈
bool pop(SqStack &S,int &e){
// 栈空
if(S.top == -1){
return false;
}
e = S.data[S.top--];
return true;
}
// 读栈顶元素
bool getTop(SqStack &S,int &e){
if(S.top == -1){
return false;
}
e = S.data[S.top];
return true;
}
// ************顺序栈的相关实现结束************
// 定位顶点v在顶表的下标位置,不存在返回-1
int locateVex(GraphList g, VertexType v)
{
int i;
for (i = 0; i < g.vexNum; i++)
{
if (v == g.adjList[i].data )
break;
}
if (i > g.vexNum)
{
printf("结点不存在!");
return -1;
}
return i;
}
// 用邻接表表示法,构造有向网g
void createGraph(GraphList &g)
{
int i, j;
EdgeType w;
EdgeNode *e, *f;
printf("输入顶点数和边数:\n");
scanf("%d,%d", &(g.vexNum), &(g.arcNum));
//输入顶点
for (i = 0; i < g.vexNum; i++)
{
printf("顶点%d:", i+1);
g.adjList[i].data = getchar();
g.adjList[i].firstedge = NULL;
while(g.adjList[i].data == '\n')
{
g.adjList[i].data = getchar();
}
}
getchar();
// 建立边表
printf("输入边(vi, vj)上的顶点vi vj和权值w\n");
for (i = 0; i < g.arcNum; i++)
{
char v1, v2;
scanf("%c %c %d", &v1, &v2, &w);
getchar();
int m = locateVex(g, v1);
int n = locateVex(g, v2);
if (m == -1 || n == -1)
{
printf("结点不存在!");
return;
}
//向内存申请空间,生成边表结点
e = (EdgeNode*)malloc(sizeof(EdgeNode));
if(e == NULL)
{
printf("内存空间申请失败!");
return;
}
// 邻接序号为n
e->adjvex = n;
// 权值
e->weight = w;
// 单链表的头插法
e->next = g.adjList[m].firstedge;
g.adjList[m].firstedge = e;
}
}
// 拓扑排序
bool toplogicalSort(GraphList G){
SqStack S;
// 初始化一个栈,存储入度为0的结点
initStack(S);
// 统计每个顶点的入度数
int indegree[100];
EdgeNode *node;
for(int i = 0; i < G.vexNum; i++)
{
node = G.adjList[i].firstedge;
while (node != NULL)
{
indegree[node->adjvex]++;
node = node->next;
}
}
// 把所有度为0的结点存储到栈内
for(int i = 0;i < G.vexNum;i++){
if(indegree[i] == 0){
push(S,i);
}
}
// 开始进行拓扑排序
int i;
// 使用count记录输出了几个结点
int count = 0;
while(!isStackEmpty(S)){
// 栈顶元素出栈
pop(S,i);
// 输出顶点i
printf("%c ",G.adjList[i].data);
count++;
for(EdgeNode *p = G.adjList[i].firstedge;p != NULL;p = p->next){
// 将所有i指向的顶点的入度减1,并且把入度为0的顶点压入栈S
int v = p->adjvex;
if(!(--indegree[v])){
push(S,v);
}
}
}
return !(count < G.vexNum);
}
// 主函数测试
int main(){
GraphList G;
createGraph(G);
printf("拓扑排序序列为:\n");
toplogicalSort(G);
return 0;
}
总结
这一章的核心部分在图的应用,也就是后面几个算法的实现。关键路径我并没有实现,个人认为掌握手写做法就大概足够了。感谢网上的大佬资料。