数据结构(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("输入顶点数和边数:
");
	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]=='
'){
			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
");
	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("领接矩阵表为:
");
	for (i = 0; i < g.vexNum; i++)
	{
		for (j = 0; j < g.vexNum; j++)
		{
			printf("%6d", g.arc[i][j]);
		}
		putchar('
');
	}
}

// 图的深度优先遍历算法 
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("深度优先遍历结果为:
");
    DFS_AM(G,0);
    printf("
");
    printf("广度优先遍历结果为:
");
    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("输入顶点数和边数:
");
	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 == '
')
		{
			g.adjList[i].data = getchar();
		}
	}
	getchar();
	// 建立边表
	printf("输入边(vi, vj)上的顶点vi vj和权值w
");
	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("邻接表为:
");
	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('
');
	}
}

// 主函数测试
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("输入顶点数和边数:
");
	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]=='
'){
			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
");
	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("
");
}

// 主函数测试
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("	路径长度为:%d
", 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("
输出最短路径:
");
    // 输出最短路径
    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("
");
    }
}
int main(){
    int i, j, n;
    MGraph g;
    printf("请输入图中节点的个数
");
    scanf("%d", &n);
    int B[n][n];
    g.n = n;
    g.e = n;
    printf("请输入邻接矩阵
");
    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("
");
    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("输入顶点数和边数:
");
	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 == '
')
		{
			g.adjList[i].data = getchar();
		}
	}
	getchar();
	// 建立边表
	printf("输入边(vi, vj)上的顶点vi vj和权值w
");
	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("拓扑排序序列为:
");
    toplogicalSort(G);
    return 0;
}

总结

这一章的核心部分在图的应用,也就是后面几个算法的实现。关键路径我并没有实现,个人认为掌握手写做法就大概足够了。感谢网上的大佬资料。

原文地址:https://www.cnblogs.com/wushenjiang/p/15056358.html