【刷题系列】广度/宽度优先搜索(BFS)

系列汇总:《刷题系列汇总》



——————《剑指offeer》———————

1. 把二叉树打印成多行

  • 题目描述:从上到下按层打印二叉树,同一层结点从左至右输出。每一层输出一行。
    在这里插入图片描述
  • 优秀思路:利用队列“先进先出”的特点来保存每层节点,并动态的把本层出队,下层入队,
    import java.util.*;
    public class Solution {
        ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
            ArrayList<ArrayList<Integer>> res = new ArrayList<>(); //注意这种情况后面不能加泛型
            if(pRoot == null) return res;
            ArrayList<Integer> tempRes;
            Queue<TreeNode> q = new LinkedList<TreeNode>();
            q.add(pRoot);
            // 核心部分
            while(!q.isEmpty()){
                tempRes = new ArrayList<Integer>();
                int size = q.size(); // size即为本层节点的数量
                for(int i = 0;i<size;i++){ // 注意不能写成 q.size(),否则是动态值,则不能代表每一层的数量了
                    TreeNode cur = q.poll(); //将上一层的节点逐个出队
                    tempRes.add(cur.val);
                    if(cur.left != null) q.add(cur.left);
                    if(cur.right != null) q.add(cur.right);
                }
                res.add(tempRes);
            }
            return res;
        }
    }
    

——————《LeectCode》———————

1. 岛屿数量

  • 题目描述:给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。
    岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。此外,你可以假设该网格的四条边均被水包围。
  • 优秀思路:为了求出岛屿的数量,我们可以扫描整个二维网格。如果一个位置为 1,则将其加入队列,开始进行广度优先搜索。在广度优先搜索的过程中,每个搜索到的 1 都会被重新标记为 0。直到队列为空,搜索结束。
class Solution {
    public int numIslands(char[][] grid) {
        if(grid==null || grid.length == 0 || grid[0].length==0) return 0; 
        int nIsland = 0;
        int row = grid.length, col = grid[0].length;
        int cur = 0, curRow = 0, curCol = 0; // 当前位置

        for(int i = 0;i<row;i++){
            for(int j = 0;j<col;j++){
                if(grid[i][j]=='1'){
                    nIsland++;
                    grid[i][j] ='0';
                    Queue<Integer> q = new LinkedList<Integer>();
                    q.add(i*col+j); // i*col+j 表示元素的序号
                    while(!q.isEmpty()){
                        cur = q.poll(); // 将搜索起点出队
                        curRow = cur / col;
                        curCol = cur % col;
                        // 上
                        if(curRow - 1 >= 0 && grid[curRow-1][curCol] == '1'){
                            grid[curRow-1][curCol] = '0';
                            q.add((curRow-1)*col + curCol);
                        }
                        // 下
                        if(curRow + 1<= row - 1 && grid[curRow+1][curCol] == '1'){
                            grid[curRow+1][curCol] = '0';
                            q.add((curRow+1)*col + curCol);
                        }
                        // 左
                        if(curCol - 1 >= 0 && grid[curRow][curCol-1] == '1'){
                            grid[curRow][curCol-1] = '0';
                            q.add(curRow*col + curCol-1);
                        }
                        // 右
                        if(curCol + 1 <= col - 1 && grid[curRow][curCol+1] == '1'){
                            grid[curRow][curCol+1] = '0';
                            q.add(curRow*col + curCol+1);
                        }
                    }
                }
            }
        }
        return nIsland;
    }
}

2. 完全平方数

  • 题目描述:给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。给你一个整数 n ,返回和为 n 的完全平方数的 最少数量完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、916 都是完全平方数,而 311 不是。
  • 优秀思路(我编写的):贪心BFS
    • 建立一个队列q存储每一层的所有余数
    • 下一层逐个抛出上一层的余数remain,并计算其与所有小于它的完全平方数之差,并将其存入下一层。【注意:从最接近当前余数的最小平方数从大到小搜索,速度更快】
    • 直至新的余数 = 0,则停止搜索。
class Solution {
    public int numSquares(int n) {
        if(n<=0) return 0;
        Queue<Integer> q = new LinkedList<>(); // 存储每一层余数
        q.add(n);
        int numPS = 0; // 最小完全平方数数量
        // 贪心bfs
        while(!q.isEmpty()){
            int size = q.size(); // 本层余数的个数
            numPS++; // 每层 numPS 加 1
            for(int i = 0;i < size;i++){ // 遍历本层所有余数
                int remain = q.poll();
                for(int j = (int)Math.sqrt(remain);j >= 1;j--){ // 从最接近当前余数的最小平方数开始搜索
                    if(remain - j*j == 0) return numPS; // 若余数减小为0,则停止搜索
                    else q.add(remain - j*j);
                }
            }
        }
        return numPS;
    }
}

