算法是作用于具体数据结构之上的,深度优先搜索算法和广度优先搜索算法都是基于“图”这种数据结构的。这是因为,图这种数据结构的表达能力很强,大部分涉及搜索的场景都可以抽象成“图”。

图上的搜索算法,最直接的理解就是,在图中找出从一个顶点出发,到另一个顶点的路径。为了搞清楚图的搜索算法,必须先把图的存储方式理解透彻。

图的存储

图有多种存储方法,最常用的两种分别是:邻接矩阵和出边数组。

邻接矩阵的存储方式如下图所示,底层依赖一个二维数组。

对于无向图来说,如果顶点i与顶点j之间有边,我们就将A[i][j]和 A[j][i]标记1;对于有向图来说,如果顶点i到顶点j之间,有一条箭头从顶点i指向顶点j的边,那我们就将A[i][j]标记为1。同理,如果有一条箭头从顶点j指向顶点i的边,我们就将A[j][i]标记为1。对于带权图,数组中就存储相应的权重。

用邻接矩阵来表示一个图,虽然简单、直观,但是比较浪费存储空间。对于无向图来说,如果A[i][j]等于1,那A[j][i]也肯定等于1。实际上,我们只需要存储一个就可以了。也就是说,无向图的二维数组中,如果我们将其用对角线划分为上下两部分,那我们只需要利用上面或者下面这样一半的空间就足够了,另外一半白白浪费掉了。

针对上面邻接矩阵比较浪费内存空间的问题,出边数组可以避免空间浪费。如下图所示,每个顶点对应一条链表,链表中存储的是与这个顶点相连接的其他顶点。

例如上图中,我们要确定,是否存在一条从顶点2到顶点4的边,那我们就要遍历顶点2的所有出边,看出边是否能到达顶点4。所以,比起邻接矩阵的存储方式,在出边数组中查询两个顶点之间的关系就没那么高效了。

//无向图
public class UndirectedGraph { /*** 顶点的个数*/private int v;/*** 邻接表(出边数组)*/private ArrayList<Integer>[] edges;public UndirectedGraph(int v) {this.v = v;edges = new ArrayList[v];for (int i = 0; i < v; ++i) {edges[i] = new ArrayList<>();}}/*** 对于无向图,其实就是点s到点t的两条边* 用两条有向边代表无向图中的一条边*/public void addEdge(int s, int t) {edges[s].add(t);edges[t].add(s);}
}

深度优先搜索(DFS)

深度优先搜索用的是一种比较著名的算法思想,回溯思想。这种思想解决问题的过程,非常适合用递归来实现。例如下图中演示了用深度优先搜索的思想寻找一条从点s到点c的路径的方法。从图中我们可以看出,深度优先搜索找出来的路径,并不是点s到点c的最短路径。

我把上面的过程用递归的形式写下来如下。深度优先搜索代码里,有个比较特殊的全局变量found,它的作用是,当我们已经找到终止点之后,我们就不再递归地继续查找了。

//全局变量或者类成员变量
boolean found = false;
private int  v;
private ArrayList<Integer>[] edges;public void existPath(int start, int t) {boolean[] visited = new boolean[v];recurDfs(start, t, visited);
}private void recurDfs(int current, int t, boolean[] visited) {if (found == true) {return;}visited[current] = true;if (current == t) {found = true;return;}for (int i = 0; i < edges[current].size(); ++i) {int q = edges[current].get(i);if (!visited[q]) {recurDfs(q, t, visited);}}
}

理解了深度优先搜索算法之后,深度优先搜索的时间、空间复杂度是多少呢?从示意图可以看出,每条边最多会被访问两次,一次是遍历,一次是回退。所以,图上的深度优先搜索算法的时间复杂度是O(E),E表示边数。深度优先搜索算法的消耗内存主要是visited、数组和递归调用栈。visited数组的大小跟顶点的个数V成正比,递归调用栈的最大深度不会超过顶点的个数,所以总的空间复杂度就是O(V)。

广度优先搜索(BFS)

广度优先搜索直观地讲,就是一种“地毯式”层层推进的搜索策略,即先查找离起始顶点最近的,然后是次近的,依次往外搜索。理解起来并不难,示意图如下。

尽管广度优先搜索的原理挺简单,但代码实现还是稍微有点复杂度。实际上,这样得到的一条路径就是从起始点到指定点的最短路径。

