【面试刷题】宽度优先搜索

【面试刷题】宽度优先搜索

一、最简洁的BFS算法的通用模板

1、适用于树和图的BFS模板

  • 队列建议使用new ArrayDeque不建议使用new LinkedList(链表比数组慢)
//双端队列
Queue<Node> queue = new ArrayDeque<>();
HashMap<Node,Integer> distance = new HashMap<>();

//step 1 初始化
//把初始节点放到deque里面,如果有多个就都放进去
//并标记初始节点的距离为0,记录在distance的hashmap里
//distance有两个作用,一个是判断是否已经访问过,二是记录离起点的距离
queue.offer(node);
distance.put(node,0);

//step 2:不断访问队列
//while循环 + 每次pop队列中的一个点出来
while (!queue.isEmpty()) {
    Node node = queue.poll();
    //step 3:扩展相邻节点
    //pop出的结点的相邻节点,加入队列并在distance中存储举例
    for (Node neighbor : node.getNeighbors()) {
        continue;
    }
    distance.put(neighbor, distance.get(node) + 1);
    queue.offer(neighbor);
}

  • 复杂度:图上BFS时间复杂度 = O(N + M)

2、LeetCode-133.克隆图

class Solution {
    public Node cloneGraph(Node node) {
        //特殊情况处理
        if (node == null) {
            return null;
        }

        //第一步:找到所有点
        List<Node> nodes = findNodesByBFS(node);

        //第二步:复制所有点
        Map<Node, Node> mapping = copyNodes(nodes);
        
        //第三部:复制所有边
        copyEdges(nodes, mapping);

        //返回与给定节点具有相同val的那个节点
        return mapping.get(node);
    }

    //第一步:BFS找到所有点
    private List<Node> findNodesByBFS(Node node) {
        Queue<Node> queue = new LinkedList<>();
        //用于保存所有的点,不重复,不漏掉
        Set<Node> visited = new HashSet<>();
        queue.offer(node);
        //一旦入队,就要马上标记
        visited.add(node);

        while(!queue.isEmpty()) {
            Node curNode = queue.poll();
            for (Node neighbor : curNode.neighbors) {
                //如果之前已经找到了这个点,无需再次BFS,否则死循环
                if (visited.contains(neighbor)) {
                    continue;
                }
                //一旦访问,就要马上标记。
                visited.add(neighbor);
                queue.offer(neighbor);;
            }
        }
        return new LinkedList<>(visited);
    }

    //第二步:复制所有点
    private Map<Node,Node> copyNodes(List<Node> nodes) {
        //(旧点 -> 新点)的映射
        Map<Node,Node> mapping = new HashMap<>();
        for (Node node : nodes) {
            mapping.put(node, new Node(node.val));
        }

        return mapping;
    }

    //第三步:复制所有边
    private void copyEdges(List<Node> nodes, Map<Node,Node> mapping) {
        for (Node node : nodes) {
            Node newNode = mapping.get(node);
            //旧点有哪些邻居,新点就有哪些邻居
            for (Node neighbor : node.neighbors) {
                Node newNeighbor = mapping.get(neighbor);
                newNode.neighbors.add(newNeighbor);
            }
        }
    }
}
  • 可以看出上面的代码,最主要的就是通过BFS找到所有的点。

3、LeetCode-127.单词接龙

①生成变换列表

【面试刷题】宽度优先搜索

【面试刷题】宽度优先搜索

②变换列表图示

【面试刷题】宽度优先搜索

③代码实现

class Solution {
    public int ladderLength(String beginWord, String endWord, List<String> wordList) {
        //假设wordList不为空
        //假设beginWord和endWord是非空的,且二者不同

        //必须加入endWord。可以加入beginword
        wordList.add(endWord);

        HashSet<String> visited = new HashSet<String>();
        Queue<String> queue = new LinkedList<String>();
        queue.offer(beginWord);
        visited.add(beginWord);

        //记录最短路线长度,起始长度为1
        int length = 1;
        while(!queue.isEmpty()) {
            //到下一层(不是当前层)的长度,相当于树的深度
            length++;
            //当前层有size个元素
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                String word = queue.poll();
                //得到下一步的单词
                for (String nextWord : getNextWords(word,wordList)) {
                    if (visited.contains(nextWord)) {
                        continue;
                    }

                    //如果下一层的词为尾词,直接返回当前到下一层(不是当前层)的长度
                    if (nextWord.equals(endWord)) {
                        return length;
                    }

                    //加入下一层,为后面BFS做准备
                    visited.add(nextWord);
                    queue.offer(nextWord);
                }
            }
        }
        //不能实现首尾接龙
        return 0;
    }

    //找到可以和word接龙的所有单词
    //比如word = 'hot',dict = {'hot', 'hit', 'hog'},return ['hit','hog']
    private ArrayList<String> getNextWords(String word, List<String> wordList) {
        ArrayList<String> nextWords = new ArrayList<String>();
        //枚举字典中的每个单词
        for (String nextWord : wordList) {
            boolean hashOneDiff = false;
            for (int i = 0; i < word.length(); i++) {
                //判断两个词是否只相差一个字母。如果是,则可以接龙
                if (nextWord.charAt(i) != word.charAt(i)) {
                    //如果已经有一个不同了,那么直接就排除
                    if (hashOneDiff) {
                        hashOneDiff = false;
                        break;
                    }
                    hashOneDiff = true;
                }
            }
            if (hashOneDiff) {
                nextWords.add(nextWord);
            }
        }
        return nextWords;
    }

    //新字符串
    private String replace(String word, int index, char c) {
        char[] chars = word.toCharArray();
        chars[index] = c;
        return new String(chars);
    }
}

