面试经典150题0507

面试经典150题0507

Leetcode200 岛屿数量

网格类问题的DFS遍历方法

网格问题的基本概念:由m×n个小方格组成一个网格,每个小方格与其上下左右四个方格认为是相邻的,要在这样的网格上进行某种搜索。

岛屿问题是一类典型的网格问题,每个格子数字为0或者1。把0看作海洋,把1看作陆地,相邻的陆地就可以看作一个岛屿。

在这样的设定下,出现了各种岛屿问题的变种,包括岛屿的数量、面积、周长等。

DFS基本结构:

网格结构其实是一种简化版的图结构。

二叉树的DFS方法如下:

void traverse(TreeNode root){
    // 判断结束条件
    if(root == null){
        return;
    }
    // 访问两个相邻节点
    traverse(root.left);
    traverse(root.right);
}

二叉树的DFS由两个要素组成:访问相邻节点判断结束条件

第一个要素为访问相邻节点。二叉树结构简单,只有左子节点和右子节点两个。二叉树本身就是一个递归定义个结构:一颗二叉树,它的左子树和右子树也是一颗二叉树。那么我们的DFS遍历只需要递归调用左子树和右子树即可。

返回条件判断其实有两个含义:一方面,这表示 root 指向的子树为空,不需要再往下遍历了。另一方面,在 root == null 的时候及时返回,可以让后面的 root.leftroot.right 操作不会出现空指针异常。

参考二叉树的DFS,写出网格DFS的两个要素:

首先,网格中的每个格子有上下左右四个节点。

image-20240507130057885

其次,网格DFS的返回条件为网格中不需要继续遍历,grid[r][c]会出现数组下标越界异常的格子,也就是那些超出网格范围的格子。

image-20240507130307096

不管当前是在哪个格子,先往四个方向走一步再说,如果发现走出了网格范围再赶紧返回。这跟二叉树的遍历方法是一样的,先递归调用,发现 root == null 再返回。

网格DFS的遍历框架:

void dfs(int[][] grid, int r, int c){
    // 判断返回条件
    // 如果坐标(r, c)超出网格范围,直接返回
    if(!inArea(grid, r, c)){
        return;
    }
    // 访问相邻节点
    dfs(grid, r - 1, c);
    dfs(grid, r + 1, c);
    dfs(grid, r, c - 1);
    dfs(grid, r, c + 1);
}

// 判断坐标(r, c)是否在网格中
boolean inArea(int[][] grid, int r, int c){
    return 0 <= r && r < grid.length && 0 <= c && c < grid[0].length;
}

如何避免重复遍历

网格结构的 DFS 与二叉树的 DFS 最大的不同之处在于,遍历中可能遇到遍历过的结点。这是因为,网格结构本质上是一个「图」,我们可以把每个格子看成图中的结点,每个结点有向上下左右的四条边。在图中遍历时,自然可能遇到重复遍历结点。

因此需要标记已经遍历过的格子。在岛屿问题中,需要在所有值为1的陆地格子上做DFS遍历。没走过一个陆地格子,就把值改为2;当再遇到2的时候,知道这是遍历过的格子。

void dfs(int[][] grid, int r, int c) {
    // 返回条件
    if (!inArea(grid, r, c)) {
        return;
    }
    // 如果这个格子不是岛屿,直接返回
    if (grid[r][c] != 1) {
        return;
    }
    grid[r][c] = 2; // 将格子标记为「已遍历过」
    
    // 访问上、下、左、右四个相邻结点
    dfs(grid, r - 1, c);
    dfs(grid, r + 1, c);
    dfs(grid, r, c - 1);
    dfs(grid, r, c + 1);
}

// 判断坐标 (r, c) 是否在网格中
boolean inArea(int[][] grid, int r, int c) {
    return 0 <= r && r < grid.length 
        	&& 0 <= c && c < grid[0].length;
}

参考链接:https://leetcode.cn/problems/number-of-islands/solutions/211211/dao-yu-lei-wen-ti-de-tong-yong-jie-fa-dfs-bian-li-/?envType=study-plan-v2&envId=top-interview-150

Leetcode463 岛屿的周长

