一文带你了解dfs和bfs算法

寻路算法 00_00_00-00_00_30.gif

如上图,dfs和bfs算法通常会用来解决迷宫问题,两种算法都可以找到一条通往重点的路,但又有不一样的地方。

体验地址:http://120.79.163.94/demo/寻路算法.html

可以自己定义迷宫是否可走,及起始点和终点。

深度优先算法(dfs)

简介

dfs算法又称深度优先搜索,是计算机术语。

1、dfs是一种在开发爬虫早期使用较多的方法,是搜索算法的一种。

2、dfs的目的是要达到被搜索结构的叶结点,即那些不包含任何超链的HTML文件。

3、dfs根据已有的邻接矩阵或邻接表用递归方法编写深度优先搜索遍历算法,并输出遍历结果

作为搜索算法的一种,DFS对于寻找一个解的NP(包括NPC)问题作用很大。但是,搜索算法毕竟是时间复杂度是O(n!)的阶乘级算法,它的效率非常低,在数据规模变大时,这种算法就显得力不从心了。当节点v的所有边都己被探寻过,搜索将回溯到发现节点v的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止。属于盲目搜索。

详细解释

事实上,深度优先搜索属于图算法的一种,英文缩写为DFS即Depth First Search.其过程简要来说是对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次.

图

举例说明之:上图是一个无向图,如果我们从A点发起深度优先搜索(以下的访问次序并不是唯一的,第二个点既可以是B也可以是C,D),则我们可能得到如下的一个访问过程:A->B->E(没有路了!回溯到A)->C->F->H->G->D(没有路,最终回溯到A,A也没有未访问的相邻节点,本次搜索结束).简要说明深度优先搜索的特点:每次深度优先搜索的结果必然是图的一个连通分量.深度优先搜索可以从多点发起.如果将每个节点在深度优先搜索过程中的"结束时间"排序(具体做法是创建一个list,然后在每个节点的相邻节点都已被访问的情况下,将该节点加入list结尾,然后逆转整个链表),则我们可以得到所谓的"拓扑排序",即topological sort. [1]

基本思路

深度优先遍历图的方法是,从图中某顶点v出发:

(1)访问顶点v;

(2)依次从v的未被访问的邻接点出发,对图进行深度优先遍历;直至图中和v有路径相通的顶点都被访问;

(3)若此时图中尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行深度优先遍历,直到图中所有顶点均被访问过为止。 当然,当人们刚刚掌握深度优先搜索的时候常常用它来走迷宫.事实上我们还有别的方法,那就是广度优先搜索(BFS).

实际应用

在我们遇到的一些问题当中,有些问题我们不能够确切的找出数学模型,即找不出一种直接求解的方法,解决这一类问题,我们一般采用搜索的方法解决。搜索就是用问题的所有可能去试探,按照一定的顺序、规则,不断去试探,直到找到问题的解,试完了也没有找到解,那就是无解,试探时一定要试探完所有的情况(实际上就是穷举);

代码
function dfs(x, y) {
    const dx = [1, 0, -1, 0],
          dy = [0, 1, 0, -1];
    if (x < 0 || y < 0 || x >= map.length || y >= map[0].length) return;
    if (map[x][y] == 1 || flag[x][y] || findFlag) return;
    startCell.classList.add("now-in");
    if (x == targetX && y == targetY) {
        findFlag = true;
        canFind = true;
        return;
    }
    for (let i = 0; i < 4 && !findFlag; i++) {
        flag[x][y] = true;
        count++;
        if (!findFlag) dfs(x + dx[i], y + dy[i]);
        if (!findFlag) flag[x][y] = false;
        if (!findFlag) startCell.innerHTML = "";
        if (!findFlag) count--;
        if (!findFlag) startCell.classList.remove("pass");
    }
}

如上代码,我设置了算法遍历的方向为(这个可以自己规定),遍历过程类似于堆栈的过程不断试探,不符合则出栈回退,遍历的过程中需要防止走回头路,因此需要标记已经遍历过的坐标,这里我使用了flag来进行标记。

const dx = [1, 0, -1, 0],
    dy = [0, 1, 0, -1];
