数据结构与算法--图的搜索(深度优先和广度优先)

有时候我们需要系统地检查每一个顶点或者每一条边来获取图的各种性质,为此需要从图的某个顶点出发,访遍图中其余顶点,且使得每一个顶点只被访问一次,这个过程就称为图的搜索或者图的遍历。如果限制某个顶点只被访问一次?我们可以建立一个布尔数组,在某个顶点第一次被访问时,将该顶点在数组中对应的下标设置为true。图的搜索通常由两种方案——深度优先搜索和广度优先搜索。

深度优先搜索

深度优先搜索(Depth First Search),简称DFS,该方法主要思想是:

从某一个顶点开始,选择一条没有到达过的顶点(布尔数组中对应的值为false)

标记刚选择的顶点为“访问过”(布尔数组中对应的值设置为true)

来到某个顶点,如果该顶点周围的顶点都访问过了,返回到上个顶点

当回退后的顶点依然是上述情况,继续返回

这听起来像是递归。没错,代码确实是递归实现的,并且实现起来特别简单。

package Chap7;

import java.util.Arrays;

import java.util.List;

public class DepthFirstSearch {

// 用来标记已经访问过的顶点,保证每个顶点值访问一次

private boolean[] marked;

// s为搜索的起点

public DepthFirstSearch(UndiGraph> graph, int s) {

marked = new boolean[graph.vertexNum()];

dfs(graph, s);

}

private void dfs(UndiGraph> graph, int v) {

// 将刚访问到的顶点设置标志

marked[v] = true;

// 打印刚访问的顶点,可换成其他操作

System.out.println(v);

// 从v的所有邻接点中选择一个没有被访问过的顶点

for (int w : graph.adj(v)) {

if (!marked[w]) {

dfs(graph, w);

}

}

}

public static void main(String[] args) {

List vertexInfo = Arrays.asList("v0", "v2", "v3", "v4", "v5");

int[][] edges = {{0, 1}, {0, 2}, {0, 3},

{1, 3}, {1, 4},

{2, 4}};

UndiGraph graph = new UndiGraph<>(vertexInfo, edges);

DepthFirstSearch search = new DepthFirstSearch(graph, 0);

}

}

从代码中看出,深度优先搜索其实就两步:

标记访问过的顶点

递归地访问当前顶点所有没有被标记过的邻居顶点。

在上面的实现中,我们对访问的每个顶点执行了打印操作。打印只是告诉我们搜索的顺序。不过我们很想知道从某个起点开始到另一个顶点的路径。

为此我们用到了一个edgeTo[]的整型数组,这个数组可以记住每个顶点到起点的路径,而不是记录当前顶点到起点的路径。为了做到这一点在由边v-w第一次访问任意w时,将edgeTo[w]设为v,表示v-w是起点s到w的路径上的最后一条已知的边。比如0-2-3-5,表示从0到5的路径,那么edgeTo[5] = 3。同理如果只是到3的路径,那么edgeTo[3] =2, 到2的路径是edgeTo[2] = 0。这样,我们得到的edgeTo[]其实是一棵根结点为起点的树,而且数组里存的是下标的父结点。就像下图一样。

image

edgeTo[1]= 2,而结点1的父结点就是结点2;edgeTo[2] = 0,而顶点2的父结点就是结点0,这和树的双亲表示法有点类似。不存在给edge[0](根结点)赋值的情况,因为此例中我们的起点是顶点0,所以edgeTo[0]保持默认值0。从树中可以看出,起点到顶点5的路径是0-2-3-5。如果我们写一个方法pathTo(),若传入5,只能先获取到edgetTo[5],得到父结点为3,然后根据edgeTo[3]得到父结点2...一直到获取到根结点,可以看到获取的顺序是从叶子结点到根结点,但是真正输出路径的时候是从根结点到叶子结点,所以利用栈(Stack)可以实现这一过程。稍微想一下,先入栈的5被排在了底下,最后入栈的0排在了最顶上,确实是这样的。

现在来实现。

