BFS与DFS套路总结

概述

深度优先遍历和广度优先搜索和广度优先搜索是解决图问题最常见的方式,并且在leetcode中有许多相关的变体,但万变不离其宗,其本质结构或者算法框架时固定的,因此本文BFS和DFS算法的原理总结了对应的算法框架,并提供了几道例题来解决如何使用这些框架。

好,话不多少,我们下边正式开始。

BFS

BFS算法本质上就是从一个图的 起点出发开始搜索找到目标 终点完成搜索。

当然在该算法上会有许多变体比如:

比如迷宫,有些格子是围墙不能走,找到从起点到终点的最短距离。
再比如说连连看游戏,两个方块消除的条件不仅仅是图案相同,还得保证两个方块之间的最短连线不能多于两个拐点。你玩连连看,点击两个坐标,游戏是如何判断它俩的最短连线有几个拐点的?

这些问题背景本质上都可以看成图,都可以看做是 从起点到终点寻找最短路径的长度。

基于以上认识,我们可以将BFS的整个解决分成下边几个步骤:

  1. 起点入队列
  2. 以队列非空为循环条件,进行 节点扩散(将所有队列节点出队(同时判断出队节点是否为目标节点),获取其邻接结点)
  3. 判断获取的节点是否已被遍历,未被遍历节点入队。

进而我们可以整理出如下的 BFS框架

/**
* 给定起始节点start和目标节点target,返回其最短路径长度
**/
int BFS(Node start,Node target){
    Queue q; //核心数据结构
    Set visited: //某些情况下可以通过byte数组来进行代替
    int step = 0; //记录扩散步数
    //起始节点入队列
    q.add(start);
    visited.offer(start);
    while(q not empty) {
        //必须要用sz来保存q.size(),然后扩散sz不能直接使用q.size()
        int sz = q.size();
        //将队列中的节点进行扩散
        for(int i =0 ; i < sz; i++) {
            Node cur = q.poll();
            // 目标节点判断
            if(cur is target) {
                return step;
            }
            // 邻接结点入队列
            for(Node n:cur.adjs) {
                //未访问节点入队列
                if(n is not int visited) {
                    visitd.add(n);
                    q.offer(n);
                }
            }
        }
        // 更新步数
        step++;
    }
}

看到上边的算法框架可能有些同学会有些疑问, 既然已经通过队列判空来作为BFS条件,为何为何每次还要加一个sz来做一轮扩散??

其实这个不难理解,我们此处通过sz来扩散,保证当前节点的所有邻接结点都访问后, 步数再加一,如果不进行扩散的话,每次从队列中取出一个元素进行访问后,都会对步长加1,造成结果偏差。也就是说如果我们在套用BFS时,如果不需要步长(step)的话,其实这一步的扩散也是可以不要的。

首先我们先可以一下克隆图问题

BFS与DFS套路总结

该问题,我们在使用BFS进行解决时,发现:在整个遍历过程中,我们压给不需要步长,因此该问题在套用BFS框架时,就无需进行 扩散。