private boolean existPathBfs(int current, int t) {if (current == t) {return true;}visited[current] = true;Queue<Integer> queue = new ArrayDeque<>();queue.add(current);while (!queue.isEmpty()) {int point = queue.poll();for (int i = 0; i < edges[point].size(); ++i) {int q = edges[point].get(i);if (!visited[q]) {if (q == t) {return true;}queue.add(q);visited[q] = true;}}}return false;
}

BFS代码中的queue是一个队列,用来存储已经被访问、但相连的顶点还没有被访问的顶点。因为广度优先搜索是逐层访问的,也就是说,我们只有把第k层的顶点都访问完成之后,才能访问第k+1层的顶点。当我们访问到第k层的顶点的时候,我们需要把第k层的顶点记录下来,稍后才能通过第k层的顶点来找第k+1层的顶点。所以,我们用这个队列来实现记录的功能。

广度优先搜索的时间、空间复杂度是多少呢?最坏情况下,终止顶点t离起始顶点s很远,需要遍历完整个图才能找到。这个时候,每个顶点都要进出一遍队列,每个边也都会被访问一次,所以,广度优先搜索的时间复杂度是 O(V+E),其中,V表示顶点的个数,E表示边的个数。当然,对于一个连通图来说,也就是说一个图中的所有顶点都是连通的,E肯定要大于等于V-1,所以,广度优先搜索的时间复杂度也可以简写为O(E)。空间消耗主要在几个辅助变量visited数组、queue队列上。这三个存储空间的大小都不会超过顶点的个数,所以空间复杂度是O(V)。

DFS与BFS的实际应用

样题一:岛屿的数量

给你一个由’1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。此外,你可以假设该网格的四条边均被水包围。

DFS的思路:将二维网格看成一个无向图,竖直或水平相邻的点’1’之间有边相连。为了求出岛屿的数量,我们可以扫描整个二维网格。如果一个位置为’1’,则以其为起始节点开始进行深度优先搜索。在深度优先搜索的过程中,每个搜索到的’1’都会被重新标记。

最终岛屿的数量就是我们进行深度优先搜索的次数。

class Solution {//垂直水平四个方向组成的方向数组private int[] dx = {-1,0,1,0};private int[] dy = {0,1,0,-1};private boolean[][] visited;public int numIslands(char[][] grid) {visited = new boolean[grid.length][grid[0].length];int ans = 0;for (int i = 0; i < grid.length; i++) {for (int j = 0; j < grid[0].length; j++) {if (grid[i][j] == '1' && !visited[i][j]) {//以grid[i][j]这个点为起点,作深度优先遍历dfs(grid, i, j);//递归的次数就是图中陆地独立成片的块数ans++;}}}return ans;}//从点(x,y)出发,深度优先遍历private void dfs(char[][] grid, int x, int y) {//标记当前点grid[x][y]已经访问过了visited[x][y] = true;//然后遍历grid[x][y]点的所有出边,这里就是上下左右4个for (int i = 0; i < 4; i++) {int nx = x + dx[i];int ny = y + dy[i];//判断坐标合法时,才继续遍历if(nx < 0 || nx >= grid.length || ny < 0 || ny >= grid[0].length) {continue;}//只遍历没访问过且为陆地的点if (grid[nx][ny] == '1' && !visited[nx][ny]) {dfs(grid, nx, ny);}}}
}

同样地,我们也可以使用广度优先搜索代替深度优先搜索。

BFS的思路:为了求出岛屿的数量,我们可以扫描整个二维网格。如果一个位置为‘1’,则将其加入队列,开始进行广度优先搜索。在广度优先搜索的过程中,从原点出发能到达的每个点‘1’都会被重新标记。直到队列为空,搜索结束。

最终岛屿的数量就是我们进行广度优先搜索的次数。

class Solution {//垂直水平四个方向组成的方向数组private int[] dx = {-1,0,1,0};private int[] dy = {0,1,0,-1};private boolean[][] visited;public int numIslands(char[][] grid) {visited = new boolean[grid.length][grid[0].length];int ans = 0;for (int i = 0; i < grid.length; i++) {for (int j = 0; j < grid[0].length; j++) {if (grid[i][j] == '1' && !visited[i][j]) {//以grid[i][j]这个点为起点,作广度优先遍历bfs(grid, i, j);ans++;}}}return ans;}//从点(x,y)出发,广度优先遍历private void bfs(char[][] grid, int x, int y) {//标记点(x,y)为已经访问过了visited[x][y] = true;Queue<Pair> queue = new ArrayDeque<>();queue.add(new Pair(x, y));while (!queue.isEmpty()) {Pair pair = queue.poll();//然后遍历grid[x][y]点的所有出边,这里就是上下左右4个for (int i = 0; i < 4; i++) {int nx = pair.row + dx[i];int ny = pair.column + dy[i];//判断坐标合法时,才继续遍历if(nx < 0 || nx >= grid.length || ny < 0 || ny >= grid[0].length) {continue;}//只遍历没访问过且为陆地的点if (grid[nx][ny] == '1' && !visited[nx][ny]) {queue.add(new Pair(nx, ny));//入队时标记visited数组visited[nx][ny] = true;}}}}class Pair {int row;int column;public Pair(int row, int column) {this.row = row;this.column = column;}}
}

