数据结构——图(Graph)
目录
1.图的基本概念
2.图的基本代码操作
3.图的进阶操作和应用
1.1 图的定义
任何一个图都是由顶点和边构成的。我们采用集合的思想,可以把任何一个图用两个集合表示出来:记作
G
=
(
V
,
E
)
G = (V,E)
G=(V,E),其中V
是表示顶点的有限集合,E
为边的有限集合。
允许E
为空集,此时表示的含义是:图G只有顶点但是不存在边。
1.2 图的相关术语
1. 有向图和无向图
我们举一个例子来理解:
题目描述:
某省政府进行"畅通工程",现在给出每两个城市之间的道路和道路长度,问从给定的a城市出发,最短需要走多长才能到达b城市,题目保证a,b之间存在路径。
只看题目的话,我们就能发现,这里的路径是一个无向路径,因为a和b之间存在一条路,那么必然可以同时实现a -> b
和b -> a
,所以我们会得到一个无向图。
有向图的概念自然就明了了,对于任何一条路径他的方向是确定的,只存在a -> b
或者b -> a
.
2.度、入度、出度
对于无向图,每一个顶点的度就可以理解为它所连接的边数。对于有向图,由于点与点之间存在了方向,所以我们定义:入度是以该顶点为终点的边的条数,出度则是以该顶点为起点的边的条数。该顶点的度为入度和出度之和。比如下图中,顶点4的入度为1,出度为2.
3.路径和路径长度
在一个图G
中,从顶点i到顶点j的路径是一个顶点序列
i
=
i
0
,
i
1
,
.
.
.
.
,
i
m
=
j
i = i_0,i_1,....,i_m = j
i=i0,i1,....,im=j路径长度是指一条路径上经过的边的数目。 比如这里的从顶点1到3的路径长度为2,经过了1 -> 2
和2 -> 3
。
2.0 图的基本操作
分为这几种:
- 建图(建立存储结构)
- 销毁图
- 输出图
- 求顶点的度
2.1 建立一个图
图的建立其实就是存图。主要有两种存储结构,即邻接矩阵和邻接表。
1. 邻接矩阵存图
邻接矩阵表示的是节点之间的相邻关系,对于一个不带权的矩阵我们有:
A
[
i
]
[
j
]
=
1
,
i
≠
j
A[i][j] = {1,i \neq j}
A[i][j]=1,i=j
A
[
i
]
[
j
]
=
0
,
i
=
j
A[i][j] = 0,i = j
A[i][j]=0,i=j
对于有权图只需要在第一步里更改权值。
图的邻接矩阵对于查询两个顶点i,j之间是否有边是非常方便的。
看图
现在给出代码
前置代码:
#define MaxSize 1e3 + 10
#define INF 0x3f3f3f3f
typedef char VertexType[10];
typedef struct vertex
{
int adjvex; //顶点编号
VertexType data; //顶点的数据信息
} VType; //顶点类型
typedef struct graph
{
int e,n; //n为点数,e为边数
VType vexs[MaxSize]; //顶点集合
int edge[MaxSize][MaxSize]; //边的集合
} MatGraph; //图的邻接矩阵类型
- 建立图的邻接矩阵算法
void CreatGraph(MatGraph &g,int A[][MaxSize],int n,int e)
{
g.n = n;g.e = e;
for(int i = 0;i < n;i++){
for(int j = 0;j < n;j++){
g.edges[i][j] = A[i][j];
}
}
}
- 销毁图算法
邻接矩阵存图的空间是系统自己分配的,系统自动分配释放空间,所以在销毁图中不需要进行额外操作。
void DestoryGraph(MatGraph g)
{
}
- 输出图算法
void DisGraph(MatGraph g)
{
for(int i = 0;i < n;i++){
for(int j = 0;j < n;j++){
if(g.edges[i][j] < INF) printf("%4d",g.edges[i][j]);
else printf("%4s","∞")
}
printf("\n");
}
}
INF
是一个很大的数,我们一般认为是0x3f3f3f3f
,以后常用它来表示无穷。
- 求顶点度的算法
对于有向图和无向图,有两种不同的算法。
对于无向图:没有入度出度的说法,所以直接求总度数即可。
int Degreel(MatGraph g,int v)
{
if(v < 0 || v >= g.n) return -1; //顶点编号错误
for(int i = 0;i < g.n;i++){
if(g.edges[v][i] > 0 && g.edges[v][i] < INF) d++;
}
return d;
}
对于有向图而言,度就存在一定的区别了,对于一个点,既有出度也有入度,所以一层循环显然是解决不到的。
int Degree2(MatGraph g,int v)
{
int d1 = 0,d2 = 0,d;
if(v < 0 || v > g.n) return -1;
for(int i = 0;i < g.n;i++){ //计算出度
if(g.edges[v][i] > 0 && g.edges[v][i] < INF) d1 ++;
}
for(int i = 0;i < g.n;i++){ //计算入度
if(g.edges[i][v] > 0 && g.edges[i][v] < INF) d2 ++;
}
d = d1 + d2;
return d;
}
2.邻接表存图
邻接表的存储思路为:我们希望把图中的每一个顶点作为一个独立的头节点,然后把和这个点相邻的点全部链起来,每一个节点对应的就是一条边。
其实邻接表的存储就是:把每一条边的关系用相邻点的链接表示,每一个节点都存在三个域:权值域、指针域、顶点域。分别存放需要的各种信息。
上图:
对应的邻接表存储:
可以直观看出,邻接矩阵更容易理解,而在实际应用中,邻接表的另一种非指针表达:链式前向星更为常用,这里不再给出描述,有兴趣的同学移步CSDN(笔者也没懂),所以这里只给出邻接表的代码实现。后面的应用我们主要使用邻接矩阵。
- 邻接表建图
首先给出邻接表的声明
typedef char VertexType[10]; //定义一个字符串类型,其实用C++的string 是一样的
typedef struct edgenode //建立边节点
{
int adjext;
int weight;
struct edgenode * nectarc;
} ArcNode;
typedef struct vexnode //建立头节点
{
VertexType data;
ArcNode * firstarc; //头节点指向的第一条边
} VHeadNode;
typedef struct
{
int n,e;
VHeadNode adjlist[Maxsize]; //建立头节点数组
} AdjGraph;
- 建立邻接表算法
void CreatGraph(AdjGraph * & G,int A[][Maxsize],int n,int e)
{
ArcNode * p;
G = (AdjGraph * )malloc(sizeof(AdjGraph));
G -> n = n;
G -> e = e;
for(int i = 0;i < n;i++) G -> adjlist[i].firstace = NULL;
for(int i = 0;i < G -> n;i++){
for(int j = G -> n - 1;j >= 0;j--){
if(A[i][j] > 0 && A[i][j] < INF){
p = (ArcNode * )malloc(sizeof(ArcNode));
p -> adjvex = j;
p -> weight = A[i][j];
p -> nextarc = G -> adjlist[i].firstarc;
G -> adjlist[i].firstarc = p;
}
}
}
}
- 销毁图算法
由于分配链表空间时,我们申请了动态存储空间,所以程序结束时需要手动释放!
void DestoryGraph(AdjGraph * & G)
{
ArcNode * pre,* p;
for(int i = 0;i < G -> n;i++){
pre = G -> adjlist[i].firstarc;
if(pre != NULL){
p = pre -> nextarc;
while(p != NULL){
free(pre);
pre = p;
p = p -> nextarc;
}
free(pre);
}
}
free(G);
}
- 输出图的算法
void DisplayGraph(AdjGraph * G)
{
ArcNode * p;
for(int i = 0;i < G -> n;i++){
printf(" [%2d]",i);
p = G -> adjlist[i].firstarc;
if(p != NULL) printf("->");
while(p != NULL){
printf(" %d%(d"),p -> adjvex,p -> weight);
p = p -> nextarc;
}
printf("\n");
}
}
- 求顶点度算法
同样对于有向图和无向图有两种写法,下面给出无向图的写法:
int Degreel(AdjGraph * G,int v)
{
int d = 0;
ArcNode * p;
if(v < 0 || v >= G.n) return -1;
p = G -> adjlist[i].firstarc;
while(p != NULL){
d++;
p = p -> nextarc;
}
return d;
}
对于有向图,和邻接矩阵的思路类似,分别统计入度和出度。
int Dgreel(AdjGraph * g,int v)
{
int d1 = 0,d2 = 0,d;
ArcNode * p;
if(v < 0 || v >= G -> n) return -1;
p = G -> adjlist[i].firstarc;
while(p != NULL){
d1 ++;
p = p -> nextarc;
}
for(int i = 0;i < G -> n;i++){
p = G -> adjlist[i].firstarc;
while(p != NULL){
if(p -> adjvex == v) d2++;
p = p -> nextarc;
}
}
d = d1 + d2;
return d;
}
题目的讲解和相关算法的描述在我的录课中有比较全的讲解,大家可以移步bilibili