数据结构基础–图(14)
文章目录
图的定义:
图 G 由顶点集 V 和边集 E 组成,记为 G = (V, E),其中 V (G) 表示图 G 中顶点的有限非空集;E (G) 表示图 G 中顶点之间的关系(边)集合。若 V = {v₁, v₂, …, vₙ},则用 | V | 表示图 G 中顶点的个数,也称图 G 的阶,E = {(u, v) | u∈V, v∈V},用 | E | 表示图 G 中边的条数。
注意:线性表可以是空表,树可以是空树,但图不可以是空,即 V 一定是非空集
- 图的应用:地铁图,公交图
图的类型:
无向图
有向图
简单图
多重图
无向图的定义:
若 E 是无向边(简称边)的有限集合时,则图 G 为无向图。边是顶点的无序对,记为(v, w)或(w, v),因为**(v, w) = (w, v)**,其中 v、w 是顶点。可以说顶点 w 和顶点 v 互为邻接点。边(v, w)依附于顶点 w 和 v,或者说边(v, w)和顶点 v、w 相关联。
G 2 = ( V 2 , E 2 ) G_2 = (V_2, E_2) G2=(V2,E2)
V 2 = { A , B , C , D , E } V_2 = \{A, B, C, D, E\} V2={A,B,C,D,E}
E 2 = { ( A , B ) , ( B , D ) , ( B , E ) , ( C , D ) , ( C , E ) , ( D , E ) } E_2 = \{(A, B), (B, D), (B, E), (C, D), (C, E), (D, E)\} E2={(A,B),(B,D),(B,E),(C,D),(C,E),(D,E)}
有向图的定义:
若 E 是有向边(也称弧)的有限集合时,则图 G 为有向图。弧是顶点的有序对,记为 <v, w>,其中 v、w 是顶点,v 称为弧尾,w 称为弧头,<v, w > 称为从顶点 v 到顶点 w 的弧,也称 v 邻接到 w,或 w 邻接自 v。<v, w>≠<w, v>
G 1 = ( V 1 , E 1 ) G_1 = (V_1, E_1) G1=(V1,E1)
V 1 = { A , B , C , D , E } V_1 = \{A, B, C, D, E\} V1={A,B,C,D,E}
E 1 = { < A , B > , < A , C > , < A , D > , < A , E > , < B , A > , < B , C > , < B , E > , < C , D > } E_1 = \{<A, B>, <A, C>, <A, D>, <A, E>, <B, A>, <B, C>, <B, E>, <C, D>\} E1={<A,B>,<A,C>,<A,D>,<A,E>,<B,A>,<B,C>,<B,E>,<C,D>}
简单图的定义:
- 不存在重复边
- 不存在顶点到自身的边
多重图的定义:
图G中某两个结点之间的边数多于一条,又允许顶点通过同一条边和自己关联,则G为多重图
顶点的度、入度、出度
- 对于无向图:
顶点 v 的度是指依附于该顶点的边的条数,记为 TD (v)。
在具有 n 个顶点、e 条边的无向图中, ∑ i = 1 n T D ( v i ) = 2 e \sum_{i = 1}^{n}TD(v_i)=2e ∑i=1nTD(vi)=2e
即无向图的全部顶点的度的和等于边数的 2 倍
- 对于有向图:
入度是以顶点 v 为终点的有向边的数目,记为 ID (v)
出度是以顶点 v 为起点的有向边的数目,记为 OD (v)。
顶点 v 的度等于其入度和出度之和,即 TD (v) = ID (v) + OD (v)。
在具有 n 个顶点、e 条边的有向图中, ∑ i = 1 n I D ( v i ) = ∑ i = 1 n O D ( v i ) = e \sum_{i = 1}^{n}ID(v_i)=\sum_{i = 1}^{n}OD(v_i)=e ∑i=1nID(vi)=∑i=1nOD(vi)=e
顶点 - 顶点的关系描述
- 路径 —— 顶点 v p v_p vp到顶点 v q v_q vq之间的一条路径是指顶点序列
无向图的路径是没有方向的,也可能存在某些点之间没有路径
有向图的路径是有方向的
- 回路 —— 第一个顶点和最后一个顶点相同的路径称为回路或环
- 简单路径 —— 在路径序列中,顶点不重复出现的路径称为简单路径。
- 简单回路 —— 除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路称为简单回路。
- 路径长度 —— 路径上边的数目
- 点到点的距离 —— 从顶点 u 出发到顶点 v 的最短路径若存在,则此路径的长度称为从 u 到 v 的距离。若从 u 到 v 根本不存在路径,则记该距离为无穷 ∞ \infty ∞。
- 无向图中,若从顶点 v 到顶点 w 有路径存在,则称 v 和 w 是连通的
- 有向图中,若从顶点 v 到顶点 w 和从顶点 w 到顶点 v 之间都有路径,则称这两个顶点是强连通的
连通图、强连通图
- 连通图
若图 G 中任意两个顶点都是连通的,则称图 G 为连通图,否则称为非连通图。
常见考点:
对于 n 个顶点的无向图 G,
若 G 是连通图,则最少有 n-1 条边
若 G 是非连通图,则最多可能有 C n − 1 2 C_{n - 1}^2 Cn−12条边
- 强连通图
若图中任何一对顶点都是强连通的,则称此图为强连通图。
常见考点:
对于 n 个顶点的有向图 G,
若 G 是强连通图,则最少有 n 条边(形成回路)
子图
设有两个图 G = (V, E) 和 G’ = (V’, E’),若 V’ 是 V 的子集,且 E’ 是 E 的子集,则称 G’ 是 G 的子图。
若有满足V(G’) = V(G) 的子图 G’,则称其为 G 的生成子图
PS : 并非任意挑选几个点、几条边都能构成子图
连通分量
无向图中的极大连通子图称为连通分量
- 极大连通子图:子图必须连通,且包含尽可能多的顶点和边
有向图中的极大强连通子图称为有向图的强连通分量
- 极大连通子图:子图必须强连通,且包含尽可能多的顶点和边
生成树、生成森林
生成树:
连通图的生成树是包含图中全部顶点的一个极小连通子图
- 极小连通子图:边尽可能的少,但是要保持连通
若图中顶点数为n,则它生成树含有n - 1 条边。对生成树而言,若砍去它的一边,则会变成非连通图,若加上一条边则会形成一个回路。
生成森科:
在非连通图中,连通分量的生成树构成了非连通图的生成森林
边的权、带权图 / 网
- 边的权 —— 在一个图中,每条边都可以标上具有某种含义的数值,该数值称为该边的权值。
- 带权图 / 网 —— 边上带有权值的图称为带权图,也称网。
- 带权路径长度 —— 当图是带权图时,一条路径上所有边的权值之和,称为该路径的带权路径长度
几种特殊形态的图
- 无向完全图:无向图中任意两个顶点之间都存在边
若无向图的顶点数 ∣ V ∣ = n |V| = n ∣V∣=n,则 ∣ E ∣ ∈ [ 0 , C n 2 ] = [ 0 , n ( n − 1 ) / 2 ] |E| \in [0, C_n^2] = [0, n(n - 1)/2] ∣E∣∈[0,Cn2]=[0,n(n−1)/2]
- 有向完全图:有向图中任意两个顶点之间都存在方向相反的两条弧
若有向图的顶点数 ∣ V ∣ = n |V| = n ∣V∣=n,则 ∣ E ∣ ∈ [ 0 , 2 C n 2 ] = [ 0 , n ( n − 1 ) ] |E| \in [0, 2C_n^2] = [0, n(n - 1)] ∣E∣∈[0,2Cn2]=[0,n(n−1)]
-
稀疏图和稠密图是相对的
-
树 --不存在回路,且连通的无向图
n 个顶点的树,必有 n - 1 条边。
常见考点:n 个顶点的图,若 ∣ E ∣ > n − 1 |E|>n - 1 ∣E∣>n−1,则一定有回路
- 有向树–一个顶点的入度为0,其余顶点的入度均为1的有向图,称为有向树
图的存储–邻接矩阵法
图的邻接矩阵只是存储 0 和 1两种(不带权值)
实现代码:
#define MaxVertexNum 100 //顶点数目的最大值
typedef struct{
char Vex[MaxVertexNum]; //顶点表
int Edge[MaxVertexNum][MaxVertexNum]; //邻接矩阵,边表
int vexnum,arcnum; //图的当前顶点数和边数/弧数
} MGraph;
顶点中可以存储更加复杂的信息,只是因为是存储 0 和 1 所有用char
char Vex[MaxVertexNum]; //顶点表
也可以用bool型或枚举型变量来表示边
int Edge[MaxVertexNum][MaxVertexNum]; //邻接矩阵,边表
结点数为n的图(G=(V,E))的邻接矩阵A是 n × n n \times n n×n的。将G的顶点编号为 v 1 , v 2 , … , v n v_1, v_2, \dots, v_n v1,v2,…,vn,则
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 ) 中的边 A[i][j] = \begin{cases} 1, & \text{若} (v_i, v_j) \text{或} <v_i, v_j> \text{是} E(G) \text{中的边} \\ 0, & \text{若} (v_i, v_j) \text{或} <v_i, v_j> \text{不是} E(G) \text{中的边} \end{cases} A[i][j]={1,0,若(vi,vj)或<vi,vj>是E(G)中的边若(vi,vj)或<vi,vj>不是E(G)中的边
在无向图中:
第 i 个结点的度 = 第 i 行(或第 i 列)的非零元素的个数
时间复杂度:O(|v|)
在有向图中:
第 i 个结点的出度 = 第 i 行的非零元素个数
第 i 个结点的入度 = 第 i 列的非零元素个数
第 i 个结点的度 = 第 i 行、第 i 列的非零元素个数之和
时间复杂度:O(|v|)
图的邻接矩阵(带权值)/ 网
如果两点之间不村子边的话,我们用无穷(∞)来表示它们之间不存在相连的边
实现代码:
#define MaxVertexNum 100 //顶点数目的最大值
#define INFINITY 最大的int值 //宏定义常量"无穷"
typedef char VertexType; //顶点的数据类型
typedef int EdgeType; //带权图中边上权值的数据类型
typedef struct{
VertexType Vex[MaxVertexNum]; //顶点
EdgeType Edge[MaxVertexNum][MaxVertexNum]; //边的权
int vexnum,arcnum; //图的当前顶点数和弧数
}MGraph;
可用int 的上限值表示“无穷”
#define INFINITY 最大的int值 //宏定义常量"无穷"
邻接矩阵的性能分析
空间复杂度: O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2) —— 只和顶点数相关,和实际的边数无关
适合用于存储稠密图
无向图的邻接矩阵是对称矩阵,可以压缩存储(只存储上三角区 / 下三角区)
邻接矩阵的性质
A | B | C | D | |
---|---|---|---|---|
A | 0 | 1 | 0 | 0 |
B | 1 | 0 | 1 | 1 |
C | 0 | 1 | 0 | 1 |
D | 0 | 1 | 1 | 0 |
设图G的邻接矩阵为A(矩阵元素为(0/1)),则 A n A^n An的元素 A n [ i ] [ j ] A^n[i][j] An[i][j]等于由顶点i到顶点j的长度为n的路径的数目
A 2 [ 1 ] [ 4 ] = a 1 , 1 a 1 , 4 + a 1 , 2 a 2 , 4 + a 1 , 3 a 3 , 4 + a 1 , 4 a 4 , 4 = 1 A^2[1][4] = a_{1,1}a_{1,4} + a_{1,2}a_{2,4} + a_{1,3}a_{3,4} + a_{1,4}a_{4,4} = 1 A2[1][4]=a1,1a1,4+a1,2a2,4+a1,3a3,4+a1,4a4,4=1
A 2 [ 2 ] [ 2 ] = a 2 , 1 a 1 , 2 + a 2 , 2 a 2 , 2 + a 2 , 3 a 3 , 2 + a 2 , 4 a 4 , 2 = 3 A^2[2][2] = a_{2,1}a_{1,2} + a_{2,2}a_{2,2} + a_{2,3}a_{3,2} + a_{2,4}a_{4,2} = 3 A2[2][2]=a2,1a1,2+a2,2a2,2+a2,3a3,2+a2,4a4,2=3
A 2 [ 3 ] [ 3 ] = a 3 , 1 a 1 , 3 + a 3 , 2 a 2 , 3 + a 3 , 3 a 3 , 3 + a 3 , 4 a 4 , 3 = 1 A^2[3][3] = a_{3,1}a_{1,3} + a_{3,2}a_{2,3} + a_{3,3}a_{3,3} + a_{3,4}a_{4,3} = 1 A2[3][3]=a3,1a1,3+a3,2a2,3+a3,3a3,3+a3,4a4,3=1
A 2 [ 1 ] [ 2 ] = a 1 , 1 a 1 , 2 + a 1 , 2 a 2 , 2 + a 1 , 3 a 3 , 2 + a 1 , 4 a 4 , 2 = 0 A^2[1][2] = a_{1,1}a_{1,2} + a_{1,2}a_{2,2} + a_{1,3}a_{3,2} + a_{1,4}a_{4,2} = 0 A2[1][2]=a1,1a1,2+a1,2a2,2+a1,3a3,2+a1,4a4,2=0
图的存储–邻接表(顺序 + 链式存储)
邻接表适用于存储稀疏图
实现代码:
// 图结构体(邻接表存储)
typedef struct{
AdjList vertices; // 顶点数组
int vexnum; // 图的当前顶点数
int arcnum; // 图的当前边/弧数
}ALGraph;
// 顶点结点结构体
typedef struct VNode{
VertexType data; // 顶点存储的数据
ArcNode *first; // 指向第一条依附于该顶点的边/弧
}VNode, AdjList[MaxVertexNum]; // AdjList为顶点数组类型
// 边/弧结点结构体
typedef struct ArcNode{
int adjvex; // 边/弧指向的顶点下标
struct ArcNode *next; // 指向下一条边/弧的指针
// InfoType info; // 边/弧的权值等附加信息(可根据需要启用)
}ArcNode;
这个方法与孩子表示法相似
无向图:
边结点的数量是 2 ∣ E ∣ 2|E| 2∣E∣,整体空间复杂度为$ O(|V| + 2|E|)$
有向图:
边结点的数量是$ |E| ,整体空间复杂度为 ,整体空间复杂度为 ,整体空间复杂度为 O(|V| + |E|)$
图的邻接表的表示方式不唯一
图的邻接矩阵:只要确定了顶点编号,图的邻接矩阵表示方法唯一
图的存储–十字链表(存储有向图)
空间复杂度:$ O(|V| + |E|)$
如何找到指定顶点的所有出边?
–顺着绿色线路找
如何找到指定顶点的所有入边?
–顺着橙色线路找
图的存储–邻接多重表(存储无向图)
空间复杂度:$ O(|V| + |E|)$
删除边、删除节点等操作很方便
图的基本操作
图的存储常考考点:邻接矩阵 、 邻接表
<>这个是有向图,()这个是无向图
Adjacent(G,x,y)
:判断图G
是否存在边<x, y>
或(x, y)
。
无向图 :邻接矩阵时间复杂度:O(1) 、 邻接表时间复杂度:O(1)
有向图 :邻接矩阵时间复杂度:O(1) 、 邻接表时间复杂度:O(1) ~ O(|v|)
Neighbors(G,x)
:列出图G
中与结点x
邻接的边。
无向图 :O(|v|) 、 邻接表时间复杂度:O(1) ~ O(|v|)
有向图 :邻接矩阵时间复杂度:O(|v|) 、 邻接表时间复杂度:出边 :O(1) ~ O(|v|),入边 :$ O(|E|)$
InsertVertex(G,x)
:在图G
中插入顶点x
。
无向图 :邻接矩阵时间复杂度O(1) 、 邻接表时间复杂度:O(1)
有向图 :邻接矩阵时间复杂度O(1) 、 邻接表时间复杂度:O(1)
DeleteVertex(G,x)
:从图G
中删除顶点x
。
无向图 :邻接矩阵时间复杂度:O(|v|) 、 邻接表时间复杂度:O(1) ~ $ O(|E|)$
有向图 :邻接矩阵时间复杂度:O(|v|) 、 邻接表时间复杂度:删出边 :O(1) ~ O(|v|),删入边 :$ O(|E|)$
AddEdge(G,x,y)
:若无向边(x, y)
或有向边<x, y>
不存在,则向图G
中添加该边。
无向图 :邻接矩阵时间复杂度O(1) 、 邻接表时间复杂度:O(1)
有向图 :邻接矩阵时间复杂度O(1) 、 邻接表时间复杂度:O(1)
RemoveEdge(G,x,y)
:若无向边(x, y)
或有向边<x, y>
存在,则从图G
中删除该边。
无向图 :邻接矩阵时间复杂度O(1) 、 邻接表时间复杂度:O(1) ~ $ O(|E|)$
有向图 :邻接矩阵时间复杂度O(1) 、 邻接表时间复杂度:O(1) ~ $ O(|E|)$
FirstNeighbor(G,x)
:求图G
中顶点x
的第一个邻接点,若有则返回顶点号。若x
没有邻接点或图中不存在x
,则返回-1
。
无向图 :邻接矩阵时间复杂度O(1) ~ O(|v|) 、 邻接表时间复杂度:O(1)
有向图 :邻接矩阵时间复杂度O(1) ~ O(|v|)、 邻接表时间复杂度:找出边邻接点:O(1),找入边邻接点O(1) ~ $ O(|E|)$,但是一般只考察找出边的操作
NextNeighbor(G,x,y)
:假设图G
中顶点y
是顶点x
的一个邻接点,返回除y
之外顶点x
的下一个邻接点的顶点号,若y
是x
的最后一个邻接点,则返回-1
。
无向图 :邻接矩阵时间复杂度O(1) ~ O(|v|) 、 邻接表时间复杂度:O(1)
Get_edge_value(G,x,y)
:获取图G
中边(x, y)
或<x, y>
对应的权值。
无向图 :邻接矩阵时间复杂度O(1) 、 邻接表时间复杂度:O(1) ~ O(|v|)
Set_edge_value(G,x,y,v)
:设置图G
中边(x, y)
或<x, y>
对应的权值为v
。
常考的:FirstNeighbor(G,x)
、NextNeighbor(G,x,y)
图的广度优先遍历(BFS)
树 VS 图
树 :不存在 “ 回路 ” ,搜索相邻的结点时,不可能搜到已经访问过的结点
图:搜索相邻的顶点时,有可能搜到已经访问过的顶点
解决方法(设置一个标记,用来标记已经搜索到的顶点)
广度优先遍历(Breadth - First - Search, BFS)要点
- 找到与一个顶点相邻的所有顶点
- 标记哪些顶点被访问过
- 需要一个辅助队列
相关函数
- FirstNeighbor(G, x):求图 G 中顶点 x 的第一个邻接点,若有则返回顶点号;若 x 没有邻接点或图中不存在 x,则返回 - 1。
- NextNeighbor(G, x, y):假设图 G 中顶点 y 是顶点 x 的一个邻接点,返回除 y 之外顶点 x 的下一个邻接 点的顶点号,若 y 是 x 的最后一个邻接点,则返回 - 1。
广度优先遍历序列:
从顶点1出发的广度优先遍历序列:
1,2,5,6,3,7,4,8
从顶点3出发的广度优先遍历序列:
3,4,6,7,8,2,1,5
同一个图的邻接矩阵表示方式唯一,因此广度优先遍历序列唯一
同一个图的邻接表表示方式不唯一,因此广度优先遍历序列不唯一
实现代码:
bool visited[MAX_VERTEX_NUM]; //访问标记数组,初始都为 false
void BFSTraverse(Graph G){ //对图G进行广度优先遍历
for(i=0;i<G.vexnum;++i)
visited[i]=FALSE; //访问标记数组初始化
InitQueue(Q); //初始化辅助队列Q
for(i=0;i<G.vexnum;++i) //从0号顶点开始遍历
if(!visited[i]) //对每个连通分量调用一次BFS
BFS(G,i); //vi未访问过,从vi开始BFS
}
//广度优先遍历
void BFS(Graph G,int v){ //从顶点v出发,广度优先遍历图G
visit(v); //访问初始顶点v
visited[v]=TRUE; //对v做已访问标记
Enqueue(Q,v); //顶点v入队列Q
while(!isEmpty(Q)){
DeQueue(Q,v); //顶点v出队列
for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w))
//检测v所有邻接点
if(!visited[w]){ //w为v的尚未访问的邻接顶点
visit(w); //访问顶点w
visited[w]=TRUE;//对w做已访问标记
EnQueue(Q,w); //顶点w入队列
}//if
}//while
}
- 对于无向图来说,调用BFS函数的次数 = 连通分量数
空间复杂度:最坏的情况,辅助队列大小为 O(|v|)
时间复杂度:
邻接矩阵:
访问 |V| 个顶点需要 O (|V|) 的时间
查找每个顶点的邻接点都需要 O (|V|) 的时间,而总共有 |V| 个顶点
时间复杂度 = O (|V|²)
邻接表
访问 |V| 个顶点需要 O (|V|) 的时间
查找各个顶点的邻接点共需要 O (|E|) 的时间
时间复杂度 = O (|V| + |E|)
- 广度优先生成树
广度优先生成树由广度优先遍历过程确定。由于邻接表的表示方式不唯一,因此基于邻接表的广度优先生成树也不唯一。
- 广度优先生成森林
对非连通图的广度优先遍历,可得到广度优先生成森林
图的深度优先遍历(DFS)
图的深度优先遍历与树的先根遍历相类似
树的先根遍历:
//树的先根遍历
void PreOrder(TreeNode *R){
if (R!=NULL){
visit(R); //访问根节点
while(R还有下一个子树T)
PreOrder(T); //先根遍历下一棵子树
}
}
图的深度优先遍历:
从2出发的深度优先遍历序列:
2,1,5,6,3,4,7,8
实现代码:
bool visited[MAX_VERTEX_NUM]; //访问标记数组
void DFSTraverse(Graph G){ //对图G进行深度优先遍历
for(v=0;v<G.vexnum;++v) //初始化已访问标记数据
visited[v]=FALSE;
for(v=0;v<G.vexnum;++v) //本代码中是从v=0开始遍历
if(!visited[v])
DFS(G,v);
}
void DFS(Graph G,int v){ //从顶点v出发,深度优先遍历图G
visit(v); //访问顶点v
visited[v]=TRUE; //设已访问标记
for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w))
if(!visited[w]){ //w为u的尚未访问的邻接顶点
DFS(G,w);
} //if
}
空间复杂度:来自函数调用栈,最坏情况,递归深度为 O (|V|) -->(无特殊情况下是这个复杂化度)
空间复杂度:最好情况,O (1)
时间复杂度:访问各个结点所需要的时间 + 探索各条边所需时间
邻接矩阵:
访问 |V| 个顶点需要 O (|V|) 的时间
查找每个顶点的邻接点都需要 O (|V|) 的时间,而总共有 |V| 个顶点
时间复杂度 = O (|V|²)
邻接表:
访问 |V| 个顶点需要 O (|V|) 的时间
查找各个顶点的邻接点共需要 O (|E|) 的时间
时间复杂度 = O (|V| + |E|)
- 深度优先生成树:
同一个图的邻接矩阵表示方式唯一,因此深度优先遍历序列唯一,深度优先生成树也唯一
同一个图的邻接表表示方式不唯一,因此深度优先遍历序列不唯一 ,深度优先生成树也不唯一
图的遍历与图的连通性:
对无向图进行 BFS/DFS 遍历 调用 BFS/DFS 函数的次数 = 连通分量数
对于连通图,只需调用 1 次 BFS/DFS
对有向图进行 BFS/DFS 遍历 调用 BFS/DFS 函数的次数要具体问题具体分析
若起始顶点到其他各顶点都有路径,则只需调用 1 次 BFS/DFS 函数
对于强连通图,从任一结点出发都只需要调用 1 次 BFS/DFS 函数