样题二:被包围的区域

注意关键解释:被围绕的区域不会存在于边界上,换句话说,任何边界上的 ‘O’ 都不会被填充为 ‘X’。 任何不在边界上,或不与边界上的 ‘O’ 相连的 ‘O’ 最终都会被填充为 ‘X’。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。

问题可以转化为:从边界上的‘O’出发所有能被访问到的‘O’都不能被填充为’X’,剩下的‘O’可以被填充为’X’,这是一个搜索问题。

思路:将所有存在于边界上的点’O’都打上标记,例如标记为’#’,然后从这些点出发作搜索,所有能到达的点’O’也打上标记’#’,最后将矩阵中所有标记为’#‘的点都填充为’O’,所有为’O’的点都填充为’X’。

DFS思路下的代码。

class Solution {private int[] dx = {-1,0,1,0};private int[] dy = {0,1,0,-1};private boolean[][] visited;//寻找和边界连通的O,如果这个O与边界连通,那么不能替换,否则要替换public void solve(char[][] board) {visited = new boolean[board.length][board[0].length];for (int i = 0; i < board.length; i++) {for (int j = 0; j < board[0].length; j++) {if (board[i][j] == 'X') {continue;}//判断点是否在边界上,如果在边界上并且是O,并且没访问过,就访问一次boolean isEdge = (i == 0 || j == 0 || i == board.length - 1 || j == board[0].length - 1);if (isEdge && board[i][j] == 'O') {dfs(board, i, j);}}}for (int i = 0; i < board.length; i++) {for (int j = 0; j < board[0].length; j++) {if (board[i][j] == 'O') {board[i][j] = 'X';} else if (board[i][j] == '#') {board[i][j] = 'O';continue;}}}}//从点(x,y)出发作深度优先遍历,访问从点(x,y)所能到达的所有点private void dfs(char[][] board, int x, int y) {if (board[x][y] == '#') {return;}visited[x][y] = true;board[x][y] = '#';//然后遍历board[x][y]的所有点,这里就是上下左右4个for (int i = 0; i < 4; i++) {int nx = x + dx[i];int ny = y + dy[i];//判断坐标合法时,才继续遍历if(nx < 0 || nx >= board.length || ny < 0 || ny >= board[0].length) {continue;}//只遍历没访问过且为'O'的点if (board[nx][ny] == 'O' && !visited[nx][ny]) {dfs(board, nx, ny);}}}
}

BFS思路下的代码。

class Solution {private int[] dx = {-1,0,1,0};private int[] dy = {0,1,0,-1};private boolean[][] visited;//寻找和边界连通的O,如果这个O与边界连通,那么不能替换,否则要替换public void solve(char[][] board) {visited = new boolean[board.length][board[0].length];for (int i = 0; i < board.length; i++) {for (int j = 0; j < board[0].length; j++) {if (board[i][j] == 'X') {continue;}//判断点是否在边界上,如果在边界上并且是'O',并且没访问过,就访问一次boolean isEdge = (i == 0 || j == 0 || i == board.length - 1 || j == board[0].length - 1);if (isEdge && board[i][j] == 'O') {bfs(board, i, j);}}}for (int i = 0; i < board.length; i++) {for (int j = 0; j < board[0].length; j++) {if (board[i][j] == 'O') {board[i][j] = 'X';} else if (board[i][j] == '#') {board[i][j] = 'O';continue;}}}}//从点(x,y)出发作深度优先遍历,访问从点(x,y)所能到达的所有点private void bfs(char[][] board, int x, int y) {visited[x][y] = true;board[x][y] = '#';Queue<Pair> queue = new ArrayDeque<>();queue.add(new Pair(x, y));while (!queue.isEmpty()) {Pair pair = queue.poll();//然后遍历board[x][y]的所有点,这里就是上下左右4个for (int i = 0; i < 4; i++) {int nx = pair.row + dx[i];int ny = pair.column + dy[i];//判断坐标合法时,才继续遍历if(nx < 0 || nx >= board.length || ny < 0 || ny >= board[0].length) {continue;}//只遍历没访问过且为'O'的点if (board[nx][ny] == 'O' && !visited[nx][ny]) {queue.add(new Pair(nx, ny));visited[nx][ny] = true;board[nx][ny] = '#';}}}}class Pair {int row;int column;public Pair(int row, int column) {this.row = row;this.column = column;}}
}