package Chap7;

import java.util.Arrays;

import java.util.LinkedList;

import java.util.List;

public class DepthFirstSearch {

// 用来标记已经访问过的顶点,保证每个顶点值访问一次

private boolean[] marked;

// 起点

private final int s;

// 到该顶点的路径上的最后一条边

private int[] edgeTo;

public DepthFirstSearch(UndiGraph> graph, int s) {

this.s = s;

marked = new boolean[graph.vertexNum()];

edgeTo = new int[graph.vertexNum()];

dfs(graph, s);

}

private void dfs(UndiGraph> graph, int v) {

// 将刚访问到的顶点设置标志

marked[v] = true;

// System.out.println(v);

// 从v的所有邻接点中选择一个没有被访问过的顶点

for (int w : graph.adj(v)) {

if (!marked[w]) {

edgeTo[w] = v;

dfs(graph, w);

}

}

}

// 连通图的任意一个顶点都有某条路径能到达任意一个顶点,如果v在这个连通图中,必然存在起点到v的路径

// 现在marked数组中的值都是true,所以数组中若有这个v(在这个连通图中), 返回true就表示路径存在

public boolean hasPathTo(int v) {

return marked[v];

}

public Iterable pathTo(int v) {

if (hasPathTo(v)) {

LinkedList path = new LinkedList<>();

for (int i = v; i != s; i = edgeTo[i]) {

path.push(i);

}

// 最后将根结点压入

path.push(s);

return path;

}

// 到v不存在路径,就返回null

return null;

}

public void printPathTo(int v) {

System.out.print(s+" to "+ v+": ");

if (hasPathTo(v)) {

for (int i : pathTo(v)) {

if (i == s) {

System.out.print(i);

} else {

System.out.print("-" + i);

}

}

System.out.println();

} else {

System.out.println("不存在路径!");

}

}

public static void main(String[] args) {

List vertexInfo = Arrays.asList("v0", "v1", "v2", "v3", "v4", "v5");

int[][] edges = {{3, 5},{0, 2}, {0, 1}, {0, 5},

{1, 2}, {3, 4}, {2, 3}, {2, 4}};

UndiGraph graph = new UndiGraph<>(vertexInfo, edges);

DepthFirstSearch search = new DepthFirstSearch(graph, 0);

search.printPathTo(4);

}

}

/* Output

0 to 5: 0-2-3-5

*/

只是在深度优先搜索的实现中新加了一些东西,最重要的是在dfs递归方法中插入了一行edgeTo[w] = v;,我们知道w是v的一个邻接点,那么这行的字面意思就是到w的顶点是v,即路径v-w。上面提到过这是起点s到w的最后一条边。

扩展的方法hasPathTo用来判断是否有到某顶点的路径,由于这是连通图,任意一个顶点(包括起点s)都有某条路径能到达任意一个顶点,在初始化该类时,已经调用过深度优先搜索,所以marked数组里都是true,这意味着只要某个顶点能在marked数组中找到对应的下标,那么返回true,表示肯定存在到它的路径。

我们的pathTo用来确定一条从起点到指定顶点的路径,注意这条路径不一定是最短的,也可能并非是唯一路径。必须先判断是否存在到该指定顶点的路径,如果不存在则返回null;若存在,则从查找的顶点开始入栈,i = edgeTo[i]表示树向上一层,更新当前值为结点i的父结点,直到根结点停止,由条件i != s可知,根结点并没有入栈,所以在循环之后要将根结点入栈。

printPathTo就是将pathTo返回的内容格式化输出,就像这样。表示顶点0到顶点5的路径是0-2-3-5。

0 to 5: 0-2-3-5

可见这路径并不是最短路径,0-5直接可达才是最短的。

现在我们来看下深度优先搜索的详细轨迹,注意对照着上图的邻接表:先是从起点0开始

因为2是0的邻接表第一个元素且没有被标记访问,则递归调用它并标记。edgeTo[2] = 0表示0-2这条路径。

