✨✨ 欢迎大家来到贝蒂大讲堂✨✨🎈🎈养成好习惯,先赞后看哦~🎈🎈
所属专栏:数据结构与算法
贝蒂的主页:Betty’s blog
1. 图的定义
**图(Graph)**是数学和计算机科学中的一种基本结构,它由两个主要组成部分定义:顶点集(Vertex Set)V 和边集(Edge Set)E。在一个图
G
=
(
V
,
E
)
G=(V,E)
G=(V,E)中,V
是顶点的有限非空集合,而 E
是连接这些顶点之间的边的集合,即每条边都是两个顶点之间的关系。
- 顶点(Vertices):顶点可以由字母变量表示,比如 V = { v 1 , v 2 , … , v n } V=\{v_1,v_2,\ldots,v_n\} V={v1,v2,…,vn}, n n n 表示顶点的数量,也称为图的阶或维度。
- 边(Edges):边是表示顶点间关系的元素,通常表示为 ( u , v ) (u,v) (u,v),这意味着从顶点 u u u 到顶点 v v v 有一条边。 E E E 的大小,即 ∣ E ∣ |E| ∣E∣,给出了图中边的数量。
注意:线性表有空表,树有空树,但图没有空图。也就是说图不能一个顶点没有,但是边可以为空。
2. 图的基本概念
2.1 有向图与无向图
- 在有向图中, < x , y > <x,y> <x,y>是有序的,被称为顶点x到顶点y的一条边, < x , y > <x,y> <x,y>和 < y , x > <y,x> <y,x>是两条不同的边。
- 在无向图中, ( x , y ) (x,y) (x,y)是无序的,称为顶点x和顶点y相关联的一条边,这条边没有特定方向, ( x , y ) (x,y) (x,y)和 ( y , x ) (y,x) (y,x)是同一条边。
2.2 完全图
在有
n
个顶点的无向图中,若有 n × ( n − 1 ) ÷ 2 n × ( n − 1 ) ÷ 2 n×(n−1)÷2条边,即任意两个顶点之间都有直接相连的边,则称此图为无向完全图。
在有n
个顶点的有向图中,若有 n × ( n − 1 ) n × ( n − 1 ) n×(n−1)条边,即任意两个顶点之间都有双向的边,则称此图为有向完全图。
2.3 邻接顶点
- 在无向图中,若边 ( u , v ) (u, v) (u,v)存在于图中,则称 u u u和 v v v互为邻接顶点,并且边 ( u , v ) (u, v) (u,v)依附于顶点 u u u和顶点 v v v。
- 在有向图中,若边 < u , v > <u, v> <u,v>是图中的一条边,则称顶点 u u u邻接到顶点 v v v,顶点 v v v邻接自顶点 u u u,同时称边 < u , v > <u, v> <u,v>与顶点 u u u和顶点 v v v相关联。
2.4 顶点的度
- 在有向图中,顶点的度等于该顶点的入度与出度之和,顶点的入度是以该顶点为终点的边的条数,顶点的出度是以该顶点为起点的边的条数。
- 在无向图中,顶点的度等于与该顶点相关联的边的条数,同时也等于该顶点的入度和出度。
2.5 路径与路径长度
- 若从顶点 v i v_i vi出发有一组边使其可到达顶点 v j v_j vj ,则称顶点 v i v_i vi到顶点 v j v_j vj 的顶点序列为从顶点 v i v_i vi到顶点 v j v_j vj 的路径。
- 对于不带权的图,一条路径的长度是指该路径上的边的条数;对于带权的图,一条路径的长度是指该路径上各个边权值的总和。其中权值指边附带的数据信息。
2.6 简单路径与回路
- 若路径上的各个顶点 v 1 , v 2 , v 3 . . . v n v_1,v_2,v_3...v_n v1,v2,v3...vn均不相同,则称这样的路径为简单路径。
- 若路径上第一个顶点与最后一个顶点相同,则称这样的路径为回路或环。
2.7 子图
设图 G = ( V , E ) G=(V,E) G=(V,E)和图 G 1 = ( V 1 , E 1 ) G_1=(V_1,E_1) G1=(V1,E1),若 V 1 ⊆ V V_1⊆V V1⊆V且 E 1 ⊆ E E_1⊆E E1⊆E,那么称 G 1 G_1 G1是 G G G的子图。
2.8 连通图与强连通图
- 在无向图中,若从顶点 v 1 v_1 v1 到顶点 v 2 v_2 v2 有路径,则称顶点 v 1 v_1 v1 与顶点 v 2 v_2 v2 是连通的。如果图中任意一对顶点都是连通的,则称此图为连通图。
- 在有向图中,若每一对顶点 v i v_i vi 和 v j v_j vj 之间都存在一条从 v i v_i vi 到 v j v_j vj 的路,也存在一条从 v j v_j vj 到 v i v_i vi 的路,则称此图是强连通图。
2.9 生成树与最小生成树
- 一个连通图的最小连通子图称为该图的生成树,有 n n n个顶点的连通图的生成树有 n n n个顶点和 n − 1 n - 1 n−1条边。
- 最小生成树指的是一个图的生成树中,总权值最小的生成树。
3. 图的存储结构
因为图中既有节点,又有边(节点与节点之间的关系),因此,在图的存储中,只需要保存:节点和边关系即可。 下面我们将介绍两种常见的表示方法:邻接矩阵与邻接表。
3.1 邻接矩阵
邻接矩阵:先用一个数组将定点保存,然后采用矩阵来表示节点与节点之间的关系 。
对于一个具有
n
n
n个顶点的图
G
=
(
V
,
E
)
G=(V,E)
G=(V,E),其中
V
V
V 是顶点集,
E
E
E 是边集。其邻接矩阵
A
A
A是一个
n
×
n
n\times n
n×n 的矩阵,定义如下:
- 如果顶点 i i i 和顶点 j j j 之间有边相连,那么 A [ i ] [ j ] = 1 A[i][j]=1 A[i][j]=1(对于无向图, A [ i ] [ j ] = A [ j ] [ i ] = 1 A[i][j]=A[j][i]=1 A[i][j]=A[j][i]=1)。
- 如果顶点 i i i 和顶点 j j j 之间没有边相连,那么 A [ i ] [ j ] = 0 A[i][j]=0 A[i][j]=0(对于无向图, A [ i ] [ j ] = A [ j ] [ i ] = 0 A[i][j]=A[j][i]=0 A[i][j]=A[j][i]=0)。
例如,对于一个简单的无向图,有三个顶点
V
=
{
v
1
,
v
2
,
v
3
}
V=\{v_1,v_2,v_3\}
V={v1,v2,v3},如果
v
1
v_1
v1 和
v
2
v_2
v2 之间有边相连,
v
2
v_2
v2 和
v
3
v_3
v3 之间有边相连,
v
1
v_1
v1 和
v
3
v_3
v3 之间没有边相连,那么这个图的邻接矩阵为:
[
0
1
0
1
0
1
0
1
0
]
\begin{bmatrix} 0 & 1 & 0\\ 1 & 0 & 1\\ 0 & 1 & 0 \end{bmatrix}
010101010
如果是对于具有
n
n
n 个顶点的带权图
G
=
(
V
,
E
,
W
)
G=(V,E,W)
G=(V,E,W),其中
V
V
V 是顶点集,
E
E
E 是边集,
W
W
W 是权值集合。其邻接矩阵
A
A
A 是一个
n
×
n
n\times n
n×n 的矩阵。
- 如果顶点 v i v_i vi 和顶点 v j v_j vj 之间有边相连,那么 A [ i ] [ j ] A[i][j] A[i][j] 存放着该边对应的权值。
- 如果顶点 v i v_i vi 和顶点 v j v_j vj 之间没有边相连,那么 A [ i ] [ j ] A[i][j] A[i][j] 通常被设为一个特定的标识值,比如无穷大或者一个特殊的标记,具体取决于应用场景和算法需求。
例如,有一个带权无向图,三个顶点分别为 v 1 v_1 v1、 v 2 v_2 v2、 v 3 v_3 v3, v 1 v_1 v1 和 v 2 v_2 v2 之间边的权值为 2, v 2 v_2 v2 和 v 3 v_3 v3 之间边的权值为 3, v 1 v_1 v1 和 v 3 v_3 v3 之间没有边相连。那么这个图的邻接矩阵为: [ 0 2 ∞ 2 0 3 ∞ 3 0 ] \begin{bmatrix} 0 & 2 & ∞\\ 2 & 0 & 3\\ ∞ & 3 & 0 \end{bmatrix} 02∞203∞30
3.2 邻接表
邻接表:使用数组表示顶点的集合,使用链表表示边的关系。
对于一个具有
n
n
n个顶点的图
G
=
(
V
,
E
)
G=(V,E)
G=(V,E),邻接表的主要思想是为每个顶点建立一个链表,链表中存储与该顶点相邻的其他顶点。
具体来说:
- 对于无向图,图中的每一条边 ( u , v ) (u,v) (u,v),如果顶点 u u u的链表中还没有顶点 v v v,则将顶点 v v v添加到顶点 u u u的链表中;同理,如果顶点 v v v的链表中还没有顶点 u u u,则将顶点 u u u添加到顶点 v v v的链表中。
- 对于有向图,图中的每一条边 < u , v > <u,v> <u,v>,只需要将顶点 v v v添加到顶点 u u u的链表中。
例如,对于一个具有 5 5 5个顶点的无向图,顶点分别为 v 1 , v 2 , v 3 , v 4 , v 5 v_1,v_2,v_3,v_4,v_5 v1,v2,v3,v4,v5,如果有边 ( v 1 , v 2 ) (v_1,v_2) (v1,v2)、 ( v 1 , v 3 ) (v_1,v_3) (v1,v3)、 ( v 2 , v 4 ) (v_2,v_4) (v2,v4)、 ( v 3 , v 4 ) (v_3,v_4) (v3,v4)、 ( v 3 , v 5 ) (v_3,v_5) (v3,v5),那么其邻接表表示如下:
- 顶点 v 1 v_1 v1的链表: v 2 v_2 v2、 v 3 v_3 v3。
- 顶点 v 2 v_2 v2的链表: v 1 v_1 v1、 v 4 v_4 v4。
- 顶点 v 3 v_3 v3的链表: v 1 v_1 v1、 v 4 v_4 v4、 v 5 v_5 v5。
- 顶点 v 4 v_4 v4的链表: v 2 v_2 v2、 v 3 v_3 v3。
- 顶点 v 5 v_5 v5的链表: v 3 v_3 v3。
如果是对于一个具有 5 5 5个顶点的有向图,顶点分别为 v 1 , v 2 , v 3 , v 4 , v 5 v_1,v_2,v_3,v_4,v_5 v1,v2,v3,v4,v5,如果有边 < v 1 , v 2 > <v_1,v_2> <v1,v2>、 < v 1 , v 3 > <v_1,v_3> <v1,v3>、 < v 2 , v 4 > <v_2,v_4> <v2,v4>、 < v 3 , v 4 > <v_3,v_4> <v3,v4>、 < v 3 , v 5 > <v_3,v_5> <v3,v5>,那么其邻接表表示如下:
- 顶点 v 1 v_1 v1的链表: v 2 v_2 v2、 v 3 v_3 v3。
- 顶点 v 2 v_2 v2的链表: v 4 v_4 v4。
- 顶点 v 3 v_3 v3的链表: v 4 v_4 v4、 v 5 v_5 v5。
- 顶点 v 4 v_4 v4的链表:没有任何边。
- 顶点 v 5 v_5 v5的链表:没有任何边。
3.3 邻接矩阵与邻接表的优缺点
对于邻接矩阵:
优点:
- 直观性强,邻接矩阵能够 O ( 1 ) O(1) O(1)的时间判断两个顶点是否相连,并获得相连边的权值。
- 适合稠密图:图中的边越多,邻接矩阵的空间利用率就越高。
缺点:
- 不合适稀疏图:会浪费大量空间。
- 不适合查找一个顶点连接出去的所有边:需要遍历矩阵中对应的一行,该过程的时间复杂度是 O ( N ) O ( N ) O(N) ,其中 N N N表示的是顶点的个数。
对于邻接表:
优点:
- 适合查找一个顶点连接出去的所有边:只需要遍历对应的链表即可。
- 适合稀疏图:图中的边越少,邻接表存储的空间就越少。
缺点:
- 不合适稠密图:一个顶点会链接大量数据,需要遍历顶点对应位置的链表来确定两点是否相连,该过程的时间复杂度是 O ( E ) O(E) O(E),其中 E E E表示从源顶点连接出去的边的数量。
4. 邻接矩阵与邻接表的实现
4.1 邻接矩阵
4.1.1 邻接矩阵的结构
为了支持所有类型,我们实现一个模版类。其中肯定有两个模版参数V
与W
分别代表顶点与权值类型,MAX_W
表示两个顶点之间没有直接相连的值,一般我们默认为INT_MAX
,并且还需要一个bool
类型的模版参数Direction
代表是有向图还是无向图,false
为无向,true
为有向。
邻接矩阵的成员变量有三个分别为:数组_vertexs
代表边的集合,哈希表_indexMap
来映射不同类型与下标的关系,二维数组_matrix
代表邻接矩阵。
template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
class Graph
{
public:
//构造函数
Graph(const V*vertexs,int n);
//获取对应顶点的下标
int getVertexsIndex(const V& v);
void addEdge(const V& src, const V& dest, const W& weight);
//打印顶点集合和邻接矩阵
void Print();
private:
vector<V> _vertexs;//顶点集合
unordered_map<V, int> _indexMap;//映射关系
vector<vector<W>> _matrix;//邻接矩阵
};
4.1.2 邻接矩阵的初始化
我们首先顶点集合全部初始化,然后将邻接矩阵的值全设为INT_MAX
,最后将顶点集合与下标建立映射关系。
//构造函数
Graph(const V*vertexs,int n)
:_vertexs(vertexs, vertexs + n)
, _matrix(n,vector<int>(n, MAX_W))
{
//映射下标
for (int i = 0; i < n; i++)
{
_indexMap[vertexs[i]] = i;
}
}
4.1.3 添加边
添加边之前我们需要先找到对应顶点的下标,如果找不到需要抛异常。然后根据对应下标更新邻接矩阵,如果是无向图继续更新。
//获取对应顶点的下标
int getVertexsIndex(const V& v)
{
auto it = _indexMap.find(v);
if (it == _indexMap.end())
{
throw invalid_argument("不存在的顶点");
return -1;
}
else
{
return it->second;//返回对应下标
}
}
//添加边
void addEdge(const V& src, const V& dest, const W& weight)
{
int srci = getVertexsIndex(src);//获取起始点下标
int desti = getVertexsIndex(dest);//获取终点下标
_matrix[srci][desti] = weight;
if (Direction == false)
{
_matrix[desti][srci] = weight;//无向图
}
}
4.1.4 打印邻接矩阵
最后我们打印邻接矩阵,方便测试程序的正误。
//打印顶点集合和邻接矩阵
void Print()
{
int n = _vertexs.size();
//打印顶点集合
for (int i = 0; i < n; i++) {
cout << "[" << i << "]->" << _vertexs[i] << endl;
}
cout << endl;
//打印邻接矩阵
cout << " ";
for (int i = 0; i < n; i++) {
printf("%4d", i);
}
cout << endl;
for (int i = 0; i < n; i++) {
cout << i << " "; //竖下标
for (int j = 0; j < n; j++) {
if (_matrix[i][j] == MAX_W) {
printf("%4c", '*');
}
else {
printf("%4d", _matrix[i][j]);
}
}
cout << endl;
}
}
4.2 邻接表
4.2.1 邻接表的结构
同样邻接表我们编写成一个模板类,相比与邻接矩阵我们可以不需要代表权值最大的MAX_W
。并且为了方便描述我们需要首先编写一个关于边Edge
的类。这个类包含起始与终点下标,已经对应的权值。
然后邻接表中有三个成员变量:数组_vertexs
代表边的集合,哈希表indexMap
来映射不同类型与下标的关系,数组_linkTable
代表邻接矩阵。
template<class W>
//边
struct Edge
{
int _srci;//起始下标
int _desti;//终点下标
W _w;//权值
Edge<W>* _next;
Edge(int srci,int desti,const W&w)
:_srci(srci)
,_desti(desti)
,_w(w)
,_next(nullptr)
{}
};
template<class V, class W, bool Direction = false>
class Graph
{
typedef Edge<W> Edge;
public:
//构造函数
Graph(const V* vertexs, int n);
//获取对应顶点的下标
int getVertexsIndex(const V& v);
//添加边
void addEdge(const V& src, const V& dest, const W& weight);
//打印顶点集合和邻接表
void Print();
private:
vector<V> _vertexs;//顶点集合
unordered_map<V, int> _indexMap;//映射关系
vector<Edge*> _linkTable;//邻接矩阵
};
4.2.2 邻接表的初始化
我们首先顶点集合全部初始化,然后将邻接表的值全设为nullptr
,最后将顶点集合与下标建立映射关系;
//构造函数
Graph(const V* vertexs, int n)
:_vertexs(vertexs, vertexs + n)
, _linkTable(n, nullptr)
{
//映射下标
for (int i = 0; i < n; i++)
{
_indexMap[vertexs[i]] = i;
}
}
4.2.3 添加边
添加边之前我们需要先找到对应顶点的下标,如果找不到需要抛异常。然后根据对应下标更新邻接矩阵,如果是无向图继续更新。
//获取对应顶点的下标
int getVertexsIndex(const V& v)
{
auto it = _indexMap.find(v);
if (it == _indexMap.end())
{
throw invalid_argument("不存在的顶点");
return -1;
}
else
{
return it->second;//返回对应下标
}
}
//添加边
void addEdge(const V& src, const V& dest, const W& weight)
{
int srci = getVertexsIndex(src);//获取起始点下标
int desti = getVertexsIndex(dest);//获取终点下标
Edge* sre = new Edge(srci, desti, weight);
//进行头插
sre->_next = _linkTable[srci];
_linkTable[srci] = sre;
if (Direction == false)
{
Edge* dese = new Edge(desti, srci, weight);
dese->_next = _linkTable[desti];
_linkTable[desti] = dese;
}
}
4.2.4 打印邻接矩阵
最后我们打印邻接表,方便测试程序的正误。
//打印顶点集合和邻接表
void Print()
{
int n = _vertexs.size();
//打印顶点集合
for (int i = 0; i < n; i++) {
cout << "[" << i << "]->" << _vertexs[i] << " ";
}
cout << endl << endl;
//打印邻接表
for (int i = 0; i < n; i++) {
Edge* cur = _linkTable[i];
cout << "[" << i << ":" << _vertexs[i] << "]->";
while (cur) {
cout << "[" << cur->_desti << ":" << _vertexs[cur->_desti] << ":" << cur->_w << "]->";
cur = cur->_next;
}
cout << "nullptr" << endl;
}
}
5. 源码
5.1 邻接矩阵
namespace Matrix
{
template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
class Graph
{
public:
//构造函数
Graph(const V*vertexs,int n)
:_vertexs(vertexs, vertexs + n)
, _matrix(n,vector<int>(n, MAX_W))
{
//映射下标
for (int i = 0; i < n; i++)
{
_indexMap[vertexs[i]] = i;
}
}
//获取对应顶点的下标
int getVertexsIndex(const V& v)
{
auto it = _indexMap.find(v);
if (it == _indexMap.end())
{
throw invalid_argument("不存在的顶点");
return -1;
}
else
{
return it->second;//返回对应下标
}
}
//添加边
void addEdge(const V& src, const V& dest, const W& weight)
{
int srci = getVertexsIndex(src);//获取起始点下标
int desti = getVertexsIndex(dest);//获取终点下标
_matrix[srci][desti] = weight;
if (Direction == false)
{
_matrix[desti][srci] = weight;//无向图
}
}
//打印顶点集合和邻接矩阵
void Print() {
int n = _vertexs.size();
//打印顶点集合
for (int i = 0; i < n; i++) {
cout << "[" << i << "]->" << _vertexs[i] << endl;
}
cout << endl;
//打印邻接矩阵
cout << " ";
for (int i = 0; i < n; i++) {
printf("%4d", i);
}
cout << endl;
for (int i = 0; i < n; i++) {
cout << i << " "; //竖下标
for (int j = 0; j < n; j++) {
if (_matrix[i][j] == MAX_W) {
printf("%4c", '*');
}
else {
printf("%4d", _matrix[i][j]);
}
}
cout << endl;
}
}
private:
vector<V> _vertexs;//顶点集合
unordered_map<V, int> _indexMap;//映射关系
vector<vector<W>> _matrix;//邻接矩阵
};
}
5.2 邻接表
namespace LinkTable
{
template<class W>
struct Edge
{
int _srci;//起始下标
int _desti;//终点下标
W _w;//权值
Edge<W>* _next;
Edge(int srci,int desti,const W&w)
:_srci(srci)
,_desti(desti)
,_w(w)
,_next(nullptr)
{}
};
template<class V, class W, bool Direction = false>
class Graph
{
typedef Edge<W> Edge;
public:
//构造函数
Graph(const V* vertexs, int n)
:_vertexs(vertexs, vertexs + n)
, _linkTable(n, nullptr)
{
//映射下标
for (int i = 0; i < n; i++)
{
_indexMap[vertexs[i]] = i;
}
}
//获取对应顶点的下标
int getVertexsIndex(const V& v)
{
auto it = _indexMap.find(v);
if (it == _indexMap.end())
{
throw invalid_argument("不存在的顶点");
return -1;
}
else
{
return it->second;//返回对应下标
}
}
//添加边
void addEdge(const V& src, const V& dest, const W& weight)
{
int srci = getVertexsIndex(src);//获取起始点下标
int desti = getVertexsIndex(dest);//获取终点下标
Edge* sre = new Edge(srci, desti, weight);
//进行头插
sre->_next = _linkTable[srci];
_linkTable[srci] = sre;
if (Direction == false)
{
Edge* dese = new Edge(desti, srci, weight);
dese->_next = _linkTable[desti];
_linkTable[desti] = dese;
}
}
//打印顶点集合和邻接表
void Print()
{
int n = _vertexs.size();
//打印顶点集合
for (int i = 0; i < n; i++) {
cout << "[" << i << "]->" << _vertexs[i] << " ";
}
cout << endl << endl;
//打印邻接表
for (int i = 0; i < n; i++) {
Edge* cur = _linkTable[i];
cout << "[" << i << ":" << _vertexs[i] << "]->";
while (cur) {
cout << "[" << cur->_desti << ":" << _vertexs[cur->_desti] << ":" << cur->_w << "]->";
cur = cur->_next;
}
cout << "nullptr" << endl;
}
}
private:
vector<V> _vertexs;//顶点集合
unordered_map<V, int> _indexMap;//映射关系
vector<Edge*> _linkTable;//邻接矩阵
};
}