样题三:省份数量

思路:给出的矩阵isConnected其实就是典型的邻接矩阵,这种图的存储方式很简洁。对于无向图,矩阵按对角线对称。问题可以转化成求无向图中连通块的个数,顶点数量为n,顶点的编号为0到n-1,矩阵isConnected描述了各个顶点的连接情况(矩阵中有一半信息是冗余的)。只需要从一个顶点出发遍历其能到达的其他所有顶点,当前顶点与其能到达的其他顶点构成连通块,统计这样的连通块的个数。

DFS与BFS的思路都能解决此问题,代码如下:

class Solution {//在无向图中,若从顶点a到顶点b有路径,则称a和b是连通的。//若无向图中,任意两个不同的顶点都连通,则称无向图为连通图//求无向图中连通块数量public int findCircleNum(int[][] isConnected) {int ans = 0;//初始化int v = isConnected.length;boolean[] visited = new boolean[v];//无向图中共有v个顶点,顶点编号为0到v-1,找图中连通块的数量for (int i = 0; i < v; i++) {//已经访问过的不再访问if (!visited[i]) {//连通块的数量就是dfs或bfs搜索的次数// dfs(isConnected, i, visited);bfs(isConnected, i, visited);ans++;}}return ans;}//深度优先搜索private void dfs(int[][] isConnected, int cur, boolean[] visited) {//已经访问过了,不再访问if (visited[cur]) {return;}visited[cur] = true;//从点cur出发,遍历其能到达的所有其他点,cur自己除外for (int i = 0; i < isConnected[0].length; i++) {if (i != cur && isConnected[cur][i] == 1) {dfs(isConnected, i, visited);}}}//广度优先搜索private void bfs(int[][] isConnected, int cur, boolean[] visited) {//已经访问过了,不再访问if (visited[cur]) {return;}Queue<Integer> queue = new ArrayDeque<>();visited[cur] = true;queue.add(cur);//从顶点cur出发,遍历其能到达的其他点while (!queue.isEmpty()) {Integer vertex = queue.poll();//从顶点cur出发,遍历其能到达的并且没有访问过的其他点for (int i = 0; i < isConnected[0].length; i++) {if (i != vertex && isConnected[vertex][i] == 1 && visited[i] == false) {queue.add(i);visited[i] = true;}}}}
}

小结

广度优先搜索,通俗的理解就是,地毯式层层推进,从起始顶点开始,依次往外遍历,遍历得到的路径就是,起始顶点到终止顶点的最短路径。广度优先搜索需要借助队列来实现。深度优先搜索用的是回溯思想,非常适合用递归实现。二者是图上的两种最常用、最基本的搜索算法,比起其他高级的搜索算法,比如A*、IDA*等,要简单粗暴,没有什么优化,所以,也被叫作暴力搜索算法。所以,这两种搜索算法仅适用于状态空间不大,也就是说图不大的搜索。