//即依次为上、右、下、左进行遍历,走不通则回溯

广度优先算法(bfs)

简介

宽度优先搜索算法(又称广度优先搜索)是最简便的图的搜索算法之一,这一算法也是很多重要的图的算法的原型。Dijkstra单源最短路径算法和Prim最小生成树算法都采用了和宽度优先搜索类似的思想。其别名又叫BFS,属于一种盲目搜寻法,目的是系统地展开并检查图中的所有节点,以找寻结果。换句话说,它并不考虑结果的可能位置,彻底地搜索整张图,直到找到结果为止。

BFS,其英文全称是Breadth First Search。 BFS并不使用经验法则算法。从算法的观点,所有因为展开节点而得到的子节点都会被加进一个先进先出的队列中。一般的实验里,其邻居节点尚未被检验过的节点会被放置在一个被称为 open 的容器中(例如队列或是链表),而被检验过的节点则被放置在被称为 closed 的容器中。(open-closed表)

详细解释

已知图G=(V,E)和一个源顶点s,宽度优先搜索以一种系统的方式探寻G的边,从而“发现”s所能到达的所有顶点,并计算s到所有这些顶点的距离(最少边数),该算法同时能生成一棵根为s且包括所有可达顶点的宽度优先树。对从s可达的任意顶点v,宽度优先树中从s到v的路径对应于图G中从s到v的最短路径,即包含最小边数的路径。该算法对有向图无向图同样适用。

之所以称之为宽度优先算法,是因为算法自始至终一直通过已找到和未找到顶点之间的边界向外扩展,就是说,算法首先搜索和s距离为k的所有顶点,然后再去搜索和S距离为k+l的其他顶点。

为了保持搜索的轨迹,宽度优先搜索为每个顶点着色:白色、灰色或黑色。算法开始前所有顶点都是白色,随着搜索的进行,各顶点会逐渐变成灰色,然后成为黑色。在搜索中第一次碰到一顶点时,我们说该顶点被发现,此时该顶点变为非白色顶点。因此,灰色和黑色顶点都已被发现,但是,宽度优先搜索算法对它们加以区分以保证搜索以宽度优先的方式执行。若(u,v)∈E且顶点u为黑色,那么顶点v要么是灰色,要么是黑色,就是说,所有和黑色顶点邻接的顶点都已被发现。灰色顶点可以与一些白色顶点相邻接,它们代表着已找到和未找到顶点之间的边界。

在宽度优先搜索过程中建立了一棵宽度优先树,起始时只包含根节点,即源顶点s.在扫描已发现顶点u的邻接表的过程中每发现一个白色顶点v,该顶点v及边(u,v)就被添加到树中。在宽度优先树中,我们称结点u 是结点v的先辈或父母结点。因为一个结点至多只能被发现一次,因此它最多只能有–个父母结点。相对根结点来说祖先和后裔关系的定义和通常一样:如果u处于树中从根s到结点v的路径中,那么u称为v的祖先,v是u的后裔。

实际应用

BFS在求解最短路径或者最短步数上有很多的应用,应用最多的是在走迷宫上。

代码
function bfs() {
      let quene = [[startX, startY]];
      step[startX][startY] = 0;
      let res = [];
      flag[startX][startY] = true;
      while (quene.length) {
        let p = quene.shift();
        res.push(p);
        let x = p[0],
          y = p[1];
        if (x == targetX && y == targetY) break;
        let f = false;
        for (let i = 0; i < 4; i++) {
          let nx = x + dx[i],
            ny = y + dy[i];
          if (nx < 0 || nx >= map.length || ny >= map[0].length || ny < 0) {
            continue;
          }
          if (map[nx][ny] == 1 || flag[nx][ny] == true) {
            continue;
          }
          flag[nx][ny] = true;
          step[nx][ny] = step[x][y] + 1;
          quene.push([nx, ny]);
          if (nx == targetX && ny == targetY) {
            f = true;
            canFind = true;
            break;
          }
        }
        if (f) break;
      }
    }