现在顶点0是顶点2的邻接表第一个元素,但是0已经被标记了,所以跳过它,看下一个。1没有被标记,所以递归调用它,并标记。edgeTo[1] = 2,表示0-2-1这条路径。

顶点1的邻接表元素都被标记过了,所以不再递归,方法从dfs(1)中返回到上一个顶点2,现在检查2的下一个邻接顶点,3没被标记所以递归它并标记,edgeTo[3] = 2表示0-2-3这条路径。

顶点5是3的邻接表第一个元素没被标记,递归调用它并标记,edgeTo[5] = 3表示0-2-3-5这条路径。

顶点5的邻接表元素都被标记过了,方法从dfs (5)中返回到带上一个顶点3,检查3的邻接表下一个元素,4没有被标记,所以递归它并标记,edgeTo[4] = 3表示0-2-3-4。至此,所有顶点都被标记过。搜索算是完成了。

DFS的非递归版本

DFS也可以自己设一个栈模拟系统栈,下面是非递归版本。

/**

* 非递归实现DFS

*

* @param graph 图

* @param s 起点

*/

public void dfs(UndiGraph> graph, int s) {

boolean[] marked = new boolean[graph.vertexNum()];

// 模拟系统栈

LinkedList stack = new LinkedList<>();

// 起点先入栈

stack.push(s);

// 标记访问

marked[s] = true;

System.out.print(s);

while (!stack.isEmpty()) {

// 取出刚访问的顶点

int v = stack.peek();

for (int w : graph.adj(v)) {

if (!marked[w]) {

marked[w] = true;

System.out.print(w);

stack.push(w);

// 模拟DFS只存入一个就好,一定要break

break;

}

}

// 所有邻接点都被访问过了,模拟递归的返回

stack.pop();

}

}

广度优先搜索

深度优先搜索得到的路径上面已经看到,并不是最短路径,很自然地我们对下面的问题感到兴趣:单点最短路径,即给定一个图和一个起点s,是否存在到给定顶点v的一条路径,如果有找出最短的那条。

解决这个问题方法是广度优先搜索(Breadth First Search),简称BFS。

这个算法的思想大体是:

从起点开始,标记之并加入队列。

起点出列,其所有未被标记的邻接点在被标记后,入列。

队列头的元素出列,将该元素的所有未被标记的邻接点标记后,入列。

如此反复,当队列为空时,所有顶点也都被标记过了。不像DFS的递归那样隐式地使用栈(系统管理的,以支持递归),BFS使用了队列。

package Chap7;

import java.util.Arrays;

import java.util.LinkedList;

import java.util.List;

import java.util.Queue;

public class BreadthFirstSearch {

// 用来标记已经访问过的顶点,保证每个顶点值访问一次

private boolean[] marked;

// 起点

private final int s;

// 到该顶点的路径上的最后一条边

private int[] edgeTo;

public BreadthFirstSearch(UndiGraph> graph, int s) {

this.s = s;

marked = new boolean[graph.vertexNum()];

edgeTo = new int[graph.vertexNum()];

bfs(graph, s);

}

public void bfs(UndiGraph> graph, int s) {

marked[s] = true;

// offer入列, poll出列

Queue queue = new LinkedList<>();

queue.offer(s);

while (!queue.isEmpty()) {

int v = queue.poll();

// System.out.print(v+" ");

for (int w: graph.adj(v)) {

if (!marked[w]) {

edgeTo[w] = v;

marked[w] = true;

queue.offer(w);

}

}

}

}

public boolean hasPathTo(int v) {

return marked[v];

}

public Iterable pathTo(int v) {

if (hasPathTo(v)) {

LinkedList path = new LinkedList<>();

for (int i = v; i != s; i = edgeTo[i]) {

path.push(i);

}

// 最后将根结点压入

path.push(s);

return path;

}

// 到v不存在路径,就返回null

return null;

}

public void printPathTo(int v) {

System.out.print(s+" to "+ v+": ");

if (hasPathTo(v)) {

for (int i : pathTo(v)) {

if (i == s) {

System.out.print(i);

} else {

System.out.print("-" + i);

}

}

System.out.println();

} else {

System.out.println("不存在路径!");

}

}

public static void main(String[] args) {

List vertexInfo = Arrays.asList("v0", "v1", "v2", "v3", "v4", "v5");

int[][] edges = {{3, 5},{0, 2}, {0, 1}, {0, 5},

{1, 2}, {3, 4}, {2, 3}, {2, 4}};

UndiGraph graph = new UndiGraph<>(vertexInfo, edges);

BreadthFirstSearch search = new BreadthFirstSearch(graph, 0);

search.printPathTo(5);

}

}

