一篇彻底搞懂深度优先搜索(DFS)及广度优先搜索(BFS)

概述:
深度优先搜索(Depth First Search)是遍历图形结构的一种方式,也可以说是一种图算法,其过程是对每一个可能的分支路径深入到不能再深入为止,并且每个节点只访问一次。与之对应的另一种遍历图形结构的方式----广度优先搜索(Breadth First Search)则是从某个节点开始向四周扩散,类似一层一层的遍历,它们的相同点是图的每个节点都要访问到并且只访问一次。下面通过几个案例实战来加深对这两种算法的理解。

案例一:
给定一个整形数组nums和一个整数target,你可以给数组nums里的数前面添加’+‘或’-'符号,使之构成一个表达式,求这样构成的表达式的计算结果与target相等的有多少种?
例如nums=[1,1],target=0,可以构成的表达式有+1+1=2、+1-1=0、-1+1=0、-1-1=-2,所以答案为2。
题意很简单,把所有情况列出来依次与target比较,结果就出来了,若nums长度为n,则有2^n种情况。

对于这类问题,我们一般首先会把它的解集空间给画出来,然后将这棵解集树遍历一遍,答案就有了,而选择何种方式遍历就对应了何种解法(DFS OR BFS),很显然,本题目的解集树是一颗二叉树,因为每个数有两种情况’+‘或’-'嘛。
用DFS遍历一颗树,就相当于是树的前序遍历,而BFS则对应树的层次遍历,如果对于树的遍历还不熟悉的话,建议先学习下这篇文章二叉树的遍历(递归与非递归)

下面先用DFS来解决这个问题。DFS的特点很符合栈结构的性质,很容易想到我们可以用递归来做,因为递归函数天然具备栈结构性质,代码如下,

/**
     * 
     * @param current 当前累加和
     * @param index 数组nums下标
     * @param nums
     * @param target
     */
    public void dfs(int current, int index, int[] nums, int target) {
        if (index == nums.length) {
            if (current == target) {
                ans++;
            }
            return;
        }
        dfs(current + nums[index], index + 1, nums, target);
        dfs(current - nums[index], index + 1, nums, target);
    }

就几行代码搞定,可以发现这就是相当于使用前序遍历遍历了一颗二叉树!

接下来再用BFS实现。因为解集空间为树形结构,我们使用树的层次遍历就好了,这里需要队列这种数据结构,还有一个需要注意到的点,就是树的节点需要保存哪些信息呢?节点类型如下,

private static class Node {
        int current;
        int index;

        public Node(int current, int index) {
            this.current = current;
            this.index = index;
        }
    }

使用层次遍历依次遍历完整棵树,结果就出来了。

public int bfs(int[] nums, int target) {
        int ans = 0;
        Queue<Node> queue = new LinkedList<>();
        Node node = new Node(0, 0);
        queue.offer(node);
        while (!queue.isEmpty()) {
            node = queue.poll();
            if (node.index == nums.length) {
                if (node.current == target) {
                    ans++;
                }
            } else {
                queue.offer(new Node(node.current + nums[node.index], node.index + 1));
                queue.offer(new Node(node.current - nums[node.index], node.index + 1));
            }
        }
        return ans;
    }

案例二:
求字符数组chars的全排列,如chars=[‘a’,‘b’,‘c’],那么输出abc、acb、bac、bca、cab、cba
它的解集空间是什么呢?首先第一层根节点是空的,第二层是chars的每个字符,…是一颗多叉树。又是树形结构,同理,DFS那我就用树的前序遍历模板,BFS就用层次遍历的模板。代码如下,

public void dfs(char[] chars, boolean[] mark, List<Character> ans) {
        if (ans.size() == chars.length) {
            System.out.println(ans);
        } else {
            for (int i = 0; i < chars.length; i++) {
                if (!mark[i]) {
                    mark[i] = true;
                    ans.add(chars[i]);
                    dfs(chars, mark, ans);
                    ans.remove(ans.size() - 1);
                    mark[i] = false;
                }
            }
        }
    }

