点击关注上方“五分钟学算法”,

设为“置顶或星标”,第一时间送达干货。

转自景禹

小禹禹,你们好呀,景禹今天给你们说一说图的遍历方法!

小禹禹: 好呀好呀,图的遍历方法都包含哪些呢?

景禹: 图的遍历方法包括 深度优先遍历(搜索)广度优先遍历(搜索) 两种方式。小禹禹能给我说一下树的四种遍历方式吗?

聪明的小禹禹: 树的四种遍历方式分别为:前序遍历、中序遍历和后序遍历、层序遍历。这四种遍历方式小禹禹掌握的可熟悉了。(不熟悉的小伙伴可以看之前的文章: 一文横扫二叉树的所有遍历方法)

小禹禹可真棒,那我们正式开始学习图的遍历方法喽!

深度优先搜索

算法思想

深度优化遍历( Depth First Search ),也有称为 深度优化搜索 ,简称为 DFS 。事实上,我们在树的遍历中早已涉及DFS,层序遍历、中序遍历和后序遍历都属于深度优先遍历的方式,因为这些遍历方式本质上都归结于。为了讲清楚DFS,我们先来看两个概念。

右手原则: 在没有碰到重复顶点的情况下,分叉路口始终是向右手边走,每路过一个顶点就做一个记号。

左手原则: 在没有碰到重复顶点的情况下,分叉路口始终是向左手边走,每路过一个顶点就做一个记号。

本文约定以右手原则进行深度优先遍历。废话不多说,我们以下图说明深度优先搜索。

原则上,我们可以从图中的任何一个顶点开始,进行深度优先遍历,假设我们从顶点A开始,遍历过程中的每一步如下:

第一步:从顶点A开始,将顶点A标记为已访问顶点。

第二步:根据约定的右手原则,访问顶点B,并将B标记为已访问顶点。

第三步:右手原则,访问顶点C

第四步:右手原则,访问顶点D

第五步:右手原则,访问顶点E

第六步:右手原则,访问顶点F

第七步:右手原则,应该先访问顶点F的邻接顶点A,但发现A已被访问,则访问除A之外的最右侧顶点G。

第八步:右手原则,先访问顶点B,顶点B已被访问;再访问顶点D,顶点D已经被访问;最后访问顶点H。

第九步:发现顶点H的邻接顶点均已被访问,则退回到顶点G;

第十步:顶点G的邻接顶点均已被访问,则退回到顶点F;

第十一步:顶点F的邻接顶点已被访问,则退回到顶点E;

第十二步:顶点E的邻接顶点均已被访问,则退回到顶点D;

第十三步:顶点D的邻接顶点I尚未被访问,则访问顶点I;

第十四步:顶点I的邻接顶点均已被访问,则退回到顶点D;

顶点D的邻接顶点均已被访问,退回到顶点C;顶点C的邻接顶点均已被访问,则退回到顶点B;顶点B的邻接顶点均已被访问,则退回到顶点A,顶点A为起始顶点,深度优先搜索结束。

小禹禹:景禹,你是不是给我讲的有点儿啰嗦了?

景禹:哈哈!觉得啰嗦就对了,上面所说的这些过程不正就是递归的过程吗?

小禹禹:欧迪斯奈,原来如此。

为了更清楚的理解图的深度优先搜索和二叉树的前序遍历、中序遍历、后序遍历均属于一类方法,我们对最终的遍历结果图做一定的位置调整:

细心的小禹禹一定发现,这就是我们的前序遍历过程呀!相信看到这里的小禹禹一定对深度优先搜索豁然开朗了。

为了更加清楚图的深度优先搜索,我们将上面的过程总结为以下三个步骤:

  1. 首先选定一个未被访问过的顶点V作为起始顶点(或者访问指定的起始顶点V),并将其标记为已访问过;

  2. 然后搜索与顶点V邻接的所有顶点,判断这些顶点是否被访问过,如果有未被访问过的顶点W;再选取与顶点W邻接的未被访问过的一个顶点并进行访问,依次重复进行。当一个顶点的所有的邻接顶点都被访问过时,则依次回退到最近被访问的顶点。若该顶点还有其他邻接顶点未被访问,则从这些未被访问的顶点中取出一个并重复上述过程,直到与起始顶点V相邻接的所有顶点都被访问过为止。

  3. 若此时图中依然有顶点未被访问,则再选取其中一个顶点作为起始顶点并进行遍历,转(2)。反之,则遍历结束。