3. 删除无效的括号

  • 题目描述:给你一个由若干括号和字母组成的字符串 s ,删除最小数量的无效括号,使得输入的字符串有效。返回所有可能的结果。答案可以按 任意顺序 返回。
  • 优秀思路:逐个删除括号,判断删除后是否合法即可
class Solution {
    public List<String> removeInvalidParentheses(String s) {
        // 当前层的
        Set<String> level = new HashSet<>();
        level.add(s);
        while (true) {
            // 过滤不合法的:写法1——速度更快
            List<String> valid = new ArrayList<String>();
            for(String tempS:level) if(isValid(tempS)) valid.add(tempS);
            // 过滤不合法的:写法2
            // List<String> valid = level.stream().filter(this::isValid).collect(Collectors.toList());

            if (valid.size() > 0) return valid;

            // 下一层
            Set<String> nextLevel = new HashSet<>(); // 可防止重复
            for (String item : level) {
                // 每次移除一个括号
                for (int i = 0; i < item.length(); i++) {
                    if (item.charAt(i) == '(' || item.charAt(i) == ')') {
                        nextLevel.add(item.substring(0, i) + item.substring(i + 1));
                    }
                }
            }
            level = nextLevel;
            // 全部括号都被移除依然不符合,跳出循环
            if (level.size() == 0) return new ArrayList<>();
        }
    }

    // 计数法判断括号是否合法
    public boolean isValid(String s) {
        int cnt = 0;
        for (char c : s.toCharArray()) {
            if (c == '(') cnt++;
            else if (c == ')')  cnt--;
            if (cnt < 0) return false;
        }
        // 最后左右括号不相等,也不合法
        return cnt == 0;
    }
}

4.二叉树的右视图

  • 题目描述:给定一棵二叉树,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
    在这里插入图片描述

  • 优秀思路(我的):建立队列存储每一层的节点,一直更新最右边的点

    class Solution {
        public List<Integer> rightSideView(TreeNode root) {
            List<Integer> rightView = new ArrayList<>();
            if(root == null) return rightView;
            // 输出每层最右边的节点
            Queue<TreeNode> q = new LinkedList<>();
            q.add(root);
            rightView.add(root.val);
            while(!q.isEmpty()){
                int size = q.size();
                int rightValue = -1;
                for(int i = 0;i < size;i++){
                    TreeNode temp = q.poll();
                    if(temp.left != null){
                        q.add(temp.left);
                        rightValue = temp.left.val; // 更新最右边的点
                    }
                    if(temp.right != null){
                        q.add(temp.right);
                        rightValue = temp.right.val; // 更新最右边的点
                    }
                }
                if(rightValue != -1) rightView.add(rightValue);
            }
            return rightView;
        }
    }
    
  • 可改进的点:不用一直更新右边的值,当遍历到本层最右边的值时,加入其值即可

    class Solution {
        public List<Integer> rightSideView(TreeNode root) {
            List<Integer> res = new ArrayList<>();
            if(root == null) return res;
            // 输出每层最右边的节点
            Queue<TreeNode> q = new LinkedList<>();
            q.add(root);
            while(!q.isEmpty()){
                int size = q.size();
                for(int i = 0;i < size;i++){
                    TreeNode temp = q.poll();
                    if(temp.left != null) q.add(temp.left);
                    if(temp.right != null) q.add(temp.right);
                    if(i == size - 1) res.add(temp.val); // 遍历到本层最右边的值时,加入其值即可
                }
            }
            return res;
        }
    }
    

5. 对称二叉树

  • 题目描述:给定一个二叉树,检查它是否是镜像对称的。
    • 我的思路(6% - 8%):list保存每层的节点值,判断当前list是否对称