如上代码,bfs使用的是队列数据结构来进行解决,每走一步便将这一步周边的有效坐标入队,优先判断了最近的路径,因此bfs得出的答案总是最短的路径,遍历的过程中还需要防止走回头路,因此需要标记已经遍历过的坐标,这里我使用了flag来进行标记,step记录的则是走到每一个位置的最短路径。

对比

dfs

深度优先搜索用栈(stack)来实现,整个过程可以想象成一个倒立的树形:

1、把根节点压入栈中。

2、每次从栈中弹出一个元素,搜索所有在它下一级的元素,把这些元素压入栈中。并把这个元素记为它下一级元素的前驱。

3、找到所要找的元素时结束程序。

4、如果遍历整个树还没有找到,结束程序。

bfs

广度优先搜索使用队列(queue)来实现,整个过程也可以看做一个倒立的树形:

1、把根节点放到队列的末尾。

2、每次从队列的头部取出一个元素,查看这个元素所有的下一级元素,把它们放到队列的末尾。并把这个元素记为它下一级元素的前驱。

3、找到所要找的元素时结束程序。

4、如果遍历整个树还没有找到,结束程序。

html完整代码

<html>
  <head>
    <title>寻路算法</title>
  </head>
  <body>
    <div class="body">
      <div class="body-content1">
        <div class="dfs-title">寻路算法</div>
        <div id="dfs-content" class="dfs-cell"></div>
        <div id="btn-list">
          <div id="btn-start-dfs" class="btn-start">dfs</div>
          <div id="btn-start-bfs" class="btn-start">bfs</div>
          <div id="btn-reset">重置</div>
        </div>
      </div>
      <div class="body-content2">
        <div class="dfs-title">.</div>
        <div class="start-point point">
          开始坐标:
          <input id="start-point-x" type="number" placeholder="" />
          <input id="start-point-y" type="number" placeholder="" />
        </div>
        <div class="target-point point">
          终点坐标:
          <input id="target-point-x" type="number" placeholder="" />
          <input id="target-point-y" type="number" placeholder="" />
        </div>
      </div>
    </div>
  </body>
  <script>
    let count = 0; //步数计数
    //迷宫地图
    let map = [
      [0, 0, 1, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 1, 0, 0, 0],
      [1, 0, 0, 0, 0, 1, 0, 0],
      [0, 0, 0, 0, 1, 0, 1, 0],
      [1, 0, 0, 0, 1, 0, 0, 0],
      [0, 0, 1, 0, 1, 0, 0, 0],
      [0, 1, 0, 0, 1, 1, 0, 1],
      [0, 0, 1, 1, 1, 0, 0, 0],
      [0, 0, 1, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 1, 0, 0, 0],
      [1, 0, 0, 0, 0, 1, 0, 0],
      [0, 0, 0, 0, 1, 0, 1, 0],
      [1, 0, 0, 0, 1, 0, 0, 0],
      [0, 0, 1, 0, 1, 0, 0, 0],
      [0, 1, 0, 0, 1, 1, 0, 1],
      [0, 0, 1, 1, 1, 0, 0, 0],
    ];
    let cellSize = 20; //px单元格大小
    let startX = 0, //开始坐标
      startY = 0;
    let targetX = 15, //结束坐标
      targetY = 7;
    let canFind = false;

    //遍历方向
    let dx = [0, 1, 0, -1],
      dy = [1, 0, -1, 0];

    let flag = new Array(map.length); //dfs标记走过的路径
    for (let i = 0; i < flag.length; i++) {
      flag[i] = new Array(map[i].length).fill(false);
    }
    //能否到达
    let findFlag = false;

    let step = new Array(map.length); //bfs标记走过的路径
    for (let i = 0; i < step.length; i++) {
      step[i] = new Array(map[i].length).fill(Infinity);
    }
    //单元格点击事件
    function cellClick(e) {
      let wFlag = 0;
      let classList = [...e.classList];
      if (classList.includes("now-in")) return;
      if (classList.includes("wall")) {
        e.classList.remove("wall");
        e.classList.add("space");
      } else if (classList.includes("space")) {
        e.classList.remove("space");
        e.classList.add("wall");
        wFlag = 1;
      }
      let id = e.id.split("-");
      let x = id[2],
        y = id[3];
      map[x][y] = wFlag;
      // console.log(map[x][y], x, y);
    }
    //初始化页面
    function init() {
      initPage();
      initData();
    }
    function initData() {
      const startPointX = document.getElementById("start-point-x");
      const startPointY = document.getElementById("start-point-y");
      const targetPointX = document.getElementById("target-point-x");
      const targetPointY = document.getElementById("target-point-y");
      startPointX.value = startX;
      startPointY.value = startY;
      targetPointX.value = targetX;
      targetPointY.value = targetY;

      startPointX.addEventListener("change", (e) => {
        if (
          e.target.value < 0 ||
          e.target.value >= map.length ||
          map[e.target.value][startY] == 1
        ) {
          alert("非法坐标,请重新输入");
          startPointX.value = startX;
          return;
        }
        startX = parseInt(e.target.value);
        initPage();
      });
      startPointY.addEventListener("change", (e) => {
        if (
          e.target.value < 0 ||
          e.target.value >= map[0].length ||
          map[startX][e.target.value] == 1
        ) {
          alert("非法坐标,请重新输入");
          startPointY.value = startY;
          return;
        }
        startY = parseInt(e.target.value);
        initPage();
      });
      targetPointX.addEventListener("change", (e) => {
        if (
          e.target.value < 0 ||
          e.target.value >= map.length ||
          map[e.target.value][targetY] == 1
        ) {
          alert("非法坐标,请重新输入");
          targetPointX.value = targetX;
          return;
        }
        targetX = parseInt(e.target.value);
        initPage();
      });
      targetPointY.addEventListener("change", (e) => {
        if (
          e.target.value < 0 ||
          e.target.value >= map[0].length ||
          map[targetX][e.target.value] == 1
        ) {
          alert("非法坐标,请重新输入");
          targetPointY.value = targetY;
          return;
        }
        targetY = parseInt(e.target.value);
        initPage();
      });
    }
    function initPage() {
      let innerHtml = ``;
      count = 0;
      findFlag = false;
      for (let i = 0; i < map.length; i++) {
        for (let j = 0; j < map[i].length; j++) {
          flag[i][j] = false;
          innerHtml += `<div id="${"dfs-id-" + i + "-" + j}" class="${
            map[i][j] == 0 ? "space" : "wall"
          } cell" style="width:${cellSize}px;height:${cellSize}px;" click="cellClick"></div>`;
        }
      }
      let dfsContent = document.getElementById("dfs-content");
      dfsContent.style.width = map[0].length * (cellSize + 2) + "px";
      dfsContent.innerHTML = innerHtml;

      let startCell = document.getElementById(
        "dfs-id-" + startX + "-" + startY
      );
      startCell.classList.add("now-in");

      let targetCell = document.getElementById(
        "dfs-id-" + targetX + "-" + targetY
      );
      targetCell.classList.add("target-in");
      let cell = document.getElementsByClassName("cell");
      for (let i = 0; i < cell.length; i++) {
        cell[i].addEventListener("click", () => {
          cellClick(cell[i]);
        });
      }
    }
    function dfs(x, y) {
      const dx = [1, 0, -1, 0],
        dy = [0, 1, 0, -1];
      if (x < 0 || y < 0 || x >= map.length || y >= map[0].length) return;
      if (map[x][y] == 1 || flag[x][y] || findFlag) return;
      let startCell = document.getElementById("dfs-id-" + x + "-" + y);
      startCell.classList.add("now-in");
      if (x == targetX && y == targetY) {
        findFlag = true;
        startCell.innerHTML = `<div style="font-size:small;text-align: center;">${count}</div>`;
        canFind = true;
        return;
      }
      for (let i = 0; i < 4 && !findFlag; i++) {
        flag[x][y] = true;
        startCell.innerHTML = `<div style="font-size:small;text-align: center;">${count}</div>`;
        count++;
        startCell.classList.add("pass");
        startCell.classList.remove("now-in");

        if (!findFlag) dfs(x + dx[i], y + dy[i]);
        if (!findFlag) flag[x][y] = false;
        if (!findFlag) startCell.innerHTML = "";
        if (!findFlag) count--;
        if (!findFlag) startCell.classList.remove("pass");
      }
    }

    function bfs() {
      let quene = [[startX, startY]];
      step[startX][startY] = 0;
      // console.log("开始bfs");
      let res = [];
      flag[startX][startY] = true;
      while (quene.length) {
        let p = quene.shift();
        res.push(p);
        let x = p[0],
          y = p[1];
        if (x == targetX && y == targetY) break;
        let f = false;
        for (let i = 0; i < 4; i++) {
          let nx = x + dx[i],
            ny = y + dy[i];
          if (nx < 0 || nx >= map.length || ny >= map[0].length || ny < 0) {
            continue;
          }
          if (map[nx][ny] == 1 || flag[nx][ny] == true) {
            continue;
          }
          flag[nx][ny] = true;
          step[nx][ny] = step[x][y] + 1;
          quene.push([nx, ny]);
          if (nx == targetX && ny == targetY) {
            f = true;
            canFind = true;
            break;
          }
        }
        if (f) break;
      }
      if (canFind) bfsDrawRoad();
    }
    //绘制路线
    function bfsDrawRoad() {
      let road = [[targetX, targetY]];
      while (road[0][0] != startX || road[0][1] != startY) {
        let x = road[0][0],
          y = road[0][1];
        for (let i = 0; i < 4; i++) {
          let nx = x + dx[i],
            ny = y + dy[i];
          if (nx < 0 || ny < 0 || nx >= step.length || ny >= step[0].length)
            continue;
          if (step[x][y] == step[nx][ny] + 1) {
            road.unshift([nx, ny]);
            break;
          }
        }
      }
      for (let i = 0; i < road.length; i++) {
        let startCell = document.getElementById(
          "dfs-id-" + road[i][0] + "-" + road[i][1]
        );
        if (i != road.length - 1) {
          startCell.classList.add("pass");
        } else {
          startCell.classList.add("now-in");
        }

        startCell.innerHTML = `<div style="font-size:small;text-align: center;">${i}</div>`;
      }
    }
    //页面初始化
    init();
    //开始dfs
    let startBtnDfs = document.getElementById("btn-start-dfs");
    startBtnDfs.addEventListener("click", () => {
      canFind = false;
      init();
      dfs(startX, startY);
      // console.log(canFind);
      if (!canFind) alert("无法达到");
    });
    //开始bfs
    let startBtnBfs = document.getElementById("btn-start-bfs");
    startBtnBfs.addEventListener("click", () => {
      canFind = false;
      init();
      bfs();
      // console.log(canFind);
      if (!canFind) alert("无法达到");
    });
    //重置页面
    let resetBtn = document.getElementById("btn-reset");
    resetBtn.addEventListener("click", () => {
      init();
    });
  </script>
  <style>
    .body {
      display: flex;
      margin: auto;
    }
    .body-content1 {
      flex: 1;
    }
    .body-content2 {
      flex: 1;
    }
    .point {
      margin-top: 1rem;
    }
    .dfs-cell {
      display: flex;
      flex-wrap: wrap;
      margin: auto;
    }
    .dfs-cell > .cell {
      border: 1px solid gray;
      cursor: pointer;
    }
    .dfs-cell > .wall {
      background-color: black;
    }
    .now-in {
      background-color: yellow;
    }
    .target-in {
      background-color: rgb(245, 162, 9);
    }
    .pass {
      background-color: yellowgreen;
    }
    .dfs-title {
      text-align: center;
      margin: 2rem;
    }
    #btn-list {
      display: flex;
      justify-content: space-between;
      width: 20rem;
      margin: auto;
    }
    .btn-start {
      padding: 1rem;
      margin: auto;
      margin-top: 2rem;
      text-align: center;
      background-color: violet;
      width: 2rem;
      cursor: pointer;
    }
    #btn-reset {
      padding: 1rem;
      margin: auto;
      margin-top: 2rem;
      text-align: center;
      background-color: violet;
      width: 2rem;
      cursor: pointer;
    }
  </style>
</html>

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