1. 图 Graph
- 非线性结构
- 结点与结点之间关系是任意的
图的形式描述 V:vertex E:edge
G = (V, E)
V是顶点的集合 图是没有空图的 vi | v0, v1...vn-1
E是边的集合 <vi,vj> | <v0,v1>, <v1,v2>.....
<vi,vj> 是顶点vi到顶点vj之间是否存在路径的判断条件
图是一个二元组,是顶点以及顶点之间路径的集合连通图:图中任意两个顶点之间都存在一条路径(即任意两个顶点是连通的)
强连通图:图中任意两个顶点之间都存在双向的有向路径。具体来说,一个有向图是强连通图,当且仅当对于图中的任意两个顶点 (vi) 和 (vj),都存在一条从 (vi) 到 (vj) 的有向路径和一条从 (vj) 到 (vi) 的有向路径
2. 分类
无向图:
路径 -- 边
如果<vi, vj>存在 那么<vj, vi>一定存在
有向图:
路径 -- 弧
如果<vi, vj>存在 那么<vj, vi>不一定存在
3. 网
如果在图的关系<vi,vj>或<vj,vi>上 附加了一个权值w 称w为弧或边上的权
带权的图叫做网
4. 顶点的度
无向图:
顶点的边的条数有向图:
入度:以顶点为终点的弧的数目
出度:以顶点为起点的弧的数目
顶点的度 = 入度 + 出度
完全图:
- 无向图:每两个顶点存在一条边 顶点为n 边:n*(n-1)/2
- 有向图:每两个顶点都存在着两个方向相反的边 顶点为n 边:n*(n-1)
5. 图的物理存储结构
存储图:
- 顶点的集合
- 关系的集合
邻接矩阵 (二维数组) <------------------
邻接表(数组 + 链表)------> 数组存储起点
十字链表( 逆邻接表 ):(数组 + 链表) ------> 数组存储终点
邻接多重链表
邻接点:
在无向图中,如果存在一条边(vi,vj),则vi和vj互为邻接点
在有向图中,如果存在一条边<vi,vj>,则vi是弧的起点,vj是弧的终点,称vj是vi的邻接点
(1)邻接矩阵 adjacency matrix
#define E_MAX INT32_MAX // 边 无穷大 不可达
#define V_MAX 256 // 顶点最大个数
typedef char VType; // 顶点的类型
typedef int WType; // 权的类型 边上的数值
typedef strutc Graph {
VType V[V_MAX]; // 一维数组 用来存储顶点的集合
WType E[V_MAX][V_MAX]; // 二维数组 用来存储 顶点之间的距离
int v_num; // 实际顶点的个数
int e_num; // 实际边的条数
} Graph;
(2)邻接表 (数组 + 链表)
struct adj {
int index; // 边的终点在数组中的下标
int w; // 权值
struct adj *next;
};
typedef struct {
VType data;
struct adj *first; // 指向以数组中的顶点为起始点的第一个终点
} Vextex;
Vertex graph[V_MAX]; // 顶点结构体数组
邻接矩阵 代码的实现
// 找顶点对应的下标
int find_index(VType *v, int n, VType x) {
for (int i = 0; i < n; i++) {
if (v[i] == x) { // 找到了
return i;
}
}
return -1; // 可能找不到顶点
}
/*
创建一个图
返回值为:图的类型
*/
Graph* create_graph() {
// 1.创建一个空图,把关系集合初始化
Graph *g = malloc(sizeof(Graph));
// 把关系的集合赋值为无穷大
for (int i = 0; i < V_MAX; i++) {
for (int j = 0; j < V_MAX; j++) {
g->E[i][j] = E_MAX;
}
}
g->v_num = 0; // 实际顶点的个数
g->e_num = 0; // 实际边的个数
// 2.根据用户的输入 初始化顶点的集合
printf("请输入顶点的值:(eg:abcd):\n");
scanf("%s", g->V);
getchar(); // 重点:吸收回车
g->v_num = strlen(g->V);
printf("请输入关系与权值:(eg:ab12):\n");
while (1) {
VType s, d; // 起点 终点
WType w; // 权值
// 输入
scanf("%c%c%d", &s, &d, &w); // 输入一组数组 不能用空格隔开 ab12
getchar(); // 重点:吸收回车
if (s == '#' && d == '#' && w == 0) { // 自己约定退出条件
break;
}
// 找顶点s d在顶点元素集合v中对应的下标
int si = find_index(g->V, g->v_num, s);
int di = find_index(g->V, g->v_num, d);
// 保存关系 权值
if (si != -1 && di != -1) {
g->E[si][di] = w;
g->e_num++;
}
}
return g;
}
6. 图的遍历
图的遍历 是树的推广 是按照某一种规则去访问图中的每一个顶点 每个顶点只能访问一次
为了避免出现同一节点被重复访问的情况,需要记住每个被访问过的结点
设置一个访问标记的数组visited
顶点i被访问过 visited[i] = 1
顶点i未被访问过 visited[i] = 0
(1)深度优先搜索DFS: depth first search
类似于树的先序遍历
设 初始时,图中各个顶点都是未被访问 int visited[V_MAX] = {0};
从图中的某个顶点出发(v0),访问v0
然后找v0的邻接点vi,如果vi没有被访问,就需要按照同样的方式访问vi
再去找vi的邻接点..(深度优先搜索)
如果该顶点所有的邻接点都被访问过了,则回到上一个顶点
然后再去以该顶点 进行深度优先搜索 去访问其他的邻接点
直到所有的访问的顶点 都被访问完毕 结束
int visited[V_MAX]; // 访问 标志位数组,表示下标对应的顶点是否被访问过 0没有访问 1已经访问
/*
得到图中顶点v的下一个邻接点
g:图
v:需要找的下一个邻接点顶点的下标
w:每一次开始找的起点
*/
int find_next_adj(Graph *g, int v, int w) {
for (int i = w; i < g->v_num; i++) { // 遍历v的这一行 找与v为顶点的下一个邻接点
if (g->E[v][i] != E_MAX) { // 关系存在,找到以v开头的后面遍历的邻接点 返回其下标
return i;
}
}
return -1; // 没有找到
}
/*
深度优先搜索(类似于树的先序遍历)
g:图
v:开始访问的顶点的下标
*/
void DFS(Graph *g, int v) {
// 1.先访问v 标记
printf("%c ", g->V[v]);
visited[v] = 1;
// 2.找v的邻接点
for (int w = find_next_adj(g, v, 0); w != -1; w = find_next_adj(g, v, w + 1)) {
if (visited[w] == 0) { // 如果找到的邻接点未被访问,则按照同样的方式往下访问
DFS(g, w);
}
}
}
// 解决非连通图
void DFS_traverse(Graph *g) {
// 全部标志位 置 0
for (int i = 0; i < g->v_num; i++) {
visited[i] = 0;
}
// DFS
for (int i = 0; i < g->v_num; i++) {
if (visited[i] == 0) {
DFS(g, i);
}
}
printf("\n");
}
(2)广度优先搜索BFS:breath first search
类似于树的层次遍历
设 初始时,图中各个顶点都是未被访问 int visited[V_MAX] = {0};
从图中的某个顶点出发(v0),访问v0
然后找v0的邻接点vi,把所有的vi都访问完了
然后再从这些已经访问的邻接点中 依照广度优先搜索的方式去全部的邻接点
队列
入队元素 先访问再入队
出队元素 把它所有未被访问过的邻接点入队
int visited[V_MAX]; // 访问 标志位数组,表示下标对应的顶点是否被访问过 0没有访问 1已经访问
/*
得到图中顶点v的下一个邻接点
g:图
v:需要找的下一个邻接点顶点的下标
w:每一次开始找的起点
*/
int find_next_adj(Graph *g, int v, int w) {
for (int i = w; i < g->v_num; i++) { // 遍历v的这一行 找与v为顶点的下一个邻接点
if (g->E[v][i] != E_MAX) { // 关系存在,找到以v开头的后面遍历的邻接点 返回其下标
return i;
}
}
return -1; // 没有找到
}
// 广度优先搜索(层序遍历--->借用队列)
void BFS(Graph *g, int v) {
// 1.初始化队列
int queue[g->v_num + 1];
int head = 0; // 队头
int tail = 0; // 队尾
// 2.访问v 再把v进行入队(入队入下标) 标志位置为1
queue[++tail] = v;
visited[v] = 1;
while (head < tail) { // 队列不为空
// 出队 获取该顶点的下标i,把所有的未访问的邻接点入队
int index = queue[++head];
printf("%c ", g->V[index]);
// 求邻接点
for (int w = find_next_adj(g, index, 0); w != -1; w = find_next_adj(g, index, w + 1)) {
if (visited[w] == 0) { // 邻接点没有被访问
// 先访问 标志位置为1 再入队
visited[w] = 1;
queue[++tail] = w;
}
}
}
}
// 解决非连通图
void BFS_traverse(Graph *g) {
// 全部标志位 置 0
for(int i = 0; i < g->v_num; i++) {
visited[i] = 0;
}
// BFS
for (int i = 0; i < g->v_num; i++) {
if (visited[i] == 0) {
BFS(g, i);
}
}
printf("\n");
}
7. 最短路径
解决带权的有向图 两个顶点之间的最短距离的问题
两个经典的算法:
迪杰斯特拉 Dijkstra 两层循环 O(n^2)
弗洛伊德 Floyd 三层循环 O(n^3)
都是用于比较 v0vi 与 v0vk+vkvi的大小的基本算法迪杰斯特拉Dijkstra
是解决从网络中任意的顶点(源点)出发,求他到其他各个顶点的最短距离
假设图中有n个顶点,求一个源顶点vm 到 它所有的n-1个顶点vi的最短距离基本思想:
vm到vi的最短路径,只有两种情况:
1. 直接到
vm和vi存在关系
2. 间接到
中间需要经过其他的顶点vk
vm ----> vk 的距离 + vk ----> vi 的距离
vm -----> vk >= vm --->vi 显而易见 直接到最短的
迪杰斯特拉Dijkstra 两层循环 0(n^2)
为了方便描述,定义数组 dist
int dist[v_num];
dist[i]; // 表示 vm 到 vi的距离
dist[0]; //表示 vm 到 v0的距离
dist[3]; //表示 vm 到 v3的距离
......(1)直接到 vm 到 vi
dist[i] = g->E[m][i]; // g->E是一个二维数组 ,保存 每两个顶点间的距离
(2)通过其他的顶点 vi
vm ...... vk ...... vi
temp = g->E[m][k] + g->E[k][i]
if (temp < dist[i]) {
dest[i] = temp; // 更新最短路径}
需要用到三个辅助数组:
- dist数组 最短路径
dist[v_num]; dist[i]; // 表示 vm 到 vi的距离 dist[0]; // 表示 vm 到 v0的距离 dist[3]; // 表示 vm 到 v3的距离 ......
- path 数组 保存已经确定好的最短路径的顶点
char path[v_num][v_num]; // 保存 实际的最短路径经过的顶点 path[i][0] = vm; // 标识起点 vm path[i]; // 保存 vm 到 vi 的路程
- s 数组
int s[v_num]; // 标志数组 标识 顶点 vi的路径是否已经确定好了是最短路径 s[i] = 0; // vm 到 vi 的最短路径暂时未确定 s[i] = 1; // vm 到 vi 的最短路径已确定
算法实现:
总体分两大步
1.
确定最短路径
从 源点 vm 到 其他各顶点的第一个最短距离的长度为
dist[k] = min{dist[i] | i = 0,1,2,3. ...n-1 | s[i] == 0}
dist 数组中的最小值 就是dist[k] 就是确定了 最短路径
但是其实我们所需要的是最小值的下标
s[k] = 1
2.
更新最短路径
s[i] == 0; (vm 到 vi的最短路径没有确定好)
if (dist[i] > g->E[k][i] + dist[k]) {
vm 到 vi 有了更短的路径
更新
dist[i] = g->E[k][i] + dist[k];
}
循环N-1次
/* 迪杰斯特拉算法 */ // 辅助数组 int s[V_MAX]; // 标记v到其他顶点的最短路径是否已经求出 int dist[V_MAX]; // 求最短路径的距离 char path[V_MAX][V_MAX]; // V->A->B /* g:图 v:源点的下标 */ void Dijkstra(Graph *g, int v) { // 参数有效性检查 if (g == NULL) { return; } // 初始化数组 for (int i = 0; i < g->v_num; i++) { s[i] = 0; // 标记v到其他顶点的最短路径是否已经求出 0:未求出 dist[i] = g->E[v][i]; // 最短路径初始化为v到各个顶点的直接距离 path[i][0] = g->V[v]; // 路径数组的第一个全部初始化为原点 path[i][1] = g->V[i]; // 路径数组的第二个全部初始化为终点 } s[v] = 1; // v->v 已经知道是无穷大 默认已经求出了最短路径 // 循环n-1次求最短路径 for (int i = 0; i < g->v_num - 1; i++) { // 1.求最短路径 在s[i]标记为0的位置找 int min = E_MAX; int min_index; // 最短路径的下标 for (int j = 0; j < g->v_num; j++) { if (s[j] == 0) { // 还没有求出最短路径 if (dist[j] < min) { min = dist[j]; min_index = j; // 记录当前求出的最短路径的下标 } } } // dist[min_index]就是最短路径,V[v] -> V[min_index] 的最短路径求出来了 path[min_index] s[min_index] = 1; // 2.根据现在的最短路径去比较 更新其他的dist // 对所有的s[j] = 0 更新dist[j] // if (dist[min_index] + g->E[min_index][j] < dist[j]) 更新 for (int j = 0; j < g->v_num; j++) { if (s[j] == 0) { // 最短路径还未被求出 if (dist[min_index] + g->E[min_index][j] < dist[j]) { // 更新 最短路径 dist[j] = dist[min_index] + g->E[min_index][j]; // 更新 路径 strcpy(path[j], path[min_index]); // VB -> VA path[j][strlen(path[j])] = g->V[j]; // VA -> VAB } } } } // 打印 V->B[4]:V->D->B for (int i = 0; i < g->v_num; i++) { if (i == v) { continue; } printf("%c->%c[%d]:", g->V[v], g->V[i], dist[i]); // 打印路径 遍历 path[i] int j; for (j = 0; path[i][j+1]; j++) { printf("%c->", path[i][j]); // 最后一个字符 单独处理 } printf("%c\n", path[i][j]); } }
8. 图代码
graph.h
#ifndef _GRAPH_H__ #define _GRAPH_H__ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <limits.h> #define E_MAX 65535 // 边 无穷大 不可达 #define V_MAX 256 // 顶点最大个数 typedef char VType; // 顶点的类型 typedef int WType; // 权的类型 边上的数值 typedef struct Graph { VType V[V_MAX]; // 一维数组 来存储顶点的集合 WType E[V_MAX][V_MAX]; // 二维数组 来存储 顶点之间的关系 int v_num; // 实际顶点的个数 int e_num; // 实际边的条数 } Graph; // 找顶点对应的下标 int find_index(VType *v, int n, VType x); /* 创建一个图 返回值为:图的类型 */ Graph* create_graph(); // 打印图 void print_graph(Graph *g); int find_next_adj(Graph *g, int v, int w); /* 深度优先搜索(类似于树的先序遍历) g:图 v:开始访问的顶点的下标 */ void DFS(Graph *g, int v); // 解决非连通图 void DFS_traverse(Graph *g); void BFS(Graph *g, int v); // 解决非连通图 void BFS_traverse(Graph *g); void Dijkstra(Graph *g, int v); #endif
graph.c
#include "graph.h" // 找顶点对应的下标 int find_index(VType *v, int n, VType x) { for (int i = 0; i < n; i++) { if (v[i] == x) { // 找到了 return i; } } return -1; // 可能找不到顶点 } /* 创建一个图 返回值为:图的类型 */ Graph* create_graph() { // 1.创建一个空图,把关系集合初始化 Graph *g = malloc(sizeof(Graph)); // 把关系的集合赋值为无穷大 for (int i = 0; i < V_MAX; i++) { for (int j = 0; j < V_MAX; j++) { g->E[i][j] = E_MAX; } } g->v_num = 0; // 实际顶点的个数 g->e_num = 0; // 实际边的个数 // 2.根据用户的输入 初始化顶点的集合 printf("请输入顶点的值:(eg:abcd):\n"); scanf("%s", g->V); getchar(); // 重点:吸收回车 g->v_num = strlen(g->V); printf("请输入关系与权值:(eg:ab12):\n"); while (1) { VType s, d; // 起点 终点 WType w; // 权值 // 输入 scanf("%c%c%d", &s, &d, &w); // 输入一组数组 不能用空格隔开 ab12 getchar(); // 重点:吸收回车 if (s == '#' && d == '#' && w == 0) { // 自己约定退出条件 break; } // 找顶点s d在顶点元素集合v中对应的下标 int si = find_index(g->V, g->v_num, s); int di = find_index(g->V, g->v_num, d); // 保存关系 权值 if (si != -1 && di != -1) { g->E[si][di] = w; g->e_num++; } } return g; } // 打印图 void print_graph(Graph *g) { // 打印顶点数组 putchar('\t'); for (int i = 0; i < g->v_num; i++) { printf("%c\t", g->V[i]); } printf("\n"); // 打印关系 for (int i = 0; i < g->v_num; i++) { printf("%c\t", g->V[i]); for (int j = 0; j < g->v_num; j++) { if (g->E[i][j] == E_MAX) { printf("-\t"); } else { printf("%d\t", g->E[i][j]); } } putchar('\n'); } } int visited[V_MAX]; // 访问 标志位数组,表示下标对应的顶点是否被访问过 0没有访问 1已经访问 /* 得到图中顶点v的下一个邻接点 g:图 v:需要找的下一个邻接点顶点的下标 w:每一次开始找的起点 */ int find_next_adj(Graph *g, int v, int w) { for (int i = w; i < g->v_num; i++) { // 遍历v的这一行 找与v为顶点的下一个邻接点 if (g->E[v][i] != E_MAX) { // 关系存在,找到以v开头的后面遍历的邻接点 返回其下标 return i; } } return -1; // 没有找到 } /* 深度优先搜索(类似于树的先序遍历) g:图 v:开始访问的顶点的下标 */ void DFS(Graph *g, int v) { // 1.先访问v 标记 printf("%c ", g->V[v]); visited[v] = 1; // 2.找v的邻接点 for (int w = find_next_adj(g, v, 0); w != -1; w = find_next_adj(g, v, w + 1)) { if (visited[w] == 0) { // 如果找到的邻接点未被访问,则按照同样的方式往下访问 DFS(g, w); } } } // 解决非连通图 void DFS_traverse(Graph *g) { // 全部标志位 置 0 for (int i = 0; i < g->v_num; i++) { visited[i] = 0; } // DFS for (int i = 0; i < g->v_num; i++) { if (visited[i] == 0) { DFS(g, i); } } printf("\n"); } // 广度优先搜索(层序遍历--->借用队列) void BFS(Graph *g, int v) { // 1.初始化队列 int queue[g->v_num + 1]; int head = 0; // 队头 int tail = 0; // 队尾 // 2.访问v 再把v进行入队(入队入下标) 标志位置为1 queue[++tail] = v; visited[v] = 1; while (head < tail) { // 队列不为空 // 出队 获取该顶点的下标i,把所有的未访问的邻接点入队 int index = queue[++head]; printf("%c ", g->V[index]); // 求邻接点 for (int w = find_next_adj(g, index, 0); w != -1; w = find_next_adj(g, index, w + 1)) { if (visited[w] == 0) { // 邻接点没有被访问 // 先访问 标志位置为1 再入队 visited[w] = 1; queue[++tail] = w; } } } } // 解决非连通图 void BFS_traverse(Graph *g) { // 全部标志位 置 0 for(int i = 0; i < g->v_num; i++) { visited[i] = 0; } // BFS for (int i = 0; i < g->v_num; i++) { if (visited[i] == 0) { BFS(g, i); } } printf("\n"); } /* 迪杰斯特拉算法 */ // 辅助数组 int s[V_MAX]; // 标记v到其他顶点的最短路径是否已经求出 int dist[V_MAX]; // 求最短路径的距离 char path[V_MAX][V_MAX]; // V->A->B /* g:图 v:源点的下标 */ void Dijkstra(Graph *g, int v) { // 参数有效性检查 if (g == NULL) { return; } // 初始化数组 for (int i = 0; i < g->v_num; i++) { s[i] = 0; // 标记v到其他顶点的最短路径是否已经求出 0:未求出 dist[i] = g->E[v][i]; // 最短路径初始化为v到各个顶点的直接距离 path[i][0] = g->V[v]; // 路径数组的第一个全部初始化为原点 path[i][1] = g->V[i]; // 路径数组的第二个全部初始化为终点 } s[v] = 1; // v->v 已经知道是无穷大 默认已经求出了最短路径 // 循环n-1次求最短路径 for (int i = 0; i < g->v_num - 1; i++) { // 1.求最短路径 在s[i]标记为0的位置找 int min = E_MAX; int min_index; // 最短路径的下标 for (int j = 0; j < g->v_num; j++) { if (s[j] == 0) { // 还没有求出最短路径 if (dist[j] < min) { min = dist[j]; min_index = j; // 记录当前求出的最短路径的下标 } } } // dist[min_index]就是最短路径,V[v] -> V[min_index] 的最短路径求出来了 path[min_index] s[min_index] = 1; // 2.根据现在的最短路径去比较 更新其他的dist // 对所有的s[j] = 0 更新dist[j] // if (dist[min_index] + g->E[min_index][j] < dist[j]) 更新 for (int j = 0; j < g->v_num; j++) { if (s[j] == 0) { // 最短路径还未被求出 if (dist[min_index] + g->E[min_index][j] < dist[j]) { // 更新 最短路径 dist[j] = dist[min_index] + g->E[min_index][j]; // 更新 路径 strcpy(path[j], path[min_index]); // VB -> VA path[j][strlen(path[j])] = g->V[j]; // VA -> VAB } } } } // 打印 V->B[4]:V->D->B for (int i = 0; i < g->v_num; i++) { if (i == v) { continue; } printf("%c->%c[%d]:", g->V[v], g->V[i], dist[i]); // 打印路径 遍历 path[i] int j; for (j = 0; path[i][j+1]; j++) { printf("%c->", path[i][j]); // 最后一个字符 单独处理 } printf("%c\n", path[i][j]); } }
main.c
#include "graph.h" int main(int argc, char *argv[]) { Graph *g = create_graph(); print_graph(g); DFS_traverse(g); // 深度搜索 BFS_traverse(g); // 广度搜索 // Dijkstra(g, 0); // 迪杰斯特拉算法 return 0; }