对于DFS直接返回有下面几种情况:

  • !inArea(grid, r, c),坐标超出网格范围。
  • grid != 1,即当前格子不是岛屿格子,分为如下两种情况:
    • grid[r][c] == 0,当前格子是海洋格子
    • grid[r][c] == 2,当前格子是已经遍历的陆地格子

岛屿的周长是计算岛屿全部的边缘,而这些边缘就再DFS函数返回的位置。如下,黄色的边是与网格边界相邻的周长,蓝色的边是与海洋格子相邻的周长。

image-20240507132236585

当因坐标超出网格范围就返回一条黄色的边,当因海洋格子返回的时候就返回一条蓝色的边。

public int islandPerimeter(int[][] grid) {
    for (int r = 0; r < grid.length; r++) {
        for (int c = 0; c < grid[0].length; c++) {
            if (grid[r][c] == 1) {
                // 题目限制只有一个岛屿,计算一个即可
                return dfs(grid, r, c);
            }
        }
    }
    return 0;
}

int dfs(int[][] grid, int r, int c) {
    // 函数因为「坐标 (r, c) 超出网格范围」返回,对应一条黄色的边
    if (!inArea(grid, r, c)) {
        return 1;
    }
    // 函数因为「当前格子是海洋格子」返回,对应一条蓝色的边
    if (grid[r][c] == 0) {
        return 1;
    }
    // 函数因为「当前格子是已遍历的陆地格子」返回,和周长没关系
    if (grid[r][c] != 1) {
        return 0;
    }
    grid[r][c] = 2;
    return dfs(grid, r - 1, c)
        + dfs(grid, r + 1, c)
        + dfs(grid, r, c - 1)
        + dfs(grid, r, c + 1);
}

// 判断坐标 (r, c) 是否在网格中
boolean inArea(int[][] grid, int r, int c) {
    return 0 <= r && r < grid.length 
        	&& 0 <= c && c < grid[0].length;
}
Leetcode695 岛屿的最大面积

找出给定二维数组中最大的岛屿面积。如果没有岛屿,则返回面积0

本题只需要对每个岛屿做DFS遍历,求出每个岛屿的面积。求面积即每遍历一个陆地格子就把面积+1;

public int maxAreaOfIsland(int[][] grid) {
    int res = 0;
    for (int r = 0; r < grid.length; r++) {
        for (int c = 0; c < grid[0].length; c++) {
            if (grid[r][c] == 1) {
                int a = area(grid, r, c);
                res = Math.max(res, a);
            }
        }
    }
    return res;
}

int area(int[][] grid, int r, int c) {
    if (!inArea(grid, r, c)) {
        return 0;
    }
    if (grid[r][c] != 1) {
        return 0;
    }
    grid[r][c] = 2;
    
    return 1 
        + area(grid, r - 1, c)
        + area(grid, r + 1, c)
        + area(grid, r, c - 1)
        + area(grid, r, c + 1);
}

boolean inArea(int[][] grid, int r, int c) {
    return 0 <= r && r < grid.length 
        	&& 0 <= c && c < grid[0].length;
}
Leetcode130 被围绕的区域

本题说明了被包围的区域不会存在于边界上,边界上的O要做特殊处理,那么剩下的O直接替换为X即可。问题转化为:如何寻找和边界联通的O

X X X X

X O O X

X X O X

X O O X

这种情况下O是不做替换的,因为和边界是连通的。可以把这种情况下的O换成#作为占位符,待搜索结束后,遇到O则替换为X,遇到#则替换为O。

如何寻找和边界联通的O?从边界出发,对网格进行DFS或者BFS即可。

class Solution {
    public void solve(char[][] board) {
        if (board == null || board.length == 0) return;
        int m = board.length;
        int n = board[0].length;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                // 从边缘o开始搜索
                boolean isEdge = i == 0 || j == 0 || i == m - 1 || j == n - 1;
                if (isEdge && board[i][j] == 'O') {
                    dfs(board, i, j);
                }
            }
        }

        // 搜索完毕后对字符进行替换
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (board[i][j] == 'O') {
                    board[i][j] = 'X';
                }
                if (board[i][j] == '#') {
                    board[i][j] = 'O';
                }
            }
        }
    }

    public void dfs(char[][] board, int i, int j) {
        if (i < 0 || j < 0 || i >= board.length  || j >= board[0].length || board[i][j] == 'X' || board[i][j] == '#') {
            // board[i][j] == '#' 说明已经搜索过了. 
            return;
        }
        board[i][j] = '#';
        dfs(board, i - 1, j); // 上
        dfs(board, i + 1, j); // 下
        dfs(board, i, j - 1); // 左
        dfs(board, i, j + 1); // 右
    }
}
Leetcode133 克隆图

