【算法】BFS总结

249 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第16天,点击查看活动详情

一、前言

BFSBreath First Search,广度优先搜索)和DFSDepth First Search,深度优先搜索)是特别常用的两种算法。

BFS 相对 DFS 的最主要区别是: BFS 找到的路径一定是最短的,但代价是空间复杂度比 DFS 高很多。

因为 BFS 的逻辑,depth 每增加一次,队列中的所有节点都向前迈一步,这个逻辑保证了一旦找到一个终点,走的步数是最少的。

BFS 算法基本上会使用队列,典型的是树的层序遍历:

void traverse(TreeNode root) {
    if (null == root) return;
    
    // 初始化队列,将 root 加入队列
    Queue<TreeNode> q = new LinkedList<>();
    q.offer(root);
    
    while (!q.isEmpty()) {
        TreeNode cur = q.poll();
        
        // 层级遍历代码
        System.out.println(root.val);
        
        if (cur.left != null) q.offer(cur.left);
        if (cur.right != null) q.offer(cur.right);
    }
}

队列的一些操作,必知:

// 创建队列
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(e); // 入队
queue.add(e);   // 入队
queue.poll();   // 出队// 优先队列:最小堆(默认)
Queue<Integer> q = new PriorityQueue<>((a, b) -> a - b);
// 优先队列:最大堆
Queue<Integer> q = new PriorityQueue<>(Collections.reverseOrder());
// 自定义排序:最小堆
Queue<ListNode> q = new PriorityQueue<>((a, b) -> a.val - b.val);

双向 BFS

双向 BFS 与 传统 BFS 区别:

  • 传统的BFS 是从起点开始向四周扩散,遇到终点时停止。
  • 双向 BFS 是从起点和终点同时开始扩散,当两边有交集的时候停止。

BFS-2022-08-0821-07-58.png

双向 BFS 局限性在于: 必须知道终点在哪。

Tips 无论传统 BFS 还是双向 BFS,空间复杂度都是一样的。双向 BFS 只是一种技巧。

题目:127. 单词接龙



二、题目

(1)二叉树的右视图(中)

LeetCode 199

题干分析

这个题目说的是,给你一棵二叉树,并且你站在这棵树的右边,你要返回从上到下看到的节点值。

# 比如说,给你的二叉树是:
​
     1
   /   \
  2     4
 / \
6   8# 站在这棵二叉树的右边看过来,从上到下看到的数字依次是:
[1, 4, 8]

思路解法

思路有二: BFSDFS

方法一:BFS

  • 层序遍历顺序:从左到右,一个个入队。
  • 输出每一层中队列的最后一个元素即可。

BFS-2022-08-0821-07-59.png

// 方法一: BFS
// Time: O(n), Space: O(n), Faster: 82.03%
public List<Integer> rightSideViewBFS(TreeNode root) {
    if (null == root) return Collections.emptyList();
    Queue<TreeNode> queue = new LinkedList<>();
    List<Integer> result = new ArrayList<>();
    queue.add(root);
    while (!queue.isEmpty()) {
        int size = queue.size();
        TreeNode node = null;
        while (size-- > 0) {
            node = queue.poll();
            if (null != node.left) queue.add(node.left);
            if (null != node.right) queue.add(node.right);
        }
        result.add(node.val);
    }
    return result;
}

方法二:DFS

  • 注意递归方向: 先右子树,再左子树。

DFS-2022-08-0821-20-51.png

// 方法二:DFS
// Time: O(n), Space: O(n), Faster: 100.00%
public List<Integer> rightSideViewDFS(TreeNode root) {
    List<Integer> result = new ArrayList<>();
    dfs(root, result, 0);
    return result;
}
​
private void dfs(TreeNode root, List<Integer> result, int level) {
    if (root == null) return;
    if (level == result.size()) result.add(root.val);
    dfs(root.right, result, level + 1);
    dfs(root.left, result, level + 1);
}

(2)打开转盘锁(中)

LeetCode 752

题干分析

你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有10个数字: '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' 。每个拨轮可以自由旋转:例如把 '9' 变为 '0','0' 变为 '9' 。每次旋转都只能旋转一个拨轮的一位数字。

锁的初始数字为 '0000' ,一个代表四个拨轮的数字的字符串。

列表 deadends 包含了一组死亡数字,一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。