DFS的实现

小禹禹:景禹,这一次我终于对深度优先搜索理解了!景禹能告诉我怎么实现吗?

深度优先遍历(搜索)最简单的实现方式就是递归,由于图的存储方式不同,递归的实现当然也略有差异。

邻接矩阵存储 递归 实现

当图采用邻接矩阵进行存储时,递归的实现操作为:

// 邻接矩阵的深度有限递归算法#define TRUE 1
#define FALSE 0
#define MAX 256typedef int Boolean; // 这里我们定义Boolean为布尔类型,其值为TRUE或FALSE
Boolean visited[MAX]; // 访问标志的数组//进行递归的遍历包含顶点i的连通图
void DFS(MGraph G, int i)
{visited[i] = TRUE; // 访问过的顶点设置为TRUEprintf("%c ", G.vexs[i]); // 打印顶点for( j=0; j < G.numVertexes; j++ ){if( G.arc[i][j]==1 && !visited[j] ){DFS(G, j); // 对为访问的邻接顶点递归调用}}
}// 邻接矩阵的深度遍历操作
void DFSTraverse(MGraph G)
{int i;// 初始化所有顶点状态都是未访问过状态for( i=0; i < G.numVertexes; i++ ){visited[i] = FALSE;}//防止图为非联通的情况,遍历整个图for( i=0; i < G.numVertexes; i++ ){if( !visited[i] ) // 若是连通图,只会执行一次{DFS(G, i);}}
}

邻接矩阵存储 栈 实现

对于上面的递归,我们也可以采用栈来实现,为了清楚的理解利用栈实现的深度优先搜索的执行过程,我们一起来看一看下面的动画(配合动画看代码):

动画演示:

实现代码:

void DFS_Stack(MGraph G, int i)
{int node;int count = 1;printf("%c ", G.vexs[i]); // 打印已访问顶点visited[i] = TRUE;node = i;push(i); //开始的节点入栈while(count < G.numVertexes) //still has node not visited{/* 所有被访问的节点依次入栈,只有node当找不到下一个相连的节点时,才使用出栈节点 */for(j=0; j < G.numVertexes; j++){if(G.arc[node][j] == 1 && visited[j] == FALSE){visited[j] = TRUE;printf("%c ", G.vexs[j]);count++;push(j); //push node jbreak;}}if(j == G.numVertexes) //与node相连的顶点均已被访问过,所以需要从stack里取出node的上一个顶点,再看该顶点的邻接顶点是否未被访问node = pop();else      //找到与node相连并且未被访问的顶点,node = j;}
}

邻接表存储递归实现

邻接表存储下图的深度优先搜索代码实现,与邻接矩阵的思想相同,只是实现略有不同:

