一. 概念
不说了
N
N
N:顶点: 必须有
E
E
E:边: 可以没有
多对多的关系
线性表:一对一
树: 一对多
图: 多对多
无向边: (V,W)
有向边: <V,W>
带权重的图是网络
2. 程序中的表示
1. 邻接矩阵:
G[N][N] ——N个定点从0-N-1编号
G
[
i
]
[
j
]
=
{
权
if <
v
i
,
v
j
>是
G
的边
0
否则
G[i][j]= \begin{cases} 权 & \text{if <$v_i,v_j$>是$G$的边}\\ 0 &\text{否则} \end{cases}
G[i][j]={权0if <vi,vj>是G的边否则
邻接表对于无向图:
- 对角元是0
- 对称矩阵
- 一半空间浪费
优点:
- 简单粗暴
- 方便检查任意一对顶点是否有边
- 方便找任一顶点的所有邻接点
- 方便计算任一顶点的度
- 无向图:对应行/列非0元素个数
- 有向图:对应行非0元素的个数是出度,对应列非0元素的个数是入度
缺点:
- 存稀疏图浪费空间(但对于稠密图甚至完全图是很合算的)
- 稀疏图还浪费时间(例如统计稀疏图一共多少条边,全部遍历)
怎样节约一半空间:
- 用一个长为 N(N-1)/2的一维数组存储(按行存储)下三角阵: G 00 , G 10 , . . . , G n − 1. n − 1 G_{00},G_{10},...,G_{n-1.n-1} G00,G10,...,Gn−1.n−1
-
G
i
j
G_{ij}
Gij对应的下标为:
i
(
i
+
1
)
/
2
+
j
i(i+1)/2+j
i(i+1)/2+j (首项1,末项i,项数i)
2.邻接表
G ( N ) G(N) G(N)为为每个结点开辟的指针数组,对应矩阵每行一个链表,只存非零元素,表示方式不唯一
-
每条边被存了两遍
-
一定要稀疏才合算
优点: -
便于找任意结点的所有邻接点
-
节约稀疏图的空间
- N个头指针+2E个结点(每个结点至少2个域)
- 节约空间图的边需要满足: N + 2 E ∗ 2 < N 2 N+2E*2<N^2 N+2E∗2<N2
-
对于顶点的度的计算:
- 无向图: 方便
- 有向图:只能计算出度,需要构造逆邻接表(存储指向自己的边)来计算入度
对于有N个顶点,E条边的图,
- 用邻接表表示,遍历所有边的时间复杂度为 O ( N + E ) O(N+E) O(N+E) 其实是( O ( N + 2 E ) O(N+2E) O(N+2E))
- 用邻接矩阵表示,遍历所有边的时间复杂度为 O ( N 2 ) O(N^2) O(N2)
3. python实现(待补充)
# 图的邻接矩阵实现
二. 遍历
DFS——深度优先
类似树的先序遍历
伪码如下:
void DFS(Vertex V)
{
visited[ V] = true; /*访问该节点*/
for (V的每个邻接点 W)
{
if (! visited[W]) /*如果没有访问*/
DFS(W) /*
}
}
时间复杂度:
- 邻接表: O ( N + E ) O(N+E) O(N+E)
- 邻接矩阵: O ( N 2 ) O(N^2) O(N2)
BFS——广度优先
类似树的层序遍历,借助了队列
伪码如下:
void BFS(Vertext V)
{ visited[V] = true; // 访问它
Enqueue(V,Q) ; //入队
while (!IsEmpty(Q)) //队不空
{ V = Dequeue(Q); // 弹出一个结点访问它的邻接点
for (V 的每个邻接点 W)
if (!visited[W]) // 如果邻接点没访问过
{ visited[W] = true;
Enqueue(W,Q) ; // 访问它并入队
}
}
}
时间复杂度:
- 邻接表: O ( N + E ) O(N+E) O(N+E)
- 邻接矩阵: O ( N 2 ) O(N^2) O(N2)
其他
无向图:
- 连通图:任意两点有路径联通
- 连通分量:无向图的极大连通子图
有向图:
- 强连通:任意两点存在双向路径
- 强连通图
- 强连通分量:
每次使用DFS/BFS都是访问了一个图的一个强连通分量
对于不连通的图要对每个连通分量做遍历