开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第8天,点击查看活动详情
🔥 本文由 程序喵正在路上 原创,在稀土掘金首发!
💖 系列专栏:数据结构与算法
🌠 首发时间:2022年11月30日
🦋 欢迎关注🖱点赞👍收藏🌟留言🐾
🌟 一以贯之的努力 不得懈怠的人生
基本操作
- :判断图 是否存在边 或
- :列出图 中与结点 邻接的边
- :在图 中插入顶点
- :在图 中删除顶点
- :若无向边 或有向边 不存在,则向图 中添加该边
- :若无向边 或有向边 存在,则从图 中删除该边
- :求图 中顶点 的第一个邻接点,若有则返回顶点号;若 没有邻接点或图中不存在 ,则返回
- :假设图 中顶点 是顶点 的一个邻接点,返回除了 之外顶点 的下一个邻接点的顶点号,若 是 的最后一个邻接点,则返回
- :获取图 中边 或 对应的权值
- :设置图 中边 或 对应的权值为
广度优先遍历(BFS)
与树的广度优先遍历之间的联系
树的广度优先遍历,也就是树的层次遍历,有以下步骤:
- 若树非空,则根节点入队
- 若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队
- 重复第 步指导队列为空
对于树,由于树不存在 “回路”,因此在搜索相邻的结点时,不可能搜索到已经访问过的结点;而图则反之,所以我们需要对图中的结点进行标记,以此来识别这个结点是否访问过
广度优先遍历()要点:
- 找到与一个顶点相邻的所有顶点
- 标记哪些顶点被访问过
- 需要一个辅助队列
在广度优先遍历中,我们需要用到两个基本操作:
- :求图 中顶点 的第一个邻接点,若有则返回顶点号;若 没有邻接点或图中不存在 ,则返回
- :假设图 中顶点 是顶点 的一个邻接点,返回除了 之外顶点 的下一个邻接点的顶点号,若 是 的最后一个邻接点,则返回
算法实现
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出发,广度优先遍历图
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入队列Q
}//if
}//for
}//while
}
结论:对于无向图,调用 函数的次数 连通分量数(连通分量:一个极大连通子图为一个连通分量)
复杂度分析
空间复杂度:
- 最坏情况,我们访问第一个顶点时,所有顶点都和它连通,此时辅助队列大小为
如果我们是用邻接矩阵存储的图:
- 访问 个顶点需要 的时间
- 查找每个顶点的邻接点都需要 的时间,而总共有 个顶点
- 时间复杂度
如果我们是用邻接表存储的图:
- 访问 个顶点需要 的时间
- 查找每个顶点的邻接点共需要 的时间
- 时间复杂度
广度优先生成树
广度优先生成树由广度优先遍历过程确定。由于邻接表的表示方式不唯一,因此基于邻接表的广度优先生成树也不唯一
对于非连通图的广度优先遍历,我们还可以得到广度优先生成森林
深度优先遍历(DFS)
与树的深度优先遍历之间的联系
树的深度优先遍历分为先根遍历和后根遍历,图的深度优先遍历和树的先根遍历比较相似
//树的先根遍历
void PreOrder(TreeNode *R) {
if (R != NULL) {
visit(R); //访问根节点
while (R还有下一个子树T)
PreOrder(T); //先根遍历下一棵子树
}
}
算法实现
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.vexnuj; ++v) //解决非连通图无法遍历完的问题
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为v的未访问的邻接顶点
DFS(G, w);
}
}
}
复杂度分析
空间复杂度:来自函数调用栈,最坏情况下,递归深度为 ;最好情况为
时间复杂度 访问每个节点所需时间 探索每条边所需时间
如果是用邻接矩阵存储的图:
- 访问 个顶点需要 的时间
- 查找每个顶点的邻接点都需要 的时间,总共有 个顶点
- 总时间复杂度为
如果是用邻接表存储的图:
- 访问 个顶点需要 的时间
- 查找每个顶点的邻接点共需要 的时间
- 时间复杂度
你会发现,深度优先遍历和广度优先遍历的复杂度是一样的
深度优先遍历序列
对于上图:
- 从 出发的深度优先遍历序列为:
- 从 出发的深度优先遍历序列为:
- 从 出发的深度优先遍历序列为:
注意:如果邻接表不一样,深度优先遍历序列也可能不一样;同时,因为邻接矩阵表示方式唯一,所以深度优先遍历序列唯一
深度优先生成树
将深度优先遍历序列写成树的形式,即为对应的深度优先生成树
如果是非连通图,那么会有深度优先生成森林
图的遍历和图的连通性
对无向图进行 遍历,调用 函数的次数等于连通分量数;如果是连通图的话,我们只需要调用一次 函数
对有向图进行 遍历,调用 函数的次数要根据具体的数进行具体分析;如果起始顶点到其他顶点都有路径,那么我们只需要调用一次函数即可
对于强连通图,我们从任何一个顶点出发都只需要调用一次