/* Output

0 to 5: 0-5

*/

我把打印操作注释了,实际上它会输出如下内容

0 2 1 5 3 4

先是打印了起点,然后依次打印了0的所有邻接点2、1、5,之后按照队列的出列顺序,打印2的所有未被标记的邻接点,实际上这已经打印完了所有顶点。而且从代码里也能看出,不像DFS那样每次只标记一个顶点,BFS每次都标记了若干顶点。

上面的代码中,除了bfs的实现代码,其余有关path的方法可以直接使用DFS中的实现。还是来看下详细的搜索轨迹。

首先顶点0入列

顶点0出列,将它所有邻接点2、1、5(参考DFS中的邻接表图片),标记他们。且edgeTo[2]、edgeTo[1]、edgeTo[5]的值都设为0

顶点2出列,检查它的邻接点,0、1已经被标记,于是将3、4入列,并标记它们。edgeTo[3]、edgeTo[4]的值都设为2

顶点1出列,其邻接点均已被标记

顶点5出列,其邻接点均已被标记

顶点3出列,其邻接点均已被标记

顶点4出列,其邻接点均已被标记

可以发现,实际上标记工作和edgeTo数组在第三步之后就已经完成,之后的工作只是检查出列的顶点的邻接点是否被标记过而已。

我们不妨打印下起点到其余各个顶点的路径

0 to 5: 0-5

0 to 4: 0-2-4

0 to 3: 0-2-3

0 to 2: 0-2

0 to 1: 0-1

不难发现,这些路径都是最短路径。实际上有这么一个命题:对于从s可达的任意顶点v,广度优先搜索都能找到一条从s到v的最短路径。

广度优先搜索是先覆盖起点附近的顶点,只在邻近的所有顶点都被访问后才向前进,其搜索路径短而直接;而深度优先搜索是寻找离起点更远的顶点,只有在碰到周围的邻接点都被访问过了才往回退,选一个近处的顶点,继续深入到更远的地方,其路径长而曲折。

以上深度优先和广度优先的实现对于有向图也是适用的,把接收的参数的换成有向图即可。

by @sunhaiyu

2017.9.19