class Solution {
    public boolean isSymmetric(TreeNode root) {
        if(root == null) return true;
        Queue<TreeNode> q = new LinkedList<>();
        List<Integer> list;
        // Stack<Integer> Stack = new Stack<Integer>(); // 用栈实现
        q.add(root);
        while(!q.isEmpty()){
            int size = q.size();
            list = new ArrayList<>();
            for(int i = 0;i<size;i++){
                TreeNode temp = q.poll();
                if(temp.left != null){
                    q.add(temp.left);
                    list.add(temp.left.val);
                }else{
                    list.add(-100); // null用-100代替
                }
                if(temp.right != null){
                    q.add(temp.right);
                    list.add(temp.right.val);
                }else{
                    list.add(-100);
                }
            }
            if(!isSymmetricHelper(list)) return false;
        }
        return true;

    }
    // 判断序列是否对称
    private boolean isSymmetricHelper(List<Integer> list){
        int i = 0;
        int j = list.size() - 1;
        while(i <= j){
            if(list.get(i) != list.get(j)) return false;
            else{
                i++;
                j--;
            }
        }
        return true;
    }
}
  • 优秀思路1:【递归】因镜像对称,定义双指针p、q从根节点开始跑,p向左时q向右,p向右时q向左,两指针对应节点值必然相等,若不等则说明不对称
class Solution {
    public boolean isSymmetric(TreeNode root) {
        if(root == null) return true;
        return check(root, root);
    }
    private boolean check(TreeNode p, TreeNode q){
        if(p == null && q == null) return true;
        else if((p == null || q == null) || p.val != q.val) return false;
        else return check(p.left,q.right) && check(p.right,q.left);
    }
}
  • 优秀思路2:【迭代】因镜像对称,定义双指针p、q从根节点开始跑,p向左时q向右,p向右时q向左,两指针对应节点值必然相等,若不等则说明不对称
class Solution {
    public boolean isSymmetric(TreeNode root) {
        if(root == null) return true;
        return check(root, root);
    }
    private boolean check(TreeNode p, TreeNode q){
        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(p);
        queue.add(q);
        while(!queue.isEmpty()){
            p = queue.poll();
            q = queue.poll();
            if(p == null && q == null) continue; // 注意:局部相同不能说明相同
            else if((p == null || q == null) || p.val != q.val) return false;
            else{
                queue.add(p.left);
                queue.add(q.right);

                queue.add(p.right);
                queue.add(q.left);
            }
        }
        return true;
    }
}

6. 单词接龙(困难,需重写)

  • 题目描述:字典 wordList 中从单词 beginWordendWord转换序列 是一个按下述规格形成的序列,给你两个单词 beginWordendWord 和一个字典 wordList ,找到从 beginWordendWord最短转换序列 中的 单词数目 。如果不存在这样的转换序列,返回 0
    • 序列中第一个单词是 beginWord
    • 序列中最后一个单词是 endWord
    • 每次转换只能改变一个字母。
    • 转换过程中的中间单词必须是字典 wordList 中的单词。
  • 一般思路:传统BFS
class Solution {
   public int ladderLength(String beginWord, String endWord, List<String> wordList) {
		Queue<String> queue = new LinkedList<String>();
		queue.add(beginWord);
		boolean[] vis = new boolean[wordList.size()+1];
		int layer = 1;
		while(!queue.isEmpty()) {
			layer++;
			int size = queue.size();
			while(size-->0) { // 遍历所有层
				String cur = queue.poll(); // 取出当前遍历的单词
				for (int i = 0; i < wordList.size(); i++) {
					if(!vis[i]){
                        String next = wordList.get(i);
                        if(canChange(next,cur)) {
                            if(next.equals(endWord)) return layer;
                            queue.add(next);
                            vis[i] = true;
                        }
                    }
					
				}
			}
		}
		return 0;
	}

    //判断两单词是否可以相互转换
	public boolean canChange(String s,String t) {
		int unSame = 0;
		for (int i = 0; i < s.length(); i++) {
			if(s.charAt(i) != t.charAt(i)) unSame++;
            if(unSame > 1) return false;
		}
		return true;
	}
}
  • 优秀思路:双向BFS,定义双队列分别从beginWordendWord开始搜索,遍历每一层时总是从层单词数较少的那段开始搜索。(小技巧就是:每次比较两队列的size,总是让queue1存储size较少的队列,就可以实现总是对queue1进行操作)
