秋风阁——北溪入江流:https://focus-wind.com/
秋风阁——数据结构-图
文章目录
图(Graph)的基本概念
图(Graph)是一种比线性表和树更为复杂的非线性(多对多)数据结构。图中元素的关系是任意的,每个元素(也称为顶点)具有多个直接前驱和后继,所以图可以表达数据元素之间广泛存在着的更加复杂的关系。
图(Graph):图 G G G由顶点集 V ( G ) V(G) V(G)和边集 E ( G ) E(G) E(G)组成,形式上记为 G = ( V , E ) G = (V, E) G=(V,E)。其中, V ( G ) V(G) V(G)是**顶点(Vertex)的非空有限集合; E ( G ) E(G) E(G)是 V ( G ) V(G) V(G)中任意两个顶点之间的关系集合,又称为边(Edge)**的有限集合。
图的基本术语
-
有向图:当图G中的每条边有方向时,则称G为有向图。
- 有向边(弧) 有向边通常用两个顶点组成的有序对来表示,又称为弧
- 弧尾:弧的起始结点称为弧尾
- 弧头:弧的终止结点称为弧头
- 一个有向图,其顶点数 v v v和边数 e e e满足 0 ≤ e ≤ v ( v − 1 ) 0 \le e \le v(v - 1) 0≤e≤v(v−1)
- 完全有向图:如果 e = v ( v − 1 ) e = v(v - 1) e=v(v−1)时,则称该有向图为完全有向图
- 有向边(弧) 有向边通常用两个顶点组成的有序对来表示,又称为弧
-
无向图:图G中每条边是无方向的,称为无向图。无向图的两个顶点之间最多只存在一条边。
- 一个无向图,其顶点数 v v v和边数 e e e满足 0 ≤ e ≤ v ( v − 1 ) 2 0 \le e \le \frac{v(v - 1)}{2} 0≤e≤2v(v−1)
- 完全无向图:如果 e = v ( v − 1 ) 2 e = \frac{v(v - 1)}{2} e=2v(v−1)时,则称该无向图为完全无向图
-
稀疏图与稠密图:若图G的顶点与边的关系满足 e < v l o g 2 v e < vlog_2v e<vlog2v,则该图为稀疏图,否则为稠密图。
-
顶点的度(degree):顶点的度是指依附于某顶点v的边数,通常记作 T D ( v ) TD(v) TD(v)
- 入度:顶点v的入度是指以顶点为终点的弧的数目,记作 I D ( v ) ID(v) ID(v)
- 出度:顶点v的出度是指以顶点为起点的弧的数目,记作 O D ( v ) OD(v) OD(v)
- T D ( v ) = I D ( v ) + O D ( v ) TD(v) = ID(v) + OD(v) TD(v)=ID(v)+OD(v)
-
边的权、网图:
- 权(weight):与边有关的数据信息称为权,在实际应用中,权值可以有某种含义。
- 网图或网络(network):边上带权的图称为网图或网络。
- 有向带权图:如果边是有方向的带权图,称为有向带权图。
-
路径(path):若从顶点 v 1 v_1 v1出发,沿着一些边经过顶点 v 1 , v 2 , ⋯ , v n − 1 v_1, v_2, \cdots, v_{n - 1} v1,v2,⋯,vn−1到达顶点 v n v_n vn,则称顶点序列 ( v 1 , v 2 , ⋯ , v n − 1 , v n ) (v_1, v_2, \cdots, v_{n - 1}, v_n) (v1,v2,⋯,vn−1,vn)为从 v 1 v_1 v1到 v n v_n vn的一条路径。
-
路径长度:沿路径经过的边数称为该路径的长度。
-
简单路径:若路径中的顶点不重复出现,则称这条路径为简单路径。
-
简单回路(简单环):起点和终点相同并且路径长度不小于2的简单路径,被称为简单路径或简单环。
-
有根图:在一个有向图中,若存在一个顶点v,从该顶点可到达图中其他所有的顶点,则称这个有向图为有根图,v称为该图的根。
-
子图:对于图 G = ( V , E ) , G ′ = ( V ′ , E ′ ) G = (V, E), G' = (V', E') G=(V,E),G′=(V′,E′),若存在 V ′ ∈ V , E ′ ∈ E V' \in V, E' \in E V′∈V,E′∈E,则称图 G ′ G' G′是图 G G G的一个子图。
-
连通:
- 无向图:
- 连通:在无向图G中,如果顶点 v i v_i vi和 v j ( i ≠ j ) v_j(i \ne j) vj(i=j)存在路径,则称 v i v_i vi和 v j v_j vj是连通的
- 连通图:如果V(G)中任意两个顶点都连通,则称G为连通图,否则为非连通图。
- 连通分量:无向图G中的极大连通子图称为连通分量。对任何连通图而言,连通分量就是其自身;非连通图可有多个连通分量。
- 有向图:
- 强连通:在有向图G中,如果顶点 v i v_i vi和 v j ( i ≠ j ) v_j(i \ne j) vj(i=j)存在路径,则称 v i v_i vi和 v j v_j vj是强连通的
- 强连通图:如果V(G)中任意两个顶点都强连通,则称G为强连通图,否则为非连通图。
- 强连通分量:有向图G中的极大强连通子图称为强连通分量。
- 无向图:
图的存储方法
邻接矩阵(边多)
邻接矩阵是指用一个一维数组存储图中的顶点信息,用一个二维数组存储图中的边信息(即各顶点之间的邻接关系),存储顶点之间邻接关系的二维数组称为邻接矩阵。
结点数为n的图
G
=
(
V
,
E
)
G = (V, E)
G=(V,E)的邻接矩阵
A
A
A是
n
×
n
n \times n
n×n的。将G的顶点编号为
v
1
,
v
2
,
⋯
,
v
n
v_1, v_2, \cdots, v_n
v1,v2,⋯,vn。若
(
v
i
,
v
j
)
∈
E
(v_i, v_j) \in E
(vi,vj)∈E,则
A
[
i
]
[
j
]
=
1
A[i][j] = 1
A[i][j]=1,否则
A
[
i
]
[
j
]
=
0
A[i][j] = 0
A[i][j]=0
A
[
i
]
[
j
]
=
{
1
若
(
v
i
,
v
j
)
或
<
v
i
,
v
j
>
∈
E
(
G
)
0
若
(
v
i
,
v
j
)
或
<
v
i
,
v
j
>
∉
E
(
G
)
\begin{equation} A[i][j] = \left\{ \begin{array}{rr} 1 & 若(v_i, v_j)或<v_i, v_j> \in E(G) \\ 0 & 若(v_i, v_j)或<v_i, v_j> \notin E(G) \end{array} \right. \end{equation}
A[i][j]={10若(vi,vj)或<vi,vj>∈E(G)若(vi,vj)或<vi,vj>∈/E(G)
对于带权图,若顶点
v
i
v_i
vi和
v
j
v_j
vj之间有边相连,则邻接矩阵中对应项存放着该边对应的权值,对于不相连的顶点,则用
∞
\infty
∞代表两个顶点之间不存在边。
A
[
i
]
[
j
]
=
{
w
i
j
若
(
v
i
,
v
j
)
或
<
v
i
,
v
j
>
∈
E
(
G
)
0
或
∞
若
(
v
i
,
v
j
)
或
<
v
i
,
v
j
>
∉
E
(
G
)
\begin{equation} A[i][j] = \left\{ \begin{array}{rr} w_{ij} & 若(v_i, v_j)或<v_i, v_j> \in E(G) \\ 0或\infty & 若(v_i, v_j)或<v_i, v_j> \notin E(G) \end{array} \right. \end{equation}
A[i][j]={wij0或∞若(vi,vj)或<vi,vj>∈E(G)若(vi,vj)或<vi,vj>∈/E(G)
上图的两个图对应的邻接矩阵分别为:
A
1
=
[
0
1
1
0
0
0
0
0
0
0
0
1
1
0
0
0
]
,
A
2
=
[
0
1
1
1
1
0
0
0
1
0
0
1
1
0
1
0
]
A_1 = \begin{bmatrix} 0 & 1 & 1 & 0 \\ 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 \\ 1 & 0 & 0 & 0 \end{bmatrix}, A_2 = \begin{bmatrix} 0 & 1 & 1 & 1 \\ 1 & 0 & 0 & 0 \\ 1 & 0 & 0 & 1 \\ 1 & 0 & 1 & 0 \end{bmatrix}
A1=⎣
⎡0001100010000010⎦
⎤,A2=⎣
⎡0111100010011010⎦
⎤
邻接矩阵的特点
根据图的邻接矩阵的存储方法,易知图有以下特点:
- 无向图的邻接矩阵一定是一个对称矩阵。因此,在具体存放无向图邻接矩阵时只需考虑存放上(下)三角矩阵的元素即可。
- 对于无向图,邻接矩阵第 i i i行(或第 i i i列)非零元素(或非 ∞ \infty ∞元素)的个数正好是第 i i i个顶点的度 T D ( v i ) TD(v_i) TD(vi)
- 对于有向图,邻接矩阵第 i i i行(或第 i i i列)非零元素(或非 ∞ \infty ∞元素)的个数正好是第 i i i个顶点的出度 O D ( v i ) OD(v_i) OD(vi)(或入度 I D ( v i ) ID(v_i) ID(vi))
- 用邻接矩阵方式存储图,很容易确定图中任意两个元素之间是否相连;但是,要确定图中有多少条边,则必须按行、按列对每个元素进行检测,所花费的实际代价较大。
邻接矩阵结点类型描述
// 最大顶多数
#define MAX_VERTEX_NUM 20
// VertexType:顶点类型,EdgeType:边类型
typedef char VertexType;
typedef int EdgeType;
typedef struct {
VertexType vertex[MAX_VERTEX_NUM];
EdgeType edges[MAX_VERTEX_NUM][MAX_VERTEX_NUM];
} MatrixGraph;
创建邻接矩阵
/**
* 创建无向图的邻接矩阵
* @param graph
* @param edgesNum 无向图边的个数
*/
void CreateUndirectedMatrixGraph(MatrixGraph *graph, int edgesNum) {
int i, j;
float weight;
// 读入无向图顶点信息
for (i = 0; i < MAX_VERTEX_NUM; ++i) {
printf("Please enter the %dth vertex information:", i + 1);
graph->vertex[i] = getchar();
printf("\n");
// 邻接矩阵初始化
for (j = 0; j < MAX_VERTEX_NUM; ++j) {
graph->edges[i][j] = 0;
}
}
// 读入边的权值信息
for (int k = 0; k < MAX_VERTEX_NUM; ++k) {
printf("Please enter the weight information "
"of vertices I to j (starting from subscript 0, "
"enter i, j, weight, separated by Spaces):");
scanf("%d %d %f", &i, &j, &weight);
printf("\n");
graph->edges[i][j] = weight;
graph->edges[j][i] = weight;
}
}
上述算法的时间复杂度为 O ( n + n 2 + 3 ) O(n + n^2 + 3) O(n+n2+3),通常情况下 e ≪ n 2 e \ll n^2 e≪n2,所以该算法的时间复杂度为 O ( n 2 ) O(n^2) O(n2)
邻接表(顶点多)
邻接表存储方法是一种顺序存储与链式存储相结合的存储方法。在这种方法中,只考虑非零元素。所以当图中的顶点很多而边很少时,可以有效的节省存储空间。
邻接表存储结构由顶点表和邻接链表两部分组成。
对于每个顶点
v
i
v_i
vi,使用一个具有两个域的结构体数组来存储,这个数组称为顶点表。其中,一个域称为顶点域(Vertex),用来存放顶点本身的数据信息;另一个域称为指针域,用来存放依附于该顶点的边所组成的单链表的表头结点的存储位置。
邻接于
v
i
v_i
vi的顶点
v
j
v_j
vj链接成的单链表称为
v
i
v_i
vi的邻接链表。邻接链表中每个结点由两个域构成,一个是邻接点域(Adjvex),用来存放与
v
i
v_i
vi相邻接的顶点
v
j
v_j
vj的序号
j
j
j(可以是顶点
v
j
v_j
vj在顶点表中所占数组单元的下标);二是链域(Next),用来将邻接链表中的结点链接在一起。
对于无向图,
v
i
v_i
vi的每个结点都对应与
v
i
v_i
vi相关联的一条边,所以无向图的邻接链表又称为边表;对于有向图,
v
i
v_i
vi的邻接链表中每个结点都对应于以
v
i
v_i
vi为起点射出去的一条边,所以有向图的邻接链表又称为出边表。
无向图和有向图的示例分别如下图所示:
邻接表的特点
- 若G为无向图,则需存储空间为 O ( ∣ V ∣ + 2 ∣ E ∣ ) O(|V| + 2|E|) O(∣V∣+2∣E∣);若G为有向图,则需存储空间为 O ( ∣ V ∣ + ∣ E ∣ ) O(|V| + |E|) O(∣V∣+∣E∣)。前者的倍数2是因为在无向图中,每条边在邻接表中出现了两次。
- 对于稀疏图,采用邻接表表示有利于节省存储空间。
- 在邻接表中,给定一顶点,很容易找出它的所有邻边,只需要扫描一遍它的邻接表,花费时间为 O ( n ) O(n) O(n)。但是,要确定两个顶点之间是否有邻边,在邻接矩阵中可以在常数时间内查找到,在邻接表中,需要扫描一遍邻接表查找对应结点。
邻接表结点类型描述
邻接链表:
// 邻接链表
typedef struct Node{
// 邻接点域:该弧指向的顶点的位置
int adjvex;
// 链域:指向下一条弧指针
struct Node* next;
} AdjacencyNode;
顶点表:
// 顶点表
typedef struct {
// 顶点域:顶点结点信息
VertexType vertex;
// 邻接链表:指向第一条依附于该顶点的弧的指针
AdjacencyNode *first;
} VertexNode;
创建邻接表
/**
* 创建无向图的邻接表
* @param graph
* @param vertexNum
* @param edgeNum
*/
void CreateUndirectedAdjacencyGraph(VertexNode graph[], int vertexNum, int edgeNum) {
int i, j;
AdjacencyNode *s;
// 顶点表初始化
for (i = 0; i < vertexNum; ++i) {
printf("Please enter the %dth vertex information:", i + 1);
graph[i].vertex = getchar();
graph[i].first = NULL;
printf("\n");
}
// 建立边表
for (int k = 0; k < edgeNum; ++k) {
printf("Please enter the values of vertices I to j "
"(starting with subscript 0, enter I, j, separated by Spaces):");
scanf("%d %d", &i, &j);
// 建立i到j的邻接表信息
s = (AdjacencyNode *) malloc(sizeof(AdjacencyNode));
s->adjvex = j;
s->next = graph[i].first;
graph[i].first = s;
// 邻接表无向图是对称的,建立从j到i的邻接表信息
s = (AdjacencyNode *) malloc(sizeof(AdjacencyNode));
s->adjvex = i;
s->next = graph[j].first;
graph[j].first = s;
printf("\n");
}
}
上述算法的时间复杂度为: O ( n + e ) O(n + e) O(n+e)
图的遍历
图在遍历过程中,任意一个结点都有可能与其他顶点相邻接,所以为了避免对同一顶点的重复访问,需要使用一个辅助数组 v i s i t e d [ n ] visited[n] visited[n]( n n n为顶点数)来对顶点进行标识,如果顶点 i i i被访问,则 v i s i t e d [ i ] visited[i] visited[i]置1,否则保持0。
深度优先搜索遍历(DFS)
图的深度优先搜索遍历(DFS)类似于树的先序遍历,是树先序遍历的推广。
图的深度优先搜索遍历过程是:从图中某一顶点
v
I
v_I
vI出发,访问此结点,并进行标记;然后依次搜索
v
i
v_i
vi的每个邻接点
v
j
v_j
vj,若
v
j
v_j
vj未被访问过,则对
v
j
v_j
vj进行访问和标记;然后依次搜索
v
j
v_j
vj的每个邻接点,若
v
j
v_j
vj的邻接点未被访问过,则访问
v
j
v_j
vj的邻接点,并进行标记;
⋯
\cdots
⋯直到图中和
v
i
v_i
vi的所有都被访问为止。若在非连通的情况下,图中还有另一顶点未被访问,则另选图中的另一个未被访问的结点作为出发点,重复上述过程,直到图中所有结点都被访问为止。
深度优先遍历得到的顶点序列称为该图的深度优先搜索遍历序列,又称为DFS序列。
邻接矩阵的深度优先搜索遍历算法
对于邻接矩阵的DFS序列,若确定其出发点,邻接点的序号按次序排序进行选择,则邻接矩阵作为存储结构得到的DFS序列是唯一的。
/**
* 邻接矩阵的深度优先搜索具体算法
* @param graph 邻接矩阵
* @param v 顶点在邻接矩阵顶点信息表中的数组下标值
* @param visited 访问标记数组
*/
void _DFS(MatrixGraph graph, int v, int visited[]) {
// 访问出发点
printf("%c ", graph.vertex[v]);
// 标记已访问的出发点
visited[v] = 1;
// 搜索v的邻接点
for (int j = 0; j < MAX_VERTEX_NUM; ++j) {
// 当邻接点vj未被访问,从vj开始进行新一轮的深度优先搜索
if ((0 != graph.edges[v][j]) && (0 == visited[j])) {
_DFS(graph, j, visited);
}
}
}
/**
* 深度优先搜索包装算法,保证非连通图也能完全访问
* @param graph
* @param v
*/
void DFS(MatrixGraph graph, int v) {
int visited[MAX_VERTEX_NUM];
for (int i = 0; i < MAX_VERTEX_NUM; ++i) {
visited[i] = 0;
}
// 从0开始遍历,保证所有结点都能被访问到
for (int i = 0; i < MAX_VERTEX_NUM; ++i) {
if (0 != visited[i]) {
_DFS(graph, v, visited);
}
}
}
上述算法的时间复杂度为: O ( n 2 ) O(n^2) O(n2),空间复杂度为: O ( n ) O(n) O(n)
邻接表的深度优先搜索遍历算法
邻接表的DFS序列不唯一,具体取决于邻接表中边表结点的链接次序
/**
* 邻接表的深度优先搜索具体算法
* @param graph 邻接表
* @param v 顶点在邻接表顶点信息表中的数组下标值
* @param visited 访问标记数组
*/
void _DFS(VertexNode graph[], int v, int visited[]) {
AdjacencyNode *p;
// 访问出发点
printf("%c ", graph[v].vertex);
// 标记已访问的出发点
visited[v] = 1;
p = graph[v].first;
// 搜索v的邻接点
while (NULL != p) {
if (0 == visited[p->adjvex]) {
_DFS(graph, p->adjvex, visited);
}
p = p->next;
}
}
/**
* 深度优先搜索包装算法,保证非连通图也能完全访问
* @param graph
* @param v
*/
void DFS(VertexNode graph[], int v) {
int visited[MAX_VERTEX_NUM];
for (int i = 0; i < MAX_VERTEX_NUM; ++i) {
visited[i] = 0;
}
// 从0开始遍历,保证所有结点都能被访问到
for (int i = 0; i < MAX_VERTEX_NUM; ++i) {
if (0 != visited[i]) {
_DFS(graph, v, visited);
}
}
}
上述算法的时间复杂度为: O ( 2 e + n ) O(2e + n) O(2e+n),空间复杂度为: O ( n ) O(n) O(n)
广度优先搜索遍历(BFS)
图的广度优先搜索遍历(BFS)类似于树的层次遍历。
图的广度优先搜索遍历过程是:从图中某一结点
v
i
v_i
vi出发,访问
v
i
v_i
vi;然后依次访问
v
i
v_i
vi的邻接点
v
j
v_j
vj;在所有
v
j
v_j
vj都被访问后,再依次访问
v
j
v_j
vj的邻接点
v
k
v_k
vk,
⋯
\cdots
⋯直到图中所有和
v
i
v_i
vi路径连通的顶点都被访问为止。若在非连通的情况下,图中还有另一顶点未被访问,则另选图中的另一个未被访问的结点作为出发点,重复上述过程,直到图中所有结点都被访问为止。
在图的广度优先搜索遍历过程中,先被访问的顶点,其邻接点也先被访问,具有先进先出的特性,所以可以使用一个队列来保存已访问过的顶点。
邻接矩阵的广度优先搜索遍历算法
对于邻接矩阵的BFS序列,若确定其出发点,邻接点的序号按次序排序进行选择,则邻接矩阵作为存储结构得到的DFS序列是唯一的。
/**
* 零件矩阵的广度优先搜索具体算法
* @param graph 邻接矩阵
* @param v 顶点在邻接矩阵顶点信息表中的数组下标值
* @param visited 访问标记数组
*/
void _BFS(MatrixGraph graph, int v, int visited[]) {
DataType data;
// 辅助队列
SequeenQueue queue;
// 队列初始化
InitQueue(&queue);
// 访问出发点
printf("%c ", graph.vertex[v]);
// 标记出发点
visited[v] = 1;
// 已访问结点入队
EnQueue(&queue, v);
while (!IsQueueEmpty(&queue)) {
// 队头元素出队
DeQueue(&queue, &data);
// 访问其邻接点
for (int j = 0; j < MAX_VERTEX_NUM; ++j) {
if ((0 != graph.edges[data][j]) && (0 == visited[j])) {
printf("%c ", graph.vertex[j]);
visited[j] = 1;
EnQueue(&queue, j);
}
}
}
}
/**
* 广度优先搜索包装算法,保证非连通图也能完全访问
* @param graph
* @param v
*/
void BFS(MatrixGraph graph, int v) {
int visited[MAX_VERTEX_NUM];
for (int i = 0; i < MAX_VERTEX_NUM; ++i) {
visited[i] = 0;
}
// 从0开始遍历,保证所有结点都能被访问到
for (int i = 0; i < MAX_VERTEX_NUM; ++i) {
if (0 != visited[i]) {
_BFS(graph, v, visited);
}
}
}
上述算法的时间复杂度为: O ( n 2 ) O(n^2) O(n2),空间复杂度为: O ( n ) O(n) O(n)
邻接表的广度优先搜索遍历算法
邻接表的BFS序列不唯一,具体取决于邻接表中边表结点的链接次序
/**
* 邻接表的广度优先搜索具体算法
* @param graph 邻接表
* @param v 顶点在邻接表顶点信息表中的数组下标值
* @param visited 访问标记数组
*/
void _BFS(VertexNode graph[], int v, int visited[]) {
AdjacencyNode *p;
DataType data;
// 辅助队列
SequeenQueue queue;
// 队列初始化
InitQueue(&queue);
// 访问出发点
printf("%c ", graph[v].vertex);
// 标记出发点
visited[v] = 1;
// 已访问结点入队
EnQueue(&queue, v);
while (!IsQueueEmpty(&queue)) {
// 队头元素出队
DeQueue(&queue, &data);
// 依次访问其邻接链表
p = graph[data].first;
while (NULL != p) {
if (0 == visited[p->adjvex]) {
printf("%c ", graph[v].vertex);
visited[p->adjvex] = 1;
EnQueue(&queue, p->adjvex);
}
p = p->next;
}
}
}
/**
* 广度优先搜索包装算法,保证非连通图也能完全访问
* @param graph
* @param v
*/
void BFS(VertexNode graph[], int v) {
int visited[MAX_VERTEX_NUM];
for (int i = 0; i < MAX_VERTEX_NUM; ++i) {
visited[i] = 0;
}
// 从0开始遍历,保证所有结点都能被访问到
for (int i = 0; i < MAX_VERTEX_NUM; ++i) {
if (0 != visited[i]) {
_DFS(graph, v, visited);
}
}
}
上述算法的时间复杂度为: O ( 2 e + n ) O(2e + n) O(2e+n),空间复杂度为: O ( n ) O(n) O(n)