深度搜索和广度搜索领接表实现_数据结构与算法--图的搜索(深度优先和广度优先)...相关推荐

  1. 深度搜索和广度搜索领接表实现_算法基础04-深度优先搜索、广度优先搜索、二分查找、贪心算法...

    深度优先搜索DFS.广度优先搜索BFS 比较 拿谚语打比方的话,深度优先搜索可以比作打破砂锅问到底.不撞南墙不回头:广度优先搜索则对应广撒网,多敛鱼 两者没有绝对的优劣之分,只是适用场景不同 当解决方 ...

  2. 狄斯奎诺算法 c语言,图的邻接表实现迪杰斯特拉算法(C语言).doc

    图的邻接表实现迪杰斯特拉算法(C语言) /*迪杰斯特拉算法(狄斯奎诺算法)解决的是从源点到其它所有顶点的最短路径问题*/ //算法实现: #include #include #define MAX 2 ...

  3. 狄斯奎诺算法 c语言,图的邻接表实现迪杰斯特拉算法(C语言)

    图的邻接表实现迪杰斯特拉算法(C语言). 迪杰斯特拉算法(狄斯奎诺算法)解决的是从源点到其它所有顶点的最短路径问题. 图的邻接表实现迪杰斯特拉算法(C语言) /*迪杰斯特拉算法(狄斯奎诺算法)解决的是 ...

  4. 分别用邻接矩阵和邻接表实现图的深度优先遍历和广度优先遍历_数据结构与算法学习笔记:图...

    图: 图结构区别于线性结构和树型结构,区别可见下图 逻辑上的图(graph)结构由顶点(vertex)和边(edge)组成. 一个图结构G包含顶点集合V和边集合E,任何两个顶点之间可以有一个边表示两者 ...

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

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

  6. 数据结构与算法--图的广度优先搜索 (BFS)

    广度优先搜索即是 一种"地毯式"层层推进的搜索策略,即先查找离起始顶点最近的,然后是次近的,依次往外搜索. BFS解决的最短路径问题. 采用BFS进行遍历的话,需要依赖队列,先进先 ...

  7. 数据结构二分法算法的步骤_数据结构与算法之算法思想:二分法搜索实现(python)...

    体检代检13918799149全国代人体检代检 评论时间:2021-01-14 21:34:33 南昌体检代检入职江西体检代检▂▂σ微1O611941▂▂南昌体检代检九江体检代检吉安体检代检宜春体检代 ...

  8. 学习sift算法的原理和步骤_深度学习笔记47_你也可以成为梵高_风格迁移算法的原理

    神经风格迁移 可用来干什么? 同deepDream一样是驱动图像修改的一个重大进展.可以把照片风格化,看起来就像是一幅画一样.这些为你提供非常多漂亮的艺术风格,就像是梵高所画的<星夜>. ...

  9. python数据结构视频百度云盘_数据结构与算法Python视频领课

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 课程简介: 本课程包含Python编程基础的基本语法及变量,基本数据结构,Code Structure,Function.让学生在学会Python基础的同 ...

最新文章

  1. 利用***+nat解决客户voip被封锁的问题
  2. 为什么稀疏自编码器很少见到多层的?
  3. java8 注解增强_Java8新增的重复注解功能示例
  4. 闲置服务器装win10系统,求高手帮看一下我这台闲置的老主机还能装win10或者win8.1吗?...
  5. 故宫的“烧脑奇书”又火了!豆瓣9.2分,11种结局,可以玩一年!
  6. 云+X案例展 | 民生类:中国电信天翼云携手国家天文台打造“大国重器”
  7. Bailian2748 全排列【全排列】(POJ NOI0202-1750)
  8. java 字符串 移位_算法学习之字符串左移和右移
  9. python平台无关性_Java是如何实现平台无关性的
  10. 微软Win11与万物互联时代新系统需求更加迫切
  11. ps快速抠头发-庞姿姿
  12. 左耳听风-Equifax信息泄露始末
  13. 解决IE上登陆oracle OEM时报:“证书错误,导航已阻止”的错误
  14. 创建自己的WordPress主题的三种方法
  15. 我的世界bukkit服务器开发教程第一章——开发环境
  16. signature=a335cd7040789f936f75c72e4ba37676,浅谈新教材Reading的整体教学
  17. 《纸牌屋》——交换才是硬道理?
  18. [春秋云镜]CVE-2021-44983
  19. 8年经验分享:想要成为一名合格的软件测试工程师,你得会些啥?
  20. 关于职场的经典视频链接

热门文章

  1. 2020年云计算的十大新兴趋
  2. 分布式系统:一致性协议
  3. 集成源码深度剖析:Fescar x Spring Cloud
  4. 物联网落地三大困境破解
  5. MariaDB强势席卷DB-Engines榜单后续,与阿里云达成全球独家战略合作
  6. 关于大数据你应该了解的五件事儿
  7. 共筑计算新生态 共赢数字新时代
  8. 没错!Python程序员正在消失,HR:你才知道?
  9. Kubernetes 将何去何从?
  10. 华为开源数据虚拟化引擎HetuEngine;全球超算500强:中国上榜数量增加;谷歌收购云计算公司CouldSimple ……...