class Solution {
    public int ladderLength(String beginWord, String endWord, List<String> wordList) {
        if(!wordList.contains(endWord)) return 0;

        int end = wordList.indexOf(endWord);
        wordList.add(beginWord);
        int start = wordList.size() - 1;
        Queue<Integer> queue1 = new LinkedList<>();
        Queue<Integer> queue2 = new LinkedList<>();
        Set<Integer> visited1 = new HashSet<>();
        Set<Integer> visited2 = new HashSet<>();
        queue1.offer(start);
        queue2.offer(end);
        visited1.add(start);
        visited2.add(end);
        int count = 1;
        while (!queue1.isEmpty() && !queue2.isEmpty()) {
            count++;
            // 若queue1的当前层size比queue2大,交换的时候queue2也同时保存了queue1上次遍历的结果
            if (queue1.size() > queue2.size()) {
                Queue<Integer> tmp = queue1;
                queue1 = queue2;
                queue2 = tmp;

                Set<Integer> t = visited1;
                visited1 = visited2;
                visited2 = t;
            }
            // 从size较小的一头开始搜索
            int size1 = queue1.size();
            while (size1-- > 0) {
                String s = wordList.get(queue1.poll());
                for (int i = 0; i < wordList.size(); ++i) {
                    if (visited1.contains(i) || !canConvert(s, wordList.get(i))) continue; // 遍历过了 或 不能相互转换
                    // 核心
                    if (visited2.contains(i)) return count;// 另一端也遍历到了这里
                    visited1.add(i);
                    queue1.offer(i);
                }
            }
        }
        return 0;
    }
    // 判断两单词是否可以相互转换
    public boolean canConvert(String a, String b) {
        int count = 0;
        for (int i = 0; i < a.length(); ++i) {
            if (a.charAt(i) != b.charAt(i)) {
                if (++count > 1) return false;
            }
        }
        return count == 1;
    }
}
  • 优秀思路改进:优化判断单词是否可以转换的过程,因为单词是由 a~z 这有限数量的字符组成的,可以遍历当前单词能转换成的所有单词,判断其是否包含在候选单词中。候选单词用 HashSet 保存,可以大大提高判断包含关系的性能。
class Solution {
    public int ladderLength(String beginWord, String endWord, List<String> wordList) {
        if(!wordList.contains(endWord)) return 0;
        
        wordList.add(beginWord);
        // 从两端 BFS 遍历要用的队列
        Queue<String> queue1 = new LinkedList<>();
        Queue<String> queue2 = new LinkedList<>();

        // 两端已经遍历过的节点
        Set<String> visited1 = new HashSet<>();
        Set<String> visited2 = new HashSet<>();
        queue1.offer(beginWord);
        queue2.offer(endWord);
        visited1.add(beginWord);
        visited2.add(endWord);
        
        int count = 0;
        // 将所有候选单词存在HashSet里,方便查找
        Set<String> allWordSet = new HashSet<>(wordList);

        while (!queue1.isEmpty() && !queue2.isEmpty()) {
            count++;
            // 从较小一端开始搜索
            if (queue1.size() > queue2.size()) {
                Queue<String> tmp = queue1;
                queue1 = queue2;
                queue2 = tmp;

                Set<String> t = visited1;
                visited1 = visited2;
                visited2 = t;
            }
            // 遍历该层
            int size1 = queue1.size();
            while (size1-- > 0) {
                String s = queue1.poll();
                char[] chars = s.toCharArray();
                for (int j = 0; j < s.length(); ++j) {
                    // 保存第j位的原始字符
                    char c0 = chars[j];
                    for (char c = 'a'; c <= 'z'; ++c) {
                        chars[j] = c;
                        // 修改后的单词,判断其是否包含于候选单词set
                        String newString = new String(chars);
                        // 已经访问过了,跳过
                        if (visited1.contains(newString)) {
                            continue;
                        }
                        // 两端遍历相遇,结束遍历,返回 count
                        if (visited2.contains(newString)) {
                            return count + 1;
                        }
                        // 如果单词在列表中存在,将其添加到队列,并标记为已访问
                        if (allWordSet.contains(newString)) {
                            queue1.offer(newString);
                            visited1.add(newString);
                        }
                    }
                    // 恢复第j位的原始字符
                    chars[j] = c0;
                }
            }
        }
        return 0;
    }
}

7. 二叉树的层序遍历

  • 题目描述:给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。
    在这里插入图片描述

  • 优秀思路(我的):BFS

class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> res = new ArrayList<>();
        if(root == null) return res;

        List<Integer> tempList;
        Queue<TreeNode> q = new LinkedList<>();
        q.offer(root);
        
        while(!q.isEmpty()){
            int size = q.size();
            tempList = new ArrayList<Integer>();
            while(size-- > 0){
                TreeNode cur = q.poll();
                tempList.add(cur.val);
                if(cur.left != null) q.offer(cur.left);
                if(cur.right != null) q.offer(cur.right);
            }
            res.add(tempList);
        }
        return res;
    }
}

8. 接雨水 II(平面接雨水的升级版,需重写)

  • 题目描述:给你一个 m x n 的矩阵,其中的值均为非负整数,代表二维高度图每个单元的高度,请计算图中形状最多能接多少体积的雨水。
  • 优秀思路