二、矩阵中的宽度优先搜索

1、LeetCode-200.岛屿数量

【面试刷题】宽度优先搜索

思路解析

【面试刷题】宽度优先搜索

代码实现

//定义一个class,表示坐标系中的一个点
class Coordinate {
    int x, y;
    public Coordinate(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

class Solution {
    //四个方向的偏移量
    int[] deltaX = {0, 1, -1, 0};
    int[] deltaY = {1, 0, 0, -1};
    public int numIslands(char[][] grid) {
        //特殊情况处理
        if (grid == null || grid.length == 0 || grid[0] == null || grid[0].length == 0) {
            return 0;
        }

        int islands = 0;
        int row = grid.length, col = grid[0].length;
        //记录某点是否被BFS过,如果之前已经被BFS过,不应该再次被BFS
        boolean[][] visited = new boolean[row][col];

        //遍历矩阵中的每一个点
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                //如果为海洋,无需BFS
                //如果该点已经被visited,无需做冗余遍历,重复读取
                if (grid[i][j]!='0' && !visited[i][j]) {
                    bfs(grid, i, j, visited);
                    islands++;
                }
            }
        }
        return islands;
    }

    //从一块土地出发,通过BFS,遍历整个岛屿
    private void bfs(char[][] grid, int x, int y, boolean[][] visited) {
        Queue<Coordinate> queue = new ArrayDeque<>();
        queue.offer(new Coordinate(x, y));
        visited[x][y] = true;

        while (!queue.isEmpty()) {
            Coordinate coor = queue.poll();
            //遍历上下左右四个方向
            for (int direction = 0; direction < 4; direction++) {
                int newX = coor.x + deltaX[direction];
                int newY = coor.y + deltaY[direction];
                //如果不是有效的点,就继续
                if (!isValid(grid, newX, newY, visited)) {
                    continue;
                }
                queue.offer(new Coordinate(newX, newY));
                //一旦入队,标记此店已经参加过BFS
                visited[newX][newY] = true;
            }
        }
    }

    //判断一个点是否可以被BFS
    private boolean isValid(char[][] grid, int x, int y, boolean[][] visited) {
        int n = grid.length, m = grid[0].length;
        //如果出界,返回false
        if (x < 0 || x>=n || y < 0 || y >= m) {
            return false;
        }

        //如果已经BFS过,不要再次BFS,避免:1.死循环 2.冗余的BFS变量
        if (visited[x][y]) {
            return false;
        }

        //如果是'1' ,则为true;如果是0,则为false
        return grid[x][y] == '1' ? true : false;
    }
}

反思记录

  • 处理矩阵的问题,上下左右坐标移动的模拟,通过偏移量。
  • BFS的使用,去遍历周围的岛屿。
  • 细节的处理,坐标系中的点的表示,visited数组传递的方法(避免使用全局变量),边界值的检测判断。
  • 在实现BFS时候的队列,使用了ArrayDeque这个类。

2、LintCode-611.骑士的最短路线

/**
* Definition for a point.
* class Point {
*     int x;
*     int y;
*     Point() { x = 0; y = 0; }
*     Point(int a, int b) { x = a; y = b; }
* }
*/
public class Solution {
   //8个方向的偏移量,这是骑士可以移动的
   public static final int[] X_OFFSET = {1, 1, -1, -1, 2, 2, -2, -2};
   public static final int[] Y_OFFSET = {2, -2, 2, -2, 1, -1, 1, -1};
   public int shortestPath(boolean[][] grid, Point source, Point destination) {
       //异常处理
       if (grid == null || grid.length == 0 || grid[0] == null || grid[0].length == 0) {
           return -1;
       }

       Queue<Point> queue = new ArrayDeque<>();
       //记录从起点到(x,y)的最短距离,(x, y) 代表 到起点start的最短距离
       Map<Integer,Integer> cellToDisMap = new HashMap();

       int col = grid[0].length;
       queue.offer(source);
       //加上乘以col避免重复,把二维坐标换成一维坐标
       cellToDisMap.put(source.x * col + source.y, 0);

       while (!queue.isEmpty()) {
           Point point = queue.poll();
           int curPointKey = point.x * col + point.y;
           if (point.x == destination.x && point.y == destination.y) {
               return cellToDisMap.get(curPointKey);
           }

           //遍历8个不同的方向
           for (int i = 0; i < 8; i++) {
               int adjX = point.x + X_OFFSET[i];
               int adjY = point.y + Y_OFFSET[i];
               if (!isValid(adjX, adjY, grid)) {
                   continue;
               }

               //小套路:把二维坐标转换为一维坐标
               int newPointKey = adjX * col + adjY;
               //如果之前已经到达过此点,不可能再次BFS此点找到最短路径,还会造成死循环
               if (cellToDisMap.containsKey(newPointKey)) {
                   continue;
               }

               queue.offer(new Point(adjX, adjY));
               cellToDisMap.put(newPointKey, cellToDisMap.get(curPointKey) + 1);
           }
       }

       return -1;
   }

