1. 图的概念
定义
:
图是一种非线性的数据结构,形式化描述为
:
Graph = (V, R)
其中
:
V = (Vi | Vi
属于
datatype, i = 0, 1, 2, 3, 4..... n)
是图中元素
Vi
(
Vi
称之为图的顶点,
Vertex
)
的集合。
当
i
为
0
时,
V
是空集
R=(<Vi, Vj> | Vi
,
Vj
属于
V,
且
Vi, Vj
之间存在路径,
0 <= i, j < n)
是顶点元素之间的关系集,
<Vi, Vj>
为顶点
Vi , Vj
之间是否存在路径的判断条件。 若
Vi, Vj
之间路径存在, 则关系
< Vi, Vj>
属
于
R
有向图
(Digraph)
A ----> B
与
B ----> A
有差别, 我们把这
AB
之间的路径称为
弧
无向图
(Undigraph)
A ----> B
与
B ----> A
没有差别, 我们把
AB
之间的路径称之为
边
网
若在图的关系
< Vi, Vj>
或
<Vj, Vi>
上附加一个值
w,
称
w
为弧或者边上的
权
带权的图称之为网
注意
:
权的具体含义,在不同应用的时候不一样
顶点的度
顶点的边或者弧的条数
入度: 多少条边指向一个顶点
出度:一个顶点指向其他顶点的边的数目
图的路径
一个顶点到另一个顶点的方式
两个顶点存在路径则认为这两个顶点连通,若一个图中任意的两个顶点都连通,则认为这个图为
连
通图
,若该图还有方向,则称之为
强连通图
n
个结点的无向图,当不允许结点到自身的边,也不允许结点到结点的多重边,若边的总数为
n*(n-1)/2
,
则该图一定是连通图
n
个结点的无向图,当不允许结点到自身的边,也不允许结点到结点的多重边,确保它是连通
图,所需的边的最少数量
n - 1
对于连通图来说,所有顶点的度之和为偶数
2. 图的存储结构
邻接矩阵
(
数组表示法
)
,
邻接表
(
链式存储
)
, 十字链表
(
链式存储
)
2.1 数组表示法
称之为
邻接矩阵
(
Adjacency matrix
)
G = <V, R>
用两个数组来存储图
Graph:
一维数组存储
Graph
的顶点的集合
二维数组存储
Graph
的顶点之间的关系集
R,
该二维数组就是所谓的邻接矩阵
typedef char Vtype; //顶点的数据类型
typedef int Adjtype; //邻接矩阵的数据类型,即边或者弧的权值类型
#define MAXSIZE 200 //顶点数组的大小
#define MAX 65535 //用来在邻接矩阵中表示去不了的地方
typedef struct Graph
{
Vtype V[MAXSIZE]; //顶点的集合
Adjtype Adj[MAXSIZE][MAXSIZE]; //权的集合,邻接矩阵
int vexnum; //顶点的数量
int arcnum; //边或者弧的数量
}Graph;
2.2 链表表示法
称之为
邻接表
(
Adjacency List
)
将图中每一个顶点
V
和由
V
出发的弧或者边构成一个单链表。
注意
:
邻接表只适合存储稀疏图
struct adj //邻接结点的数据类型
{
int termIndex; //一个弧或者边的顶点在顶点数组中的下标
Adjtype w; //权值
struct adj* next; //指针域
};
struct Vertex
{
Vtype data; //顶点元素
struct adj* first; //指向邻接表
};
3. 图的遍历
图的遍历是树的遍历的推广,是按照某种规划
(
或次序
)
访问图中各顶点一个且仅一次的操作,也就是将网
状结构按某种规划线性化的过程。
对图的遍历通常有
深度优先搜索
和
广度优先搜索
3.1 深度优先搜索
DFS: depth first serach
类似于树的先序遍历,设初始化的时候,图中每个顶点均未被访问,从图中某一个顶点出发
(
设
V0
出发
)
,
访问
V0,
然后搜索
V0
的一个邻接点
Vi,
若
Vi
没有被访问,则进行访问,再搜索
Vi
的一个邻接点
(
深度优
先
)
若某个顶点的邻接点全部访问完毕,则回溯到它的上一个顶点,然后再从此结点又按照深度优先的方法搜
索下去,直到能够访问的顶点全部访问完毕。
注意
:
从一个顶点进行一次深度优先搜索并不能保证访问到所有顶点
3.2 广度优先搜索
BFS: Breadth First Search
类似于树的层序遍历,设初始化的时候,图中每个顶点均未被访问,从图中某一个顶点出发
(
设
V0
出发
)
,
访问
V0,
然后依次访问
V0
的各个邻接点
(
广度优先
)
。之后从这些被访问的顶点出发,仍按照广度优先搜索
的方法搜索下去,直到能够访问的顶点都访问完毕。
3.3 深度优先搜索和广度优先搜索生成树
4. 最短路径问题
解决带权有向图两个顶点之间最短路径问题的两个经典算法
:
Dijkstra(
迪杰斯特拉
)
算法
Floyd(
弗洛伊德
)
Dijkstra(
迪杰斯特拉
)
算法:
是解决从网中任一顶点
(
源点
)
出发,求它到其他各顶点
(
终点
)
的最短路径问
题。
Dijkstra(
迪杰斯特拉
)
算法的实现
Dijkstra(
迪杰斯特拉
)
算法需要用两个辅助数组:
1
、数组
S[n] (
最短路径是否求出的标志位数组
)
S[i] = 1
表示从源点
V
到该点
Vi
的最短路径已经求出来了
S[i] = 0
表示从源点
V
到该点
Vi
的最短路径没有求出来
初始化
S[V] = 1,
其他的
S[i] = 0 (0 <= i <= n-1, i
不等于
V)
2
、数组
dist[n]
dist[n]
存放源点
V
到
Vi
这个顶点的(当前的) 最短路径长度
初始化时
:
当
V
可以直接到达
Vi
,
dist[i] = <V, Vi>
的权
当
V
不可以直接到达
Vi, dist[i] =
无穷大
思路
:
显然
:
从源点
V
到其他各顶点的最短路径中第一短的,一定是能直接到的顶点
(
邻接点
)
里最短的
step1:
从源点
V
到其他各顶点的当前最短路径中找出最短的
dist[u] = min {dist[w] w = 0, 1, 2, ..... n-1},
且
S[W] = 0
dist[u]
称为当前最优路径,
u
对应的顶点作为当前最优顶点
step2:
用当前最优路径去更新其他的路径
对
S[W] = 0
的顶点
(
最短路径还没有求出来的顶点
)
如果
dist[u] + (u, W) < dist[W]
(
当前最优路径长度
+
当前最优顶点到
w
的权
< W
当前的最短路径长度
)
==> dist[W] = dist[u] + <u, w>
不断重复
step1, step2
图代码:
#include <stdio.h>
#include <stdlib.h>
#include "queue.h"
typedef char Vtype; //顶点的数据类型
typedef int Adjtype; //邻接矩阵的数据类型,即边或者弧的权值类型
#define MAXSIZE 100 //顶点数组的大小
#define WQ_MAX 65535 //用来在邻接矩阵中表示去不了的地方
typedef struct Graph
{
Vtype V[MAXSIZE]; //顶点的集合
Adjtype Adj[MAXSIZE][MAXSIZE]; //权的集合,邻接矩阵
int vexnum; //顶点的数量
int arcnum; //边或者弧的数量
}Graph;
/*
功能: 在有n个元素的数组V中,找到值为x的元素的下标
如果没有找到,则返回-1
参数:
V: 顶点数组
x: 需要查找的顶点元素
n: 数组的长度
返回值:
成功返回x在数组V中对应的下标
失败返回-1
*/
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()
{
//step1: 初始化一个图
Graph* graph = malloc(sizeof(Graph));
graph->vexnum = 0; //顶点数量默认为0
graph->arcnum = 0; //边的数量默认为0
//先把邻接矩阵中的所有元素初始化为无穷,默认所有顶点都不连通
for(int i = 0; i < MAXSIZE; i++)
{
for(int j = 0; j < MAXSIZE; j++)
{
graph->Adj[i][j] = WQ_MAX;
}
}
//step2: 获取图中的顶点元素
printf("请输入所有的顶点元素: \n");
int i = 0; //顶点数组的下标
while(1)
{
Vtype c;
c = getchar();
if(c == '#') //如果接受到'#'则认为输入完成
break;
graph->V[i] = c;
i++;
graph->vexnum++; //顶点元素个数加1
}
getchar(); //吸收上一次接受数据时最后敲下的回车键
//step3: 输入顶点之间的权值(如: ab15 表示a->b,权值为15)
printf("请输入顶点之间的权值: \n");
while(1)
{
Vtype s, t; //s:源头 , t:目的地
Adjtype w; //权值
scanf("%c%c%d", &s, &t, &w);
getchar(); //吸收回车
if(s == '#') //当接受到'#',认为输入完毕
break;
//查找源头和目的地对应的顶点在邻接矩阵中的下标
int si = find_index(graph->V , graph->vexnum, s);
int ti = find_index(graph->V , graph->vexnum, t);
if(si != -1 && ti != -1)
{
graph->Adj[si][ti] = w;
graph->arcnum++; //弧的数量自增
}
}
return graph;
}
/*
功能: 打印出邻接矩阵
参数:
graph: 图的地址
*/
void print_graph(Graph* graph)
{
//打印顶点元素数组
printf("顶点数组: \n\t");
for(int i = 0; i < graph->vexnum; i++)
{
printf("%c\t", graph->V[i]);
}
printf("\n");
//打印邻接矩阵
printf("邻接矩阵: \n");
for(int i = 0; i < graph->vexnum; i++) //行
{
printf("%c\t", graph->V[i]);
for(int j = 0; j < graph->vexnum; j++) //列
{
if(graph->Adj[i][j] == WQ_MAX) //无穷
{
printf("#\t");
}
else
{
printf("%d\t", graph->Adj[i][j]);
}
}
printf("\n");
}
}
/****************************深度优先搜索*********************************/
int visit[MAXSIZE] = {0}; //访问标志位数组,表示相应的顶点是否被访问过
//0表示未被访问,1表示已经访问过了
/*
功能:
获取图中第v个顶点的第一个邻接点
参数:
graph: 图的地址
v: 需要获取邻接点的那个顶点的下标
返回值:
成功返回第一个邻接点的下标
失败返回-1
*/
int first_adjVex(Graph* graph, int v)
{
for(int i = 0; i < graph->vexnum; i++)
{
if(graph->Adj[v][i] != WQ_MAX)
return i;
}
return -1;
}
/*
功能: 获取图中第v个顶点的邻接点后面的那个邻接点的下标
参数:
graph: 图的地址
v: 需要获取邻接点的那个顶点的下标
w: 已经获取到的邻接点的下标
返回值:
成功返回邻接点w后面的一个邻接点的下标
失败返回-1
*/
int next_adjVex(Graph* graph, int v, int w)
{
for(int i = w+1; i < graph->vexnum; i++)
{
if(graph->Adj[v][i] != WQ_MAX)
return i;
}
return -1;
}
/*
功能: 从图的第v个顶点出发,按照深度优先搜索的方式去访问图中的其他顶点
参数:
graph: 图的地址
v: 出发的顶点的下标
*/
void DFS(Graph* graph, int v)
{
//先访问当前顶点
visit[v] = 1;
printf("%c ", graph->V[v]);
//找v的第一个邻接点,如果该邻接点没有被访问,则按照深度优先搜索的规则去进行访问
for(int w = first_adjVex(graph, v); w != -1; w=next_adjVex(graph, v, w))
{
if(visit[w] == 0) //该邻接点没有被访问
{
DFS(graph, w);
}
}
}
/*
功能: 从图中每个没有被访问的顶点出发,进行一次深度优先搜索
参数:
graph: 图的地址
*/
void DFSTraverse(Graph* graph)
{
printf("DFS: ");
int i = 0;
for(i = 0; i < graph->vexnum; i++)
{
visit[i] = 0; //先把访问状态清0
}
for(i = graph->vexnum-1; i >= 0; i--)
{
//从下标为i的顶点出发进行深度优先搜索
if(visit[i] == 0) //没有被访问
{
DFS(graph, i);
}
}
}
/************************广度优先搜索*********************************/
/*
功能: 从图的第v个顶点出发,按照广度优先搜索的方式去访问图中的其他顶点
参数:
graph: 图的地址
v: 出发的顶点的下标
*/
void BFS(Graph* graph, int v)
{
visit[v] = 1; //先访问出发的顶点
printf("%c ", graph->V[v]);
//初始化一个空的队列
Queue* queue = queue_init();
//先把出发的顶点下标进行入队
push_queue(queue, v);
//队列不为空,则进行出队并进行顶点元素的访问
while(queue_empty(queue) == false)
{
int index = pop_queue(queue);
//访问index这个下标对应的顶点的邻接点
int w; //保存index对应的顶点的临界点的下标
for(w = first_adjVex(graph, index); w != -1; w = next_adjVex(graph, index, w))
{
if(visit[w] == 0) //如果没有被访问,则进行访问
{
visit[w] = 1;
printf("%c ", graph->V[w]);
push_queue(queue, w);
}
}
}
}
/*
功能: 从图中每个没有被访问的顶点出发,进行一次广度优先搜索
参数:
graph: 图的地址
*/
void BFSTraverse(Graph* graph)
{
printf("BFS: ");
int i = 0;
for(i = 0; i < graph->vexnum; i++)
{
visit[i] = 0; //先把访问状态清0
}
for(i = graph->vexnum-1; i >= 0; i--)
{
//从下标为i的顶点出发进行广度优先搜索
if(visit[i] == 0) //没有被访问
{
BFS(graph, i);
}
}
}
int main()
{
Graph* graph = create_graph();
print_graph(graph);
/*
printf("深度优先搜索: ");
DFS(graph, graph->vexnum-1);
printf("\n");
DFSTraverse(graph);
printf("\n");
*/
printf("广度优先搜索: ");
BFS(graph, graph->vexnum-1);
printf("\n");
BFSTraverse(graph);
printf("\n");
return 0;
}
迪杰斯特拉算法代码:
#include <stdio.h>
#include <stdlib.h>
typedef char Vtype; //顶点的数据类型
typedef int Adjtype; //邻接矩阵的数据类型,即边或者弧的权值类型
#define MAXSIZE 100 //顶点数组的大小
#define WQ_MAX 65535 //用来在邻接矩阵中表示去不了的地方
typedef struct Graph
{
Vtype V[MAXSIZE]; //顶点的集合
Adjtype Adj[MAXSIZE][MAXSIZE]; //权的集合,邻接矩阵
int vexnum; //顶点的数量
int arcnum; //边或者弧的数量
}Graph;
/*
功能: 在有n个元素的数组V中,找到值为x的元素的下标
如果没有找到,则返回-1
参数:
V: 顶点数组
x: 需要查找的顶点元素
n: 数组的长度
返回值:
成功返回x在数组V中对应的下标
失败返回-1
*/
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()
{
//step1: 初始化一个图
Graph* graph = malloc(sizeof(Graph));
graph->vexnum = 0; //顶点数量默认为0
graph->arcnum = 0; //边的数量默认为0
//先把邻接矩阵中的所有元素初始化为无穷,默认所有顶点都不连通
for(int i = 0; i < MAXSIZE; i++)
{
for(int j = 0; j < MAXSIZE; j++)
{
graph->Adj[i][j] = WQ_MAX;
}
}
//step2: 获取图中的顶点元素
printf("请输入所有的顶点元素: \n");
int i = 0; //顶点数组的下标
while(1)
{
Vtype c;
c = getchar();
if(c == '#') //如果接受到'#'则认为输入完成
break;
graph->V[i] = c;
i++;
graph->vexnum++; //顶点元素个数加1
}
getchar(); //吸收上一次接受数据时最后敲下的回车键
//step3: 输入顶点之间的权值(如: ab15 表示a->b,权值为15)
printf("请输入顶点之间的权值: \n");
while(1)
{
Vtype s, t; //s:源头 , t:目的地
Adjtype w; //权值
scanf("%c%c%d", &s, &t, &w);
getchar(); //吸收回车
if(s == '#') //当接受到'#',认为输入完毕
break;
//查找源头和目的地对应的顶点在邻接矩阵中的下标
int si = find_index(graph->V , graph->vexnum, s);
int ti = find_index(graph->V , graph->vexnum, t);
if(si != -1 && ti != -1)
{
graph->Adj[si][ti] = w;
graph->arcnum++; //弧的数量自增
}
}
return graph;
}
/*
功能: 打印出邻接矩阵
参数:
graph: 图的地址
*/
void print_graph(Graph* graph)
{
//打印顶点元素数组
printf("顶点数组: \n\t");
for(int i = 0; i < graph->vexnum; i++)
{
printf("%c\t", graph->V[i]);
}
printf("\n");
//打印邻接矩阵
printf("邻接矩阵: \n");
for(int i = 0; i < graph->vexnum; i++) //行
{
printf("%c\t", graph->V[i]);
for(int j = 0; j < graph->vexnum; j++) //列
{
if(graph->Adj[i][j] == WQ_MAX) //无穷
{
printf("#\t");
}
else
{
printf("%d\t", graph->Adj[i][j]);
}
}
printf("\n");
}
}
int S[MAXSIZE]; //最短路径是否求出的标志位数组, 如果求出则对应下标为1
int dist[MAXSIZE]; //存放源点到其他各顶点的最短路径长度
int path[MAXSIZE]; //保存顶点i所利用的上一个最优顶点
/*
功能: 用迪杰斯特拉算法求出从顶点V出发到其他各个顶点的最短路径长度
参数:
graph: 图的路径
v: 出发的顶点的下标
*/
void Dijkstra(Graph* graph, int v)
{
//step1: 把辅助数组初始化
for(int i = 0; i < graph->vexnum; i++)
{
S[i] = 0;
dist[i] = graph->Adj[v][i];
path[i] = v; //直接到的是没有利用最优顶点的
}
S[v] = 1; //自己到自己的最短路径认为求出来了
dist[v] = 0; //自己到自己的最短路径长度为0
int min; //用来求最优顶点的记录板(当前的最短路径)
int i_min; //当前最优顶点的下标
for(int times = 0; times < graph->vexnum-1; times++)
{
//step2:求出当前最短路径和最优顶点
//先假设最短路径还为求出的其中一个顶点的当前最短路径为最优路径
for(int i = 0; i < graph->vexnum; i++)
{
if(S[i] == 0) //i对应的顶点的最短路径未求出
{
min = dist[i];
i_min = i;
break;
}
}
//求出S[w] == 0 的顶点里最短路径,即最优顶点
for(int i = 0; i < graph->vexnum; i++)
{
if(S[i] == 0 && dist[i] < min)
{
min = dist[i];
i_min = i;
}
}
S[i_min] = 1; //V到i_min的最短路径求出来了,是min
//step3:用当前最短路径(最优顶点)去更新其他没有确定最短路径的顶点的最短路径
for(int i = 0; i < graph->vexnum; i++)
{
if(S[i] == 0)
{
if(dist[i_min] + graph->Adj[i_min][i] < dist[i])
//当前最优路径长度 + 当前最优顶点到w的权 < W当前的最短路径长度
{
dist[i] = dist[i_min] + graph->Adj[i_min][i]; //则dist[i]被更新
path[i] = i_min;
}
}
}
}
}
int main()
{
Graph* graph = create_graph();
print_graph(graph);
Dijkstra(graph, 0);
// printf("%c -> %c : %d\n", graph->V[6], graph->V[0], dist[6]);
// printf("%c -> %c : %d\n", graph->V[3], graph->V[0], dist[3]);
for(int i = 0; i < graph->vexnum; i++)
{
if(dist[i] < WQ_MAX)
{
printf("%c -> %c : %d\t %c ", graph->V[i], graph->V[0], dist[i], graph->V[i]);
int pre_index = path[i]; //获取更新i的最短路径时的那个最优顶点的下标
while(1)
{
printf("%c ", graph->V[pre_index]);
if(pre_index == 0) //已经到达了源点
{
break;
}
pre_index = path[pre_index];
}
}
else
{
printf("%c -> %c : none\n", graph->V[0], graph->V[i]);
}
printf("\n");
}
return 0;
}