算法数据结构:广度优先搜索(BFS)

315 阅读3分钟

1、什么广度优先搜索(Breadth-First-Search)

广度优先搜索(Breadth-First-Search),简称 BFS。直观地讲,它其实就是一种地毯式水波纹层层推进的搜索策略,即先查找离起始顶点最近的,然后是次近的,依次往外搜索。

2、代码模板

2.1、python 模板

# 从 start 到 end
def bfs(graph, start, end): 
	# 已访问顶点
	visited = set()
    # LIFO
    queue = []
    queue.append[start])
    # record visited node
    visited.add(start)
    
    while queue:
    	node = queue.pop()
        visited.add(node)
        
        process(node)
        nodes = generate_related_nodes(node)
        queue.push(nodes)

	# other processing work
    ...

2.2、Java 模板

// 计算从起点 start 到终点 target 的最近距离
int bfs(Node start, Node target) {
    Queue<TreeNode> queue; 
    Set<Node> visited; // 记录已访问的顶点,避免走回头路,陷入死循环0
    // 起点 start 入队
    queue.offer(start);
    // start 入队,则代表 start 被访问过了,需要记录
    visited.add(start);
    // 记录扩散的层数 
    int step = 0;
    while (queue is not empty) {
        int size = queue.size();
        // 将当前队列中所有的节点分别出队,向外扩散一层
        while (size-- > 0) {
            Node curr = queue.poll();
            // 判断当前节点是否到达终点 target
            if (curr is target) {
                return step;
            }
            // 将当前节点的子节点入队
            for (Node next : curr.children) {
            	if (next not in visited) {
                	queue.offer(next);
                    visited.add(x);
                }
            }
        }
        // 更新步数
        step++;
    }
    return step;
}

代码中的变量解释:

  • queue用于存储距起始节点starget n 层的所有节点,是BFS的核心数据结构;
  • curr.children获取curr所有的直接孩子节点;
  • visited主要记录被访问过的节点,防止走回头路,陷入死循环,做剪枝用;大部分时候都是必须的,如果像n叉树的数据结构,只有next指针的,则不需要visited

3、实战

3.1、二叉树最小深度

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public int minDepth(TreeNode root) {
        if (root == null) {
            return 0;
        }
        Queue<TreeNode> queue = new LinkedList<>();
        // 根节点 root 入队
        queue.offer(root);
        // depth 初始化为 1,因为 root 为第一层 
        int depth = 1;
        while (!queue.isEmpty()) {
            int size = queue.size();
            // 将当前队列中所有的节点分别出队,向外扩散
            while (size-- > 0) {
                TreeNode curr = queue.poll();
                // 判断当前节点是否到达叶节点
                if (curr.left == null && curr.right == null) {
                    return depth;
                }
                // 将当前节点的子节点入队
                if (curr.left != null) {
                    queue.offer(curr.left);
                }
                if (curr.right != null) {
                    queue.offer(curr.right);
                }
            }
            depth++;
        }
        return depth;
    }
}

3.2、打开转盘锁

解法一、BFS
class Solution {

    public int openLock(String[] deadends, String target) {
        // 需要跳过的死亡密码
        Set<String> deads = Stream.of(deadends).collect(Collectors.toSet());
        // 记录被访问的节点
        Set<String> visited = new HashSet<>();

        Queue<String> queue = new LinkedList<>();

        int step = 0;
        queue.offer("0000");
        visited.add("0000");
        while (!queue.isEmpty()) {
            int size = queue.size();
            // 将当前队列中所有的节点分别出队,向外扩散一层
            while (size-- > 0) {
                String curr = queue.poll();

                if (deads.contains(curr)) {
                    // 遇到死亡密码跳过
                    continue;
                }
                // 判断当前节点是否到达终点 target
                if (curr.equals(target)) {
                    return step;
                }
                // 每次波动转盘锁的四个位置的概率一样
                // 每位密码有可能会左旋或右旋
                for (int i = 0; i < 4; i++) {
                    // 右旋
                    String plus = plusOne(curr, i);
                    if (!visited.contains(plus)) {
                        queue.offer(plus);
                        visited.add(plus);
                    }
                    // 左旋
                    String minus = minusOne(curr, i);
                    if (!visited.contains(minus)) {
                        queue.offer(minus);
                        visited.add(minus);
                    }
                }
            }
             // 更新步数
            step++;
        }
        // 如果无论如何不能解锁,返回 -1。
        return -1;
    }

    public String plusOne(String pwd, int index) {
        char[] ch = pwd.toCharArray();
        if (ch[index] == '9') {
            ch[index] = '0';
        } else {
            ch[index]++;
        }
        return new String(ch);
    }

    public String minusOne(String pwd, int index) {
        char[] ch = pwd.toCharArray();
        if (ch[index] == '0') {
            ch[index] = '9';
        } else {
            ch[index]--;
        }
        return new String(ch);
    }
}
解法二、双向 BFS
class Solution {

    public int openLock(String[] deadends, String target) {
        // 需要跳过的死亡密码
        Set<String> deads = Stream.of(deadends).collect(Collectors.toSet());
        // 记录被访问的节点
        Set<String> visited = new HashSet<>();

        Set<String> start = new HashSet<>();
        Set<String> end = new HashSet<>();

        int step = 0;
        start.add("0000");
        end.add(target);

        while (!start.isEmpty() && !end.isEmpty()) {
            // 哈希集合在遍历的过程中不能修改,用 temp 存储扩散结果
            Set<String> temp = new HashSet<>();
            
            // 将当前队列中所有的节点分别出队,向外扩散一层
            for (String curr : start) {
                if (deads.contains(curr)) {
                    // 遇到死亡密码跳过
                    continue;
                }
                // 判断当前节点是否到达终点 target
                if (end.contains(curr)) {
                    return step;
                }
                visited.add(curr);
                // 每次波动转盘锁的四个位置的概率一样
                // 每位密码有可能会左旋或右旋
                for (int i = 0; i < 4; i++) {
                    // 右旋
                    String plus = plusOne(curr, i);
                    if (!visited.contains(plus)) {
                        temp.add(plus);
                    }
                    // 左旋
                    String minus = minusOne(curr, i);
                    if (!visited.contains(minus)) {
                        temp.add(minus);
                    }
                }
            }
             // 更新步数
            step++;
            // temp 相当于 q1start
            // 这里交换 start end,下一轮 while 就是扩散 end
            // 重复此步骤,其实就是,分别从 start 和 end 扩散
            start = end;
            end = temp;
        }
        // 如果无论如何不能解锁,返回 -1。
        return -1;
    }

    public String plusOne(String pwd, int index) {
        char[] ch = pwd.toCharArray();
        if (ch[index] == '9') {
            ch[index] = '0';
        } else {
            ch[index]++;
        }
        return new String(ch);
    }

    public String minusOne(String pwd, int index) {
        char[] ch = pwd.toCharArray();
        if (ch[index] == '0') {
            ch[index] = '9';
        } else {
            ch[index]--;
        }
        return new String(ch);
    }
}

4、小结

BFS 需要借助队列来实现,遍历得到的路径就是起始顶点到终止顶点的最短路径。