字符串 target 代表可以解锁的数字,你需要给出解锁需要的最小旋转次数,如果无论如何不能解锁,返回 -1 。

示例 1:
​
输入:deadends = ["0201","0101","0102","1212","2002"], target = "0202"
输出:6
解释:
可能的移动序列为 "0000" -> "1000" -> "1100" -> "1200" -> "1201" -> "1202" -> "0202"。
注意 "0000" -> "0001" -> "0002" -> "0102" -> "0202" 这样的序列是不能解锁的,
因为当拨动到 "0102" 时这个锁就会被锁定。
​
​
示例 2:
​
输入: deadends = ["8888"], target = "0009"
输出:1
解释:把最后一位反向旋转一次即可 "0000" -> "0009"。
​
​
示例 3:
​
输入: deadends = ["8887","8889","8878","8898","8788","8988","7888","9888"], target = "8888"
输出:-1
解释:无法旋转到目标数字且不被锁定。

思路解法

思路有二: BFS 和 双向 BFS

方法一:BFS

  • deads 哈希 set 记录死亡数字
  • visited 哈希 set 记录访问过的数字
  • 队列: BFS 遍历使用
// Time: O(b^d * d^2 + md), Space: O(b^d * d^2 + md), Faster: 15.30%
public int openLock(String[] deadends, String target) {
    // 记录需要跳过的死亡密码
    Set<String> deads = new HashSet<>();
    for (String s : deadends) deads.add(s);
    // 记录已经穷举过的密码,防止走回头路
    Set<String> visited = new HashSet<>();
    Queue<String> q = new LinkedList<>();
    // 从起点开始启动 BFS
    int step = 0;
    q.offer("0000");
    visited.add("0000");
​
    while (!q.isEmpty()) {
        int size = q.size();
        for (int i = 0; i < size; ++i) {
            String cur = q.poll();
​
            // 判断密码是否合法
            if (deads.contains(cur)) {
                continue;
            }
            if (target.equals(cur)) {
                return step;
            }
​
            for (int j = 0; j < 4; ++j) {
                String up = plusOne(cur, j); // 向下拨, 0 -> 1
                if (!visited.contains(up)) {
                    q.offer(up);
                    visited.add(up);
                }
                String down = minusOne(cur, j); // 向上拨, 0 -> 9
                if (!visited.contains(down)) {
                    q.offer(down);
                    visited.add(down);
                }
            }
        }
        ++step; // 增加步数
    }
    // 穷举完了,没有找到目标
    return -1;
}
// 向下拨, 0 -> 1
private String plusOne(String s, int j) {
    char[] ch = s.toCharArray();
    if (ch[j] == '9') {
        ch[j] = '0';
    } else {
        ch[j] += 1;
    }
    return new String(ch);
}
// 向上拨, 0 -> 9
private String minusOne(String s, int j) {
    char[] ch = s.toCharArray();
    if (ch[j] == '0') {
        ch[j] = '9';
    } else {
        ch[j] -= 1;
    }
    return new String(ch);
}

方法二:双向BFS

// Time: O(b^d * d^2 + md), Space: O(b^d * d^2 + md), Faster: 84.26%
public int openLockBFS2(String[] deadends, String target) {
    // 记录需要跳过的死亡密码
    Set<String> deads = new HashSet<>();
    for (String s : deadends) deads.add(s);
    // 记录已经穷举过的密码,防止走回头路
    Set<String> visited = new HashSet<>();
    Set<String> q1 = new HashSet<>();
    Set<String> q2 = new HashSet<>();
    // 从起点开始启动 BFS
    int step = 0;
    // 初始化起点和终点
    q1.add("0000");
    q2.add(target);
​
    while (!q1.isEmpty() && !q2.isEmpty()) {
        // 在遍历的过程不能修改哈希集合
        // 用 temp 存储 q1的扩散结果
        Set<String> 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); // 向下拨, 0 -> 1
                if (!visited.contains(up)) {
                    temp.add(up);
                }
                String down = minusOne(cur, j); // 向上拨, 0 -> 9
                if (!visited.contains(down)) {
                    temp.add(down);
                }
            }
        }
        ++step; // 增加步数
        // temp 相当于 q1
        // 这里交换 q1 q2,下一轮 while 就是扩散 q2
        q1 = q2;
        q2 = temp;
    }
    // 穷举完了,没有找到目标
    return -1;
}