遍历整个图,在遍历的时候,记录已经访问过的点,使用一个字典记录。

深度遍历

class Solution{
    public Node cloneGraph(Node node){
        // map记录已经遍历过的节点
        Map<Node, Node> lookup = new HashMap<>();
        return dfs(node, lookup);
    }
    
    private Node dfs(Node node, Map<Node, Node> lookup){
        if(node == null){
            return null;
        }
        if(lookup.containsKey(node)){
            // 已经遍历过该节点
            return lookup.get(node);
        }
        Node clone = new Node(node.val, new ArrayList<>());
        lookup.put(node, clone);
        for(Node n : node.neighbors){
            clone.neighbors.add(dfs(n, lookup));
        }
        return clone;
    }
}
Leetcode399 除法求值

有向图搜索问题,本质上求两个节点之间的距离。

  • 首先定义邻接节点,里面有两个字段,分别表示邻接节点的名称和当前节点到达邻接节点所需要的倍数。
  • 然后构造一个map来存储图,map的键就是节点名称,map的值就是节点的邻接节点列表
  • 遍历给定所有算式,将节点和值都存到map中
  • 遍历需要求的问题,深搜每个节点,为了防止重复搜索,用一个集合存储已经搜索过的节点
public class Solution {
    // 构造一个map用于存储图
    static Map<String, List<Node>> map;
    public static double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries){
        int n = equations.size();
        map = new HashMap<>();
        // 存储查询结果
        double[] res = new double[queries.size()];

        // 构建图
        for (int i = 0; i < n; i++) {
            // 获取被除数和除数的节点名称
            String divided = equations.get(i).get(0);
            String divisor = equations.get(i).get(1);
            // 如果map中还没有创建某个节点字符串的键值对,则添加一个键值对
            if(!map.containsKey(divided)){
                map.put(divided, new ArrayList<>());
            }
            if(!map.containsKey(divisor)){
                map.put(divisor, new ArrayList<>());
            }

            // 创建的图为有向图
            // 除数和被除数的倍数关系为被除数和除数倍数的倒数
            map.get(divided).add(new Node(divisor, values[i]));
            map.get(divisor).add(new Node(divided, 1 / values[i]));
        }
        int cnt = 0;
        // 遍历
        for(List<String> q : queries){
            // dfs,初始倍数为1
            res[cnt] = dfs(q.get(0), q.get(1), 1.0, new HashSet<>());
            cnt++;
        }
        return res;
    }

    // cur表示当前节点,dst表示目的节点, times表示计算的倍数,set存储已经访问过的节点
    public static double dfs(String cur, String dst, double times, Set<String> set){
        // 如果map不包括当前节点或者已经走过当前节点,说明这条路径不会产生答案
        if(!map.containsKey(cur) || set.contains(cur)){
            return -1.0;
        }
        // 走到了终点,返回计算过的倍数
        if(cur.equals(dst)){
            return times;
        }
        // 在set中添加当前访问的节点
        set.add(cur);
        // 遍历当前节点的邻接节点
        for(Node node: map.get(cur)){
            // dfs,倍数需要乘上下一个节点的倍数
            double tmp = dfs(node.id, dst, times * node.num, set);
            // 搜到结果,直接返回
            if(tmp != -1.0){
                return tmp;
            }
        }
        return -1.0;
    }
}

class Node{
    // 邻接节点代表的字符串
    public String id;
    // 到达邻接节点所需的倍数
    public double num;
    public Node(String i, double n){
        id = i;
        num = n;
    }
}
Leetcode207 课程表

本题可简化为课程安排图是否是有向无环图(DAG),即课程间规定了前置条件,但不能构成任何环路,否则课程前置条件不能成立。

通过拓扑排序判断课程安排图是否是有向无环图。

**拓扑排序:**对DAG的顶点进行排序,使得对每一条有向边(u,v),均有u比v先出现。

同故课程前置条件列表prerequisites 可以得到课程安排图的 邻接表 adjacency,以降低算法时间复杂度,以下两种方法都会用到邻接表。