因此我们可以比较容易的写出下边的解决方案:

  public Node cloneGraph(Node node) {
    if (node == null) {
      return null;
    }
    Queue queue = new LinkedList<>();
    Map map = new HashMap<>();
    queue.add(node);
    map.put(node, new Node(node.val));

    while (!queue.isEmpty()) {
        //无需扩散,亦可以解决
    //  int sz = queue.size();
     // for (int i=0;i

当然,即使加上扩散步骤也不影响问题的解决。

接下来,我们看一个稍微困难的题目。

BFS与DFS套路总结

这个问题粗劣一看好像跟 ,没有任何关系??

首先我们这样想,如果改题目我们不考虑 &#x6B7B;&#x4EA1;&#x6570;&#x5B57;这一限制条件,我们会怎么做?

毫无疑问我们可以进行穷举,先从”0000″开始,每次波动一次锁,可以是[“1000″,”9000″,”0100″,”0900″…”0009”]共八种情况。我们把 每种情况都看做是图的节点,我们会发现所有的情况组合在一起就构成了一个 全连接无向图,而对密码的寻找也就变成在BFS中对target的寻找。很神奇有没有??

接下来我们可以套用模板。写出如下解决代码:

class Solution {
    public int openLock(String[] deadends, String target) {
    // 记录需要跳过的deadends信息
    Set deadSet = new HashSet<>();
    for (String deadStr : deadends) {
      deadSet.add(deadStr);
    }
    int step = 0;
    // 标记已经访问的字符
    Set visited = new HashSet<>();
    Queue queue = new LinkedList<>();
    queue.add("0000");
    visited.add("0000");

    while (!queue.isEmpty()) {
      int sz = queue.size();
      for (int i = 0; i < sz; i++) {
        String cur = queue.poll();
        // 遇到死亡数字结束此次搜寻
        if (deadSet.contains(cur)) {
          continue;
        }
        // 终止条件:找到target
        if (target.equals(cur)) {
          return step;
        }
        // 处理相邻的八种情况
        for (int j = 0; j < 4; j++) {
          String up = plusUp(cur, j);
          if (!visited.contains(up)) {
            visited.add(up);
            queue.add(up);
          }
          String down = plusDown(cur,j);
          if (!visited.contains(down)){
            visited.add(down);
            queue.add(down);
          }
        }
      }
      step ++;
    }
    return -1;
  }
//向上拨动第j位锁
  private String plusUp(String str, int j) {
    char[] strArray = str.toCharArray();
    if (strArray[j] == '9') {
      strArray[j] = '0';
    } else {
      strArray[j] += 1;
    }
    return new String(strArray);
  }
//向下波动第j位锁
  private String plusDown(String str, int j) {
    char[] strArray = str.toCharArray();
    if (strArray[j] == '0') {
      strArray[j] = '9';
    } else {
      strArray[j] -= 1;
    }
    return new String(strArray);
  }
}

3.双向BFS优化

BFS与DFS套路总结

上边我们通过BFS已经能够解决大部分问题,但是对于BFS的性能我们还是可以通过一些方法来进行优化。比如我们可以尝试通过 双向BFS来进行优化

那什么是双向BFS??

传统的BFS是从起点开始向四周进行扩散,而双向BFS则是从起点和终点同时进行扩散,直到两者相交在一起结束。

虽然理论上讲两者的最坏时间复杂度都是O(N),但实际在运行时,确实双向BFS的性能会更好一点,这是为什么那??

我们可以借助下面两张图辅助进行理解。

BFS与DFS套路总结

BFS与DFS套路总结

图示中的树形结构,如果终点在最底部,按照传统 BFS 算法的策略,会把整棵树的节点都搜索一遍,最后找到 target;而双向 BFS 其实只遍历了半棵树就出现了交集,也就是找到了最短距离。从这个例子可以直观地感受到,双向 BFS 是要比传统 BFS 高效的。

但是双向BFS最大的局限性就是, 必须知道终点在哪里,比如第一个克隆图的问题,我们便不能通过双向BFS来进行解决。而对二个问题,我们便可以采用。

int openLock(String[] deadends, String target) {
    Set deads = new HashSet<>();
    for (String s : deadends) deads.add(s);
    // 用集合不用队列,可以快速判断元素是否存在
    Set q1 = new HashSet<>();
    Set q2 = new HashSet<>();
    Set visited = new HashSet<>();

    int step = 0;
    q1.add("0000");
    q2.add(target);

    while (!q1.isEmpty() && !q2.isEmpty()) {
        // 哈希集合在遍历的过程中不能修改,用 temp 存储扩散结果
        Set temp = new HashSet<>();

        /* 将 q1 中的所有节点向周围扩散 */
        for (String cur : q1) {
            /* 判断是否到达终点 */
            if (deads.contains(cur))
                continue;
            if (q2.contains(cur))
                return step;
            visited.add(cur);

            /* 将一个节点的未遍历相邻节点加入集合 */
            for (int j = 0; j < 4; j++) {
                String up = plusOne(cur, j);
                if (!visited.contains(up))
                    temp.add(up);
                String down = minusOne(cur, j);
                if (!visited.contains(down))
                    temp.add(down);
            }
        }
        /* 在这里增加步数 */
        step++;
        // temp 相当于 q1
        // 这里交换 q1 q2,下一轮 while 就是扩散 q2
        q1 = q2;
        q2 = temp;
    }
    return -1;
}

简单来看的话,双向 BFS 还是遵循 BFS 算法框架的,只是 不再使用队列,而是使用 HashSet 方便快速判断两个集合是否有交集

DFS

与广度优先搜索不同,深度优先搜索(DFS)类似于树的 先序遍历。在搜索时 会尽可能的沿着一条所有路径进行搜索,直到该条路径上所有节点搜索完成,然后切换到另一条路径上进行搜索, 直到图的所有节点全部都被遍历

因此广度优先搜索整个过程可以分成如下步骤:

  1. 判断终止条件
  2. 对节点进行访问并加入到访问链表中
  3. 以当前节点的 邻接结点为起点,通过 递归更深层次进行搜索。

即可以简单总结出DFS的模板如下:

Set visited;
void DFS(Node start) {
    //结束条件
    if(shoud be end) {
        return;
    }
    visited.add(start);
    //递归向更深次进行遍历
    for(Node n:start.adjs) {
        if(n is not visited){
            DFS(n);
        }
    }
}

在BFS章节,我们已经通过BFS的方法,解决了该问题,但由于问题本质上就是一个对图进行遍历的问题,只不过需要在遍历的过程中进行复制。因而该问题我们也可以通过DFS来解决。

套用DFS模板可以写出如下代码:

 Map map = new HashMap<>();

  public Node DFS(Node node) {
    // 终止条件
    if (node == null) {
      return node;
    }
    //已经复制过的话,直接返回复制过的节点
    if(map.containsKey(node)) {
      return map.get(node);
    }
    // 标记访问,并创建拷贝节点
    map.put(node, new Node(node.val));
    for (Node n : node.neighbors) {
      //此处不需要进行访问判断,因为即使被访问过也需要加入到邻接结点列表中
      map.get(node).neighbors.add(DFS(n));
    }
    return map.get(node);
  }

总结

从上述内容,我们不难看出BFS相对于DFS来说 两者本质区别在搜索过程中扩散方式不同。BFS在搜索时, “齐头并进”从而使得在搜索的时候,所有节点对位于同一个层级,进而可以帮助我们在不完全遍历整个节点的情况下找到所有的 最短的路径。而DFS由于在搜索时使用的是递归堆栈,最差的空间复杂度是O(logn),要比BFS的O(n)要小得多。两者各有侧重。

参考

  1. https://mp.weixin.qq.com/s/WH_XGm1-w5882PnenymZ7g

Original: https://www.cnblogs.com/goWithHappy/p/bfs-dfs.html
Author: vcjmhg
Title: BFS与DFS套路总结

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/598968/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • centos7安装docker

    一、安装前必读 在安装 Docker 之前,先说一下配置,我这里是Centos7 Linux 内核:官方建议 3.10 以上,3.8以上貌似也可。 注意:本文的命令使用的是 roo…

    数据库 2023年6月14日
    080
  • DHCP:IP 并非与生俱来

    初识 DHCP 众所周知,因特网上的每台设备都规定了其全世界唯一的地址,也就是说 “IP 地址”,正是由于有了 IP 地址,才保证了用户在连网的计算机上操作…

    数据库 2023年6月6日
    0111
  • 数据类型拓展

    public class Demo03 { public static void main(String[] args) { //整数拓展 :进制 二进制0b 十进制 八进制0 十…

    数据库 2023年6月11日
    065
  • Jmeter性能测试场景的创建和运行

    目录 性能测试场景的分析 项目背景 Jmeter指标 性能测试场景的设计以及准备 * 性能测试的总结 性能测试场景的分析 项目背景 ​ 实际工作中,我们拿到一个项目一般来说都会是项…

    数据库 2023年6月6日
    091
  • 2022-8-31 jsp el表达式

    jsp 注意:1、JSP脚本片段中只能出现java代码,不能出现HTML元素。在 访问JSP时,JSP引擎翻译JSP页面中的脚本片段。2、JSP脚本片段中的java代码必须严格遵守…

    数据库 2023年6月14日
    070
  • HTAP的关键技术有哪些?| StoneDB学术分享会第③期

    在最新一届国际数据库顶级会议 ACM SIGMOD 2022 上,来自清华大学的李国良和张超两位老师发表了一篇论文:《HTAP Database: What is New and …

    数据库 2023年6月11日
    0141
  • Harsh =哈希 =散列

    key-hash-%-index Harsh =哈希 =散列 HarshCode=哈希码=哈希代码=散列码=散列值 哈希函数=散列函数=哈希算法=Harsh Algorithm 散…

    数据库 2023年6月14日
    074
  • Jmeter环境变量配置你不得不知道的事情

    在安装Jmeter的过程中大家肯定需要配置环境,但是为什么要配置JDK的环境变量呢?大家有没有好奇过,有没有仔细去像一下呢,其实在安装Jmeter前,大家应该都知道Jmeter是我…

    数据库 2023年6月6日
    081
  • docker-ckeditor图片img标签style属性自适应

    1,修改ckeditor的源码cofig.js文件 // &#x4E0D;&#x7ED9;&#x56FE;&#x7247;img&#x6DF…

    数据库 2023年6月9日
    0106
  • 多商户商城系统功能拆解28讲-平台端营销-消费奖励

    多商户商城系统,也称为B2B2C(BBC)平台电商模式多商家商城系统。可以快速帮助企业搭建类似拼多多/京东/天猫/淘宝的综合商城。 多商户商城系统支持商家入驻加盟,同时满足平台自营…

    数据库 2023年6月14日
    091
  • Linux(CentOS)安装Redis保姆级教程

    Linux(CentOs)安装Redis教程 一,下载Redis(两种方式) 1,找到redis官网(https://redis.io/download ) 如果想下载指定版本就去…

    数据库 2023年6月11日
    084
  • 当我用Python做了个自动工作汇报的脚本后,每天都闲的只能摸鱼

    哈喽兄弟们 之前经常编写Python脚本来进行数据处理、数据传输和模型训练。随着数据量和数据复杂性的增加,运行脚本可能需要一些时间。在等待数据处理完成时可以同时做一些其他工作。 为…

    数据库 2023年6月14日
    073
  • 2022-8-24 js

    JavaScript脚本语言,解释性 &#x4E3B;&#x8981;&#x7ED9;HTML&#x7F51;&#x9875;&#x…

    数据库 2023年6月14日
    081
  • SQL优化这5个极简法则,直接让查询原地起飞!

    SQL 作为关系型数据库的标准语言,是 IT 从业人员必不可少的技能之一。SQL 本身并不难学,编写查询语句也很容易,但是想要编写出能够高效运行的查询语句却有一定的难度。 查询优化…

    数据库 2023年5月24日
    080
  • gauss杀进程

    1)查询当前所有连接的状态 select datname,pid,application_name,state from pg_stat_activity; 2)关闭当前state…

    数据库 2023年6月16日
    091
  • Redis集群(二)哨兵模式

    一、作用和架构 1. 作用 Redis Sentinel,即Redis哨兵,在Redis 2.8版本开始引入。哨兵的核心功能是 主节点的自动故障转移。下面是Redis官方文档对于哨…

    数据库 2023年6月11日
    089
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球