深度优先搜索与广度优先搜索相关推荐

  1. 八数码深度优先搜索_深度优先搜索和广度优先搜索

    深度优先搜索和广度优先搜索 关于搜索&遍历 对于搜索来说,我们绝大多数情况下处理的都是叫 "所谓的暴力搜索" ,或者是说比较简单朴素的搜索,也就是说你在搜索的时候没有任何所 ...

  2. 算法十——深度优先搜索和广度优先搜索

    文章出处:极客时间<数据结构和算法之美>-作者:王争.该系列文章是本人的学习笔记. 搜索算法 算法是作用于数据结构之上的.深度优先搜索.广度优先搜索是作用于图这种数据结构之上的.图上的搜索 ...

  3. 深度优先遍历和广度优先遍历_图与深度优先搜索和广度优先搜索

    什么是图? 图是一种复杂的非线性表结构.由若干给定的点一级任意两点间的连线所构成.图通常用来描述事物之间的特定关系, 代表的就是事物, 线就是事物之间所具有的关系.例如社交网络就是一种典型的图关系, ...

  4. 深度优先搜索和广度优先搜索

    深度优先搜索和广度优先搜索 ​ 在人工智能的运筹学的领域中求解与图相关的应用中,这两个算法被证明是非常有用的,而且,如需高效地研究图的基本性质,例如图的连通性以及图是否存在环,这些算法也是必不可少的. ...

  5. 数据结构学习笔记——图的遍历算法(深度优先搜索和广度优先搜索)

    目录 一.图的遍历概念 二.深度优先搜索(DFS) (一)DFS算法步骤 1.邻接表DFS算法步骤 2.邻接矩阵DFS算法步骤 (二)深度优先生成树.森林 (三)DFS的空间复杂度和时间复杂度 三.广 ...

  6. 迷宫问题:深度优先搜索和广度优先搜索

    迷宫问题:深度优先搜索和广度优先搜索 1.深度优先搜索可以使用栈实现,栈顶元素为当前节点 2.当前节点搜索下一节点,判断节点是否走得通,如果走得通任意方向走一步,走不通一直弹出栈内元素,直到走得通 3 ...

  7. 学会二叉树不知道干啥?二叉树的深度优先搜索和广度优先搜索,我要打十个乃至二十个(打开你的LeetCode撸起来)学练并举

    目录 一. 图解二叉树的深度优先搜索 二. 二叉树的广度优先搜索  (层序遍历) 三. 打开LeetCode 撸起来 至此, 咱多少被刚刚的后序非递归搞得可能有点小晕晕的, 没事,层序简单呀....  ...

  8. 深度优先搜索与广度优先搜索区别和案例

    今天周末,心血来潮打开LeetCode做一道题: https://leetcode-cn.com/problems/number-of-enclaves/ 看到题,我的第一想法是: 从边缘的陆地开始, ...

  9. 根据邻接表求深度优先搜索和广度优先搜索_深度优先搜索/广度优先搜索与java的实现...

    度:某个顶点的度就是依附于该顶点的边的个数 子图:一幅图中所有边(包含依附边的顶点)的子集 路径:是由边顺序连接的一系列定点组成 环:至少含有一条边且终点和起点相同的路径 连通图:如果图中任一个到另一 ...

  10. 深度优先搜索和广度优先搜索的比较与分析

    一)深度优先搜索的特点是: (1)无论问题的内容和性质以及求解要求如何不同,它们的程序结构都是相同的,即都是深度优先算法(一)和深度优先算法(二)中描述的算法结构,不相同的仅仅是存储结点数据结构和产生 ...

最新文章

  1. web release (bat tool)
  2. Qt Creator指定构建设置
  3. 秒懂 QPS、TPS、PV、UV、GMV、IP、RPS!
  4. 作为开发者发布小程序_如何建立个人品牌作为新开发者
  5. linux下打开、关闭tomcat,实时查看tomcat运行日志
  6. pillow模块 (PIL) 生成验证码
  7. 基于matlab的2ask频带传输系统仿真与性能分析,基于MATLAB的2ASK频带传输系统仿真与性能分析汇总...
  8. ios打开html页面关闭当前页面跳转,【已解决】怎么从iOS原生界面跳转回到html页面呢...
  9. smart原则_用SMART原则,定位好副业目标
  10. 想要一款iOS矢量绘图编程软件?推荐来了
  11. 物联网的未来是什么样的
  12. dede搜索结果页列表标题长度修改方法
  13. KJ分析法(亲和图)的应用实例及知识分享
  14. java 线程 中断标志位
  15. wish平台怎么样?wish跨境电商好做吗?
  16. HDU 1849 Rabbit and Grass
  17. 大学计算机基础毕业论文操作步骤,大学计算机基础教学论文论文
  18. LeetCode1309
  19. 斐讯N1 刷机固件怎么切换 进去游戏EMUELEC系统
  20. 宝安无线快充android,华为Mate30 Pro有线无线快充实测

热门文章

  1. 木门企业最典型的十八个问题
  2. 著名的“三门问题”的验证
  3. leetcode 717. 1比特与2比特字符(python)
  4. python大侠个人信息查询_个人信息查询,教你怎么调查一个人的资料
  5. PHPStorm+Xdebug配置(phpStudy)
  6. Windows 7/10下安装Ubuntu 16.04双系统
  7. 关于印发促进智慧城市健康发展的指导意见的通知
  8. 取redis中手机验证码,并验证是否正确
  9. 网上图书商城项目学习笔记-018生成订单
  10. system-config-network