class Solution {
    public int trapRainWater(int[][] heights) {
        if (heights == null || heights.length < 3 || heights[0].length < 3) return 0;
        int n = heights.length;
        int m = heights[0].length;

        // 用一个vis数组来标记这个位置有没有被访问过
        boolean[][] vis = new boolean[n][m];

        // 优先队列中存放三元组 [x,y,h] 坐标和高度
        PriorityQueue<int[]> pq = new PriorityQueue<>((o1, o2) -> o1[2] - o2[2]);// 意思是比较两元素的索引2处的值,0 1 2分别存储的 i j height

        // 先把最外一圈放进去
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (i == 0 || i == n - 1 || j == 0 || j == m - 1) {
                    pq.offer(new int[]{i, j, heights[i][j]});
                    vis[i][j] = true;
                }
            }
        }
        int res = 0;
        // 方向数组,把dx和dy压缩成一维来做
        int[] dirs = {-1, 0, 1, 0, -1};
        while (!pq.isEmpty()) {
            int[] poll = pq.poll();
            // 看一下周围四个方向,没访问过的话能不能往里灌水
            for (int k = 0; k < 4; k++) {
                int nx = poll[0] + dirs[k];
                int ny = poll[1] + dirs[k + 1];
                // 如果位置合法且没访问过
                if (nx >= 0 && nx < n && ny >= 0 && ny < m && !vis[nx][ny]) {
                    // 如果外围这一圈中最小的比当前这个还高,那就说明能往里面灌水啊
                    if (poll[2] > heights[nx][ny]) {
                        res += poll[2] - heights[nx][ny];
                    }
                    // 如果灌水,高度得是你灌水后的高度了,如果没灌水也要取高的
                    pq.offer(new int[]{nx, ny, Math.max(heights[nx][ny], poll[2])});
                    vis[nx][ny] = true;
                }
            }
        }
        return res;
    }
}

9.克隆图

  • 题目描述:给你无向 连通 图中一个节点的引用,请你返回该图的 深拷贝(克隆)。图中的每个节点都包含它的值 val(int) 和其邻居的列表(list[Node])。
  • 优秀思路1:【递归】深度优先搜索
class Solution {
    private HashMap<Node,Node> mapClone = new HashMap<>();
    public Node cloneGraph(Node node) {
        if(node == null) return node;
        if(mapClone.containsKey(node)) return mapClone.get(node); // 已拷贝
        
        else{
            Node nodeClone = new Node(node.val,new ArrayList<Node>());// 拷贝节点的val
            mapClone.put(node,nodeClone);// 更新已拷贝map
            for(Node nei:node.neighbors){ // 拷贝节点的neighbors
                nodeClone.neighbors.add(cloneGraph(nei)); //核心!!!拷贝节点的nei = 拷贝的node的nei
            }
            return nodeClone;
        }
    }
}
  • 优秀思路2:广度优先搜索
class Solution {
    public Node cloneGraph(Node node) {
        if (node == null)  return node;
        HashMap<Node,Node> mapVis = new HashMap<>();
        Node newNode = new Node(node.val,new ArrayList<Node>());
        mapVis.put(node,newNode);

        Queue<Node> q = new LinkedList<>();
        q.add(node);
        while(!q.isEmpty()){
            Node cur = q.poll();
            for(Node nei : cur.neighbors){
                if(!mapVis.containsKey(nei)){
                    mapVis.put(nei,new Node(nei.val,new ArrayList<Node>()));
                    q.add(nei);
                }
                mapVis.get(cur).neighbors.add(mapVis.get(nei));
            }
        }
        return mapVis.get(node);
    }
}

10. 、二叉树的层序遍历 II(同7)

  • 题目描述:给定一个二叉树,返回其节点值自底向上的层序遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)
    在这里插入图片描述

  • 优秀思路

class Solution {
    public List<List<Integer>> levelOrderBottom(TreeNode root) {
        List<List<Integer>> res = new ArrayList<>();
        List<List<Integer>> res2 = new ArrayList<>();
        if(root == null) return res;

        List<Integer> tempList;
        Queue<TreeNode> q = new LinkedList<>();
        q.offer(root);
        
        while(!q.isEmpty()){
            int size = q.size();
            tempList = new ArrayList<Integer>();
            while(size-- > 0){
                TreeNode cur = q.poll();
                tempList.add(cur.val);
                if(cur.left != null) q.offer(cur.left);
                if(cur.right != null) q.offer(cur.right);
            }
            res.add(0,tempList); // 0表示插入的索引,保证总是将后面的插到前面
        }
        return res;
    }
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值