   private boolean isValid(int x, int y, boolean[][] grid) {
       if (x < 0 || x >= grid.length || y < 0 || y >= grid[0].length) {
           return false;
       }

       //值1表示障碍物,返回false
       return !grid[x][y];
   }
}

三、拓扑排序Topological Sorting

1、什么是拓扑排序

在一个有向图中,对所有的节点进行排序,要求没有一个节点指向它前面的节点。

先统计所有节点的入度,对于入度为0的节点就可以分离,然后把这个节点指向的节点的入度减一。

一直重复上述操作,直到所有的节点都被分离出来。

如果最后不存在入度为0的节点,那就说明有环,不存在拓扑排序,也就是出现无解的情况。

【面试刷题】宽度优先搜索

细节描述:

  • 统计每个点的入度
  • 将每个入度为0的点,放入队列中,作为起始节点。
  • 不断从队列中拿出一个点,去掉这个点的所有连边(指向其他点的边),其他点的相应入度-1
  • 一旦发现新的入度为0的点,丢回队列当中。

2、LeetCode-207.课程表

[题目链接](207. 课程表 - 力扣(LeetCode) (leetcode-cn.com))

【面试刷题】宽度优先搜索

解题思路:

图 + 有依赖关系 + 有向 + (环) = 拓扑排序

  • 统计每个点的入度
  • 将每个入度为0的点放入队列中作为起始节点
  • 不断从队列中拿出一个点,去掉这个点的所有连边(指向其他点的边),其他点的相应入度-1
  • 一旦发现新的入度为0的点,丢回队列当中。

【面试刷题】宽度优先搜索

【面试刷题】宽度优先搜索

代码实现:

class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        //构建图,代表(先修课 -> 多个后修课)的映射
        List[] graph = new ArrayList[numCourses];
        //图的初始化,每个先修课->空后修课List
        for (int i = 0; i < numCourses; i++) {
            graph[i] = new ArrayList<Integer>();
        }

        //1.统计每个点的入度,并构建图
        int[] inDegree = new int[numCourses];
        for (int[] edge : prerequisites) {
            //比如[0,1]: 要上课程0,先学课程1,则0的入度加1。
            //即有向图的箭头表示的是依赖关系,先修课程1,才能修课程0
            //注意这里是,先修课->后修课程的映射,即入度还是后修的课程
            graph[edge[1]].add(edge[0]);
            inDegree[edge[0]]++;
        }

        Queue queue = new LinkedList();

        //2.将每个入度为0的点放入队列中作为起始节点
        for (int i = 0; i < inDegree.length; i++) {
            if (inDegree[i] == 0) {
                queue.add(i);
            }
        }

        //记录已修课程的数量
        int numChoose = 0;
        //记录拓扑顺序
        int[] topoOrder = new int[numCourses];

        //3.不断从队列中拿出一个点,去掉这个点的所有连边(指向其他点的边),其他点的相应的入度-1
        while (!queue.isEmpty()) {
            int nowPos = (int) queue.poll();
            //记录已修课程
            topoOrder[numChoose] = nowPos;
            numChoose++;
            //从这个课程指向的其他课程,遍历,入度减一
            for (int i = 0; i < graph[nowPos].size(); i++) {
                int nextPos = (int) graph[nowPos].get(i);
                //当前点的邻居入度减一,表示每个后修课程的一门先修课程已经完成
                inDegree[nextPos]--;
                //4.一旦发现新的入度为0的点,丢回队列当中
                //表示一门后修课程的所有先修课程已经完成,可以被修了
                if (inDegree[nextPos] == 0) {
                    queue.add(nextPos);
                }
            }
        }
        //如果需要拓扑排序,也可以在此返回topoOrder
        return (numChoose == numCourses) ? true : false;
    }
}

值得学习的点:

  • 在定义先修课程和后修课程时,通过一个List来表示,先修的课程放到最前面,后修的课程放到后面。
  • 在这个图的定义中,List[] graph,很巧妙,通过课程的下标和数组下标结合在了一起。

四、BFS小结

  • 能够用BFS的不要用DFS
  • BFS使用的场景
    • 连通块问题
    • 层级遍历问题
    • 拓扑排序问题
  • 是否需要层级遍历
    • 需要多一重循环,queue.size()
  • 矩阵坐标变换数组
上一篇:【POJ】2488 A Knight‘s Journey


下一篇:JZ12 矩阵中的路径