注意,这个题目与上一个题目有所不同,这里涉及到了回溯操作,而且还用到标记数组避免重复访问。

BFS实现的代码如下,

public void bfs(char[] chars) {
        List<Integer> ans = new ArrayList<>();
        Queue<List<Integer>> queue = new LinkedList<>();
        queue.add(ans);
        while (!queue.isEmpty()) {
            ans = queue.poll();
            if (ans.size() == chars.length) {
                for (int i : ans) {
                    System.out.print(chars[i] + " ");
                }
                System.out.println();
            } else {
                for (int i = 0; i < chars.length; i++) {
                    if (!ans.contains(i)) {
                        List<Integer> temp = new ArrayList<>(ans);
                        temp.add(i);
                        queue.offer(temp);
                    }
                }
            }
        }
    }

这个ans存的是数组下标,在输出时映射下就好,这样便不用引入标记数组了。

案例三:
油田问题,对于一个字符矩阵char[][] map = new char[][]{
{’&’, ‘#’, ‘&’, ‘&’, ‘#’},
{’#’, ‘#’, ‘&, ‘&’, ‘#’},
{’&’, ‘&’, ‘#’, ‘&’, ‘#’},
{’&’, ‘#’, ‘#’, ‘&’, ‘#’},
{’#’, ‘&’, ‘&’, ‘&’, ‘#’}};
若两个’#‘字符相连(横、竖或对角方向)则属于同一块油田,问有多少块油田?这个map中有2块油田
前面两个案例的解空间都是树形的,而这题是图形的了。
思路:遍历这个矩阵,从字符为’#'的位置开始,依次向八个方向深入,同时用标记数组标记已访问过的。
DFS实现如下,

首先定义需要的全局变量,

static int ans, n, m;
// 8个方向
static int[][] direction = {{0, -1}, {1, -1}, {1, 0}, {1, 1}, {0, 1}, {-1, 1}, {-1, 0}, {-1, -1}};
static boolean[][] mark = new boolean[n][m];
public static void dfs(int i, int j) {
        if (i < 0 || i >= n || j < 0 || j >= m || mark[i][j] || map[i][j] != '#') {
            return;
        }
        mark[i][j] = true;
        for (int[] xy : direction) {
            dfs(i + xy[0], j + xy[1]);
        }
    }

主函数调用,

for (int i = 0; i < n; i++) {
      for (int j = 0; j < m; j++) {
         if (!mark[i][j] && map[i][j] == '#') {
               ans++;
               dfs(i, j);
            }
      }
}
        System.out.println(ans);

BFS实现,
对于每个节点需要保持位置信息,故也许一个节点类封装,

private static class Node {
        int i;
        int j;

        public Node(int i, int j) {
            this.i= i;
            this.j= j;
        }
    }
public int bfs(char[][] map, boolean[][] mark) {
        int ans = 0;
        for (int i = 0; i < map.length; i++) {
            for (int j = 0; j < map[0].length; j++) {
                if (!mark[i][j] && map[i][j] == '#') {
                    ans++;
                    Queue<Node> queue = new LinkedList<>();
                    Node node = new Node(i, j);
                    queue.offer(node);
                    while (!queue.isEmpty()) {
                        node = queue.poll();
                        for (int k = -1; k < 2; k++) {
                            for (int l = -1; l < 2; l++) {
                                if (k!=0 || l!=0) {
                                    int x = node.i + k, y = node.j + l;
                                    if (x>=0&&x< map.length&&y>=0&&y< map.length&&!mark[x][y]&&map[x][y]=='#') {
                                        mark[x][y] = true;
                                        queue.offer(new Node(x,y));
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        return ans;
    }

本章通过介绍几个比较简单的案例,希望能更深入的掌握DFS与BFS这两种算法。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

b17a

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值