// 邻接表的深度有限递归算法#define TRUE 1
#define FALSE 0
#define MAX 256typedef int Boolean; // 这里我们定义Boolean为布尔类型,其值为TRUE或FALSE
Boolean visited[MAX]; // 访问标志的数组void DFS(GraphAdjList GL, int i)
{EdgeNode *p;visited[i] = TRUE;printf("%c " GL->adjList[i].data);p = GL->adjList[i].firstEdge;while(p){if( !visited[p->adjvex] ){DFS(GL, p->adjvex);}p = p->next;}
}// 邻接表的深度遍历操作
void DFSTraverse(GraphAdjList GL)
{int i;for( i=0; i < GL->numVertexes; i++ ){visited[i] = FALSE; // 初始化所有顶点状态都是未访问过状态}for( i=0; i < GL->numVertexes; i++ ){if( !visited[i] ) // 若是连通图,只会执行一次{DFS(GL, i);}}
}

广度优先搜索

算法思想

广度优先遍历(Breadth First Search),又称为广度优先搜索,简称BFS。树的层序遍历方式便是一种广度优先搜索方式。为了清晰地理解广度优先搜索,我们同样以深度优先搜索的例子一起走一遍,这不过,我们对图中顶点的位置做了调整,这样看起来更清楚。

假定从顶点A开始进行广度优先搜索,则遍历的顺序为:

第一步:访问顶点A;

第二步:访问顶点A的所有未被访问的邻接顶点,顶点B和顶点F;

第三步:访问顶点B和顶点F的所有未被访问的邻接顶点,顶点C、I、G、E;

第四步:访问顶点C、I、G、E 的所有邻接顶点中未被访问的顶点,顶点D和顶点H。

BFS的实现

小禹禹:广度优先遍历的步骤好少呀!

景禹:当然不是了,景禹只是给你们展示了一层一层遍历的过程,并没有展示每一层具体如何被访问,这就要考虑到 BFS 的实现了。

那么要实现对图的广度优先遍历,显然和树的层序遍历一样,采用队列来实现。

动画演示:

实现代码:

// 邻接矩阵的广度遍历算法
void BFSTraverse(MGraph G)
{int i, j;Queue Q;for( i=0; i < G.numVertexes; i++ ){visited[i] = FALSE;}initQueue( &Q );for( i=0; i < G.numVertexes; i++ ){if( !visited[i] ){printf("%c ", G.vex[i]);visited[i] = TRUE;EnQueue(&Q, i);while( !QueueEmpty(Q) ){DeQueue(&Q, &i);for( j=0; j < G.numVertexes; j++ ){if( G.art[i][j]==1 && !visited[j] ){printf("%c ", G.vex[j]);visited[j] = TRUE;EnQueue(&Q, j);}}}}}
}

BFS应用案例:单词接龙

LeetCode 127. Word Ladder 单词接龙(Medium)

题目描述

给定两个单词(beginWord 和 endWord)和一个字典,找到从 beginWord 到 endWord 的最短转换序列的长度。转换需遵循如下规则:

每次转换只能改变一个字母。转换过程中的中间单词必须是字典中的单词。

说明:

如果不存在这样的转换序列,返回 0。所有单词具有相同的长度。所有单词只由小写字母组成。字典中不存在重复的单词。你可以假设 beginWord 和 endWord 是非空的,且二者不相同。

示例

输入: beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"]

输出: 5

解释: 一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog", 返回它的长度 5。

题目解析

拥有一个 beginWord 和一个 endWord,分别表示图上起始顶点和结束顶点(图中用红色表示)。我们希望利用一些中间顶点(单词)从起始顶点到结束顶点,中间节点是 wordList 给定的单词。我们对这个单词接龙每个步骤的唯一条件是相邻单词只可以改变一个字母。

当然现在问题的关键就构造根据 wordlist 中的单词构建出上面这样的一幅图,可我们该如何构造呢?我们可以对 wordlist 中的单词之间只改变一个字母就可以转化为另一个的单词之间建立一条连边,但是直接遍历  wordlist  并构建出上面的图是很困难的,所以我们考虑两个单词的共有部分作为连接点,什么意思呢,看下图。

我们对给定的 wordList 做一个预处理,将单词中的某个字母用 * 代替。也就是上图中样子。

既然要构建图,免不了谈及存储结构,对于这道题目最好的存储结构就是邻接表(关于图的存储结构可以参考之前的文章:图解:什么是图?(以“图”话图) )。

邻接表中,我们以单词中的某个字母用 * 代替后则字符串作为关键字 key ,每一个与  key  邻接的单词(顶点)为除 * 所占的字符以外,另外两个字符相同的单词。我们以示例中输入为例,一步一步的看一下该题目中邻接表的构建过程。

hot 中的每一个字符替换后加入邻接表中;

将其他字符依次以相同的方式加入邻接表中,则可以得到由 wordList  所构建的邻接表:

有了这个邻接表,我们便可以通过 BFS 遍历邻接表,判断是否存在从单词(顶点) hitcog 的路径,为了清晰的展示算法执行过程,可以将邻接表转化为图的形式:

后面就是进行广度优先搜索了,采用队列进行实现即可。完整的实现代码如下:

public class Solution {public static int ladderLength(String beginWord, String endWord, List<String> wordList) {if (!wordList.contains(endWord)){return 0;} int len = beginWord.length();HashMap<String, ArrayList<String>> allComboDict = new HashMap<>();//创建邻接链表wordList.forEach(curWord -> {for (int i = 0; i < len; i++) {String comboWord = curWord.substring(0, i) + "*" + curWord.substring(i + 1, len);ArrayList<String> comboWordList = allComboDict.getOrDefault(comboWord, new ArrayList<>());comboWordList.add(curWord);allComboDict.put(comboWord, comboWordList);}});// 广度优先遍历队列Queue<Pair<String, Integer>> queue = new LinkedList<>();HashMap<String, Boolean> hasVistedList = new HashMap<>();queue.add(new Pair<>(beginWord, 1));hasVistedList.put(beginWord, true);// 广度优先遍历,逐个取出队列中元素进行操作while (!queue.isEmpty()) {Pair<String, Integer> node = queue.remove();String currWord = node.getKey();int level = node.getValue();for (int i = 0; i < len; i++) {String currComboWord = currWord.substring(0, i) + "*" + currWord.substring(i + 1, len);ArrayList<String> currComboWordList = allComboDict.getOrDefault(currComboWord, new ArrayList<>());for (String word : currComboWordList) {if (word.equals(endWord))return level + 1;if (!hasVistedList.containsKey(word)){queue.add(new Pair<>(word, level + 1));hasVistedList.put(word, true);}}}}return 0;}
}

总结

图的遍历方式包括深度优先搜索(DFS)和广度优先搜索(BFS),其中 DFS 使用递归或栈进行实现,而 BFS 则采用队列进行实现。对比树的四种遍历方式,前序遍历、中序遍历和后序遍历均类似于 DFS,而层序遍历类似于 BFS,前中后序也均可采用栈的方式进行实现,层序遍历可以采用队列的方式进行实现。

这样看来,知识的融会贯通多么重要,总体而言,掌握下面的两条链,你便可以解决好多问题。

DFS 前中后序  栈 线性表

BFS  层序遍历  队列 链表

最后祝大家学习愉快,有所收获,别忘了点击右下角的在看奥!么么哒


推荐阅读

•   C++是如何从代码到游戏的?•   告诉你一个学习编程的诀窍(建议收藏)•   自学编程的八大误区!克服它!•   新手如何有效的刷算法题(LeetCode)•   10款VS Code插件神器,第7款超级实用!•   在拼多多上班,是一种什么样的体验?我tm心态崩了呀!•   写给小白,从零开始拥有一个酷炫上线的网站!


欢迎关注我的公众号“五分钟学算法”,如果喜欢,麻烦点一下“在看”~

动画解析:图的遍历方式有哪些?相关推荐

  1. 图论算法——图的遍历

    图论算法也是非常基础且重要的算法(ps:好像没有不重要的......) 图的基本应用--图的遍历,从具体的题目着手,学习图的遍历方式及代码形式. 我们先来看一下题目,然后再具体分析图的遍历方式. 题目 ...

  2. 图的遍历(BFS、DFS)

    前文使用邻接矩阵法和邻接链表法实现了图结构(MatrixGraph .ListGraph ),本节使用两种算法进行图的遍历 目录 1.定义与概括 2.广度优先搜索算法 3.深度优先搜索算法 4.小结 ...

  3. 图的遍历——深度优先遍历与广度优先遍历

    目录 何谓遍历? 图的遍历特点 图的遍历方式 深度优先搜索 过程分析 案例分析: 算法的代码实现 测试案例: 测试结果如下: 遍历非连通图 算法复杂度分析 额外补充 广度优先搜索 过程分析 辅助队列 ...

  4. 三十张图片让你彻底弄明白图的两种遍历方式:DFS和BFS

    1 引言   遍历是指从某个节点出发,按照一定的的搜索路线,依次访问对数据结构中的全部节点,且每个节点仅访问一次.图的遍历.遍历过程中得到的顶点序列称为图遍历序列. 2 深度优先搜索 2.1 算法思想 ...

  5. 有向图的广度优先遍历_图的两种遍历方式

    1 引言 遍历是指从某个节点出发,按照一定的的搜索路线,依次访问对数据结构中的全部节点,且每个节点仅访问一次. 在二叉树基础中,介绍了对于树的遍历.树的遍历是指从根节点出发,按照一定的访问规则,依次访 ...

  6. lisp遍历表中所有顶点_三十张图片让你彻底弄明白图的两种遍历方式:DFS和BFS...

    1 引言   遍历是指从某个节点出发,按照一定的的搜索路线,依次访问对数据结构中的全部节点,且每个节点仅访问一次.   在二叉树基础中,介绍了对于树的遍历.树的遍历是指从根节点出发,按照一定的访问规则 ...

  7. 分别用邻接矩阵和邻接表实现图的深度优先遍历和广度优先遍历_数据结构与算法:三十张图弄懂「图的两种遍历方式」...

    原创: 进击的HelloWorld1 引言遍历是指从某个节点出发,按照一定的的搜索路线,依次访问对数据结构中的全部节点,且每个节点仅访问一次. 在二叉树基础中,介绍了对于树的遍历.树的遍历是指从根节点 ...

  8. c++层次遍历_数据结构与算法,弄懂图的两种遍历方式

    1 引言   遍历是指从某个节点出发,按照一定的的搜索路线,依次访问对数据结构中的全部节点,且每个节点仅访问一次.  在二叉树基础中,介绍了对于树的遍历.树的遍历是指从根节点出发,按照一定的访问规则, ...

  9. 深度优先遍历访问的边集合_数据结构与算法: 三十张图弄懂「图的两种遍历方式」...

    1 引言 遍历是指从某个节点出发,按照一定的的搜索路线,依次访问对数据结构中的全部节点,且每个节点仅访问一次. 在二叉树基础中,介绍了对于树的遍历.树的遍历是指从根节点出发,按照一定的访问规则,依次访 ...

最新文章

  1. 挣值管理不是搞数字游戏
  2. Kubernetes日志分析利器:Elassandra部署使用指南
  3. C利用time函数实现简单的定时器
  4. sjms-2 创建型模式
  5. [填坑手册]小程序目录结构和组件化使用心得
  6. Centos 配置多个虚拟IP
  7. ORA-12505,TNS:listener does not currently know of SID given in connect descriptor(不知道的SID)
  8. SQL数据库实战之查找当前薪水详情以及部门编号dept_no
  9. jQuery-1.9.1源码分析系列(十) 事件系统——事件包装
  10. Mybatis之插入ListT
  11. android全局屏幕自动旋转,Android屏幕旋转
  12. MATLAB app designer 如何设置背景图片
  13. 微信新BUG曝光:好友偷偷删了你,用这样一串代码就能查出来!
  14. PAT1023 组个最小数给定数字 0-9 各若干个。你可以以任意顺序排列这些数字,但必须全部使用。目标是使得最后得到的数尽可能小例如:给定两个0两个1三个5一个8得到的数字就是10015558
  15. 基于Linux系统的边界网关协议的设计与实现
  16. 中间件选型标准和流程
  17. 经验分享:一套完整的硬件电路设计该怎么做?
  18. VIT Adapter【Vision Transformer Adapter for Dense Predictions】论文笔记
  19. 轻生女孩跳下地铁站台被4节车厢滑过生还(图)
  20. 携手并进,合作共赢 ▏麒麟信安授权北京辰极智程为京、津、冀省级区域总代理签约仪式举行

热门文章

  1. 吉利车机安装第三方软件教程,手机修改dns完整操作教程
  2. 计算机和遥感哪个专业好啊,遥感科学与技术专业毕业是干什么的
  3. fiddle 下载及配置
  4. MyBatis中如何使用insert标签呢?
  5. FBX导入错误 :ImportFBX Errors:
  6. 精通 Grails: 用 JSON 和 Ajax 实现异步 Grails
  7. 银行外包Java开发的一些感想2023.2.5
  8. SystemUi概述
  9. execute、executeQuery、executeUpdate的用途及返回值
  10. Google Earth Engine学习笔记(一)