C语言--实现图的基本操作
使用C语言实现图的创建以及遍历等基本操作
文章目录
- 图基本概念
- 邻接矩阵和邻接表对比
- 定义图
- 创建图
- 添加边
- 深度优先遍历DFS
- 运行
- 广度优先遍历BFS
- 运行
- 迪杰斯特拉算法
- 运行
- 弗洛伊德算法
- 运行
图基本概念
图由顶点和边组成。根据边之间的方向性可以分为有向图和无向图,无向图表示双向关系,有向图表示单向关系。
邻接矩阵和邻接表对比
在C语言中,图的创建可以通过邻接矩阵和邻接表两种方式来实现。
- 邻接表使用链表数组来表示图中所有顶点之间关系。每个顶点都有一个链表,链表中的每个节点表示从该顶点出发的边。优点是对稀疏图来说节省大量空间,适用于稀疏图。
- 邻接矩阵使用二维数组来表示节点之间的权值。优点是直观便于理解,缺点是对于稀疏图来说,会浪费大量空间,适用于稠密图。
本文中使用邻接矩阵来表示无向图。假设顶点从编号0开始。
定义图
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#define MAX_VERTICES 10
typedef struct Graph {
int vertices; // 图节点个数
int adjMatrix[MAX_VERTICES][MAX_VERTICES]; // 图中边的关系
} Graph;
创建图
不带权重版:0表示不连通,1表示连通
void createGraph(Graph *graph, int vertices) {
graph->vertices = vertices;
for (int i = 0; i < vertices; i++) {
for (int j = 0; j < vertices; j++) {
graph->adjMatrix[i][j] = 0;
}
}
}
带权值版:INT_MAX表示不连通
void createGraphWithWeight(Graph *graph, int vertices) {
graph->vertices = vertices;
for (int i = 0; i < vertices; i++) {
for (int j = 0; j < vertices; j++) {
if (i==j) {
graph->adjMatrix[i][j] = 0;
}else {
graph->adjMatrix[i][j] = INT_MAX;
}
}
}
}
添加边
void addEdge(Graph *graph, int u, int v) {
graph->adjMatrix[u][v] = 1;
graph->adjMatrix[v][u] = 1;
}
void addEdgeWithWeight(Graph *graph, int u, int v, int weight) {
graph->adjMatrix[u][v] = weight;
graph->adjMatrix[v][u] = weight;
}
深度优先遍历DFS
沿着一条路径深入探索,直到尽头,再回溯到上一个节点尝试其他路径。
递归通过栈来管理函数调用的层级,和DFS纵深优先的特性非常契合。
void DFS(Graph *graph, int vertex, bool visited[]) {
// 最先访问起点
visited[vertex] = true;
printf("%d ", vertex);
for (int i = 0; i < graph->vertices; i++) {
// 递归访问连通且未访问的所有节点
if (graph->adjMatrix[vertex][i] && !visited[i]) {
DFS(graph, i, visited);
}
}
}
void DFSTraverse(Graph *graph, int startVertex) {
bool visited[MAX_VERTICES] = {false};
DFS(graph, startVertex, visited);
}
运行
如何建立图:
int main() {
Graph graph;
createGraph(&graph, 4);
addEdge(&graph, 0, 1);
addEdge(&graph, 0, 2);
addEdge(&graph, 1, 3);
DFSTraverse(&graph, 0);
}
先遍历0–1–3
再回溯到1–2
广度优先遍历BFS
从一个未访问的顶点开始,先遍历这个顶点的所有相邻节点,再依次遍历每个相邻节点的相邻节点。
利用队列先进先出的特性,将待访问顶点的相邻节点依次放入队列中,然后依次出队列,实现广度优先遍历
void BFSTraverse(Graph *graph, int startVertex) {
bool visited[MAX_VERTICES] = {false};
int queue[MAX_VERTICES] = {0};
int front=-1, rear=-1;
//起始顶点加入队列
queue[++rear] = startVertex;
visited[startVertex] = true;
while(rear != front) {
int currentVertex = queue[++front];
printf("%d ", currentVertex);
for (int i = 0; i < graph->vertices; i++) {
if (graph->adjMatrix[currentVertex][i] && !visited[i]) {
// 将未访问且连通的加入队列
queue[++rear] = i;
visited[i] = true;
}
}
}
}
运行
先遍历0的邻居节点0–1–2
再遍历1和2的邻居节点3
迪杰斯特拉算法
用于寻找单源最短路径
- 采用一个一维数组标志节点是否已访问(也就是是否加入节点集中)
- 采用另外一个数组记录未包含在节点中的节点到该节点集的最短距离,初始到其他结点距离都是INT_MAX。
- 采用贪心算法,每次都选取距离节点集最近且未被访问的节点加入到节点集中,然后再根据这个节点更新最短距离数组。
int findMinIndex(Graph *graph, bool visited[], int distance[]){
int nodeNum = graph->vertices;
int min = INT_MAX,minIndex = 0;
for(int i=0;i<nodeNum;i++){
if(!visited[i] && distance[i]<min){
min = distance[i];
minIndex = i;
}
}
return minIndex;
}
void dijkstra(Graph *graph, bool visited[], int distance[]){
int nodeNum = graph->vertices;
printf("加入节点集顺序:\n");
// 解释下这里为什么是nodeNum-1,如果最后只剩一个节点不在节点集的时候,那么节点与节点集直接只有一条路径,就没有优化的必要了。
for(int count=0;count<nodeNum-1;count++){
int current = findMinIndex(graph, visited, distance);
visited[current] = true;
printf("%d\t", current);
for(int i=0;i<nodeNum;i++) {
if (!visited[i]
&& distance[current] != INT_MAX
&& graph->adjMatrix[current][i] != INT_MAX
&& distance[current] + graph->adjMatrix[current][i]< distance[i]) {
distance[i] = distance[current] + graph->adjMatrix[current][i];
}
}
}
printf("\n");
}
void dijkstraFun(Graph *graph, int start) {
int nodeNum = graph->vertices;
bool visited[nodeNum];
int distance[nodeNum];
for(int i=0;i<nodeNum;i++){
distance[i] = INT_MAX;
visited[i] = false;
}
distance[start] = 0;
dijkstra(graph, visited, distance);
for(int i=0;i<nodeNum;i++){
if (i != start) {
printf("到节点 %d 的最短距离是:%d\n",i, distance[i]);
}
}
}
运行
我们从网上随便找一个迪杰斯特拉的图,然后建立节点和边,进行测试
这里uvwxyz分别对应012345
int main() {
Graph graph;
createGraphWithWeight(&graph, 6);
addEdgeWithWeight(&graph, 0, 1,2);
addEdgeWithWeight(&graph, 0, 2,5);
addEdgeWithWeight(&graph, 0, 3,1);
addEdgeWithWeight(&graph, 1, 2,3);
addEdgeWithWeight(&graph, 1, 3,2);
addEdgeWithWeight(&graph, 2, 3,3);
addEdgeWithWeight(&graph, 2, 4,1);
addEdgeWithWeight(&graph, 2, 5,5);
addEdgeWithWeight(&graph, 3, 4,1);
addEdgeWithWeight(&graph, 4, 5,2);
dijkstraFun(&graph, 0);
}
可以非常容易验证下面结果的正确性。
弗洛伊德算法
用于寻找多源最短路径
采用动态规划的思想,通过不断更新节点之间的最短路径长度来逐步求解任意两个节点之间的最短路径。
原理可以参考这个: 弗洛伊德(Floyd’s)算法—解决最短路径经典算法
void Floyd(Graph *graph){
int len = graph->vertices;
int i,j,k;
printf("初始化矩阵:\n");
for(i=0;i<len;i++) {
for(j=0;j<len;j++) {
if(graph->adjMatrix[i][j] != INT_MAX) {
printf("%d\t",graph->adjMatrix[i][j]);
}else {
printf("∞\t");
}
}
printf("\n");
}
for(k=0;k<len;k++){
for (i = 0; i < len; i++) {
for (j = 0; j < len; j++) {
if(graph->adjMatrix[i][k] != INT_MAX
&& graph->adjMatrix[k][j] != INT_MAX
&& graph->adjMatrix[i][j] > graph->adjMatrix[i][k] + graph->adjMatrix[k][j]){
graph->adjMatrix[i][j] = graph->adjMatrix[i][k] + graph->adjMatrix[k][j];
}
}
}
}
printf("调整后的矩阵:\n");
for(i=0;i<len;i++) {
for(j=0;j<len;j++) {
if(graph->adjMatrix[i][j] != INT_MAX) {
printf("%d\t",graph->adjMatrix[i][j]);
}else {
printf("∞\t");
}
}
printf("\n");
}
}
运行
int main() {
Graph graph;
createGraphWithWeight(&graph, 6);
addEdgeWithWeight(&graph, 0, 1,2);
addEdgeWithWeight(&graph, 0, 2,5);
addEdgeWithWeight(&graph, 0, 3,1);
addEdgeWithWeight(&graph, 1, 2,3);
addEdgeWithWeight(&graph, 1, 3,2);
addEdgeWithWeight(&graph, 2, 3,3);
addEdgeWithWeight(&graph, 2, 4,1);
addEdgeWithWeight(&graph, 2, 5,5);
addEdgeWithWeight(&graph, 3, 4,1);
addEdgeWithWeight(&graph, 4, 5,2);
Floyd(&graph);
}