**深度优先遍历:**通过DFS判断图中是否有环。

  • 借助一个标志列表flags,用来判断每个节点(课程)i的状态:
    • 未被DFS访问:i==0
    • 已经被其它节点启动的DFS访问:i==-1
    • 已经被当前节点启动的DFS访问:i==1
  • numCourses个节点依次执行DFS,判断每个节点起步DFS是否存在环,如果存在直接返回False;DFS过程如下:
    • 终止条件
      • flag[i] == -1,说明当前访问节点已经被其它节点启动的DFS访问,无需重复搜索,直接返回True
      • flag[i] == 1,说明在本轮DFS搜索中节点i已经被二次访问,课程安排图有环,直接返回false
    • 将当前访问节点i对应的flag[i]置为1,即标记其被本轮DFS访问过。
    • 递归访问当前节点i的所有邻接节点j,当发现环直接返回false
    • 当前节点所有邻接节点已经被遍历,并没有发现环,则将当前节点flag置为-1并返回true
  • 若整个图的DFS都没有发现环,返回True
public class A0507canFinish {
    public static boolean canFinish(int numCourses, int[][] prerequisites){
        // 创建邻接表,表示图
        List<List<Integer>> adj = new ArrayList<>();
        for (int i = 0; i < numCourses; i++) {
            adj.add(new ArrayList<>());
        }
        int[] flags = new int[numCourses];
        for(int[] p: prerequisites){
            adj.get(p[1]).add(p[0]);
        }
        for(int i = 0; i < numCourses; i++){
            if(!dfs(adj, flags, i)){
                return false;
            }
        }
        return true;
    }
    
    public static boolean dfs(List<List<Integer>> adj, int[] flags, int i){
        if(flags[i] == 1){
            // 当前DFS已经第二次访问
            return false;
        }
        if(flags[i] == -1){
            // 之前节点的DFS访问过
            return true;
        }
        // 标记访问过当前节点
        flags[i] = 1;
        // 遍历当前节点的邻接点
        for(Integer j: adj.get(i)){
            if(!dfs(adj, flags, j)){
                return false;
            }
        }
        // 记录当前节点被遍历过
        flags[i] = -1;
        return true;
    }
}
Leetcode210 课程表Ⅱ

深度优先搜索

Leetcode207 课程表的基础上,添加一个栈用来保存遍历的节点。

class Solution {
    public int[] findOrder(int numCourses, int[][] prerequisites){
        // 建立邻接表
        List<List<Integer>> adj = new ArrayList<>();
        for (int i = 0; i < numCourses; i++) {
            adj.add(new ArrayList<>());
        }
        int[] flags = new int[numCourses];
        for(int[] p: prerequisites){
            adj.get(p[1]).add(p[0]);
        }
        // 用栈保存访问序列
        Stack<Integer> stack = new Stack<>();
        for (int i = 0; i < numCourses; i++) {
            if(!dfs(adj, flags, stack, i)){
                return new int[0];
            }
        }
        int[] res = new int[numCourses];
        for (int i = 0; i < numCourses; i++) {
            res[i] = stack.pop();
        }
        return res;
    }

    public boolean dfs(List<List<Integer>> adj, int[] flags, Stack<Integer> stack, int i){
        if(flags[i] == 1){
            // 本轮DFS已经访问过,形成环
            return false;
        }
        if(flags[i] == -1){
            // 前面节点的DFS已经访问过
            return true;
        }
        // 修改标志位
        flags[i] = 1;
        for (int j: adj.get(i)) {
            // dfs当前课程的后续课程
            if(!dfs(adj, flags, stack, j)){
                return false;
            }
        }
        // 修改标记位
        flags[i] = -1;
        stack.push(i);
        return true;
    }
}
r> stack, int i){
        if(flags[i] == 1){
            // 本轮DFS已经访问过,形成环
            return false;
        }
        if(flags[i] == -1){
            // 前面节点的DFS已经访问过
            return true;
        }
        // 修改标志位
        flags[i] = 1;
        for (int j: adj.get(i)) {
            // dfs当前课程的后续课程
            if(!dfs(adj, flags, stack, j)){
                return false;
            }
        }
        // 修改标记位
        flags[i] = -1;
        stack.push(i);
        return true;
    }
}
  • 27
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值