LeetCode200:岛屿数量

要求—

给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
LeetCode200:岛屿数量_第1张图片

分析题目—

上下是0,左右是0即是岛屿
DFS(深度优先搜索)问题通常是在树或者图结构上进行的,岛屿问题是这类网格 DFS 问题的典型代表。

思路—

网格类问题的 DFS 遍历方法通用思路:
网格问题是由 m*n 个小方格组成一个网格,每个小方格与其上下左右四个方格认为是相邻的,要在这样的网格上进行某种搜索。
岛屿问题是一类典型的网格问题。每个格子中的数字可能是 0 或者 1。我们把数字为 0 的格子看成海洋格子,数字为 1 的格子看成陆地格子,这样相邻的陆地格子就连接成一个岛屿。
LeetCode200:岛屿数量_第2张图片
在这样一个设定下,就出现了各种岛屿问题的变种,包括岛屿的数量、面积、周长等。不过这些问题,基本都可以用 DFS 遍历来解决。
DFS 的基本结构
二叉树的 DFS 有两个要素:「访问相邻结点」和「判断 base case」
base case:在二叉树中,base case主要判断节点是否为空,如果为空,则直接返回,而在网格中,base case则为判断点是否越界,如果存在越界,则直接返回。

public void traverse(TreeNode root) {
    // 判断 base case
    if (root == null) {
        return;
    }
    // 访问两个相邻结点:左子结点、右子结点
    traverse(root.left);
    traverse(root.right);
}
  • 第一个要素是访问相邻结点。二叉树的相邻结点非常简单,只有左子结点和右子结点两个。二叉树本身就是一个递归定义的结构:一棵二叉树,它的左子树和右子树也是一棵二叉树。那么我们的 DFS 遍历只需要递归调用左子树和右子树即可。
  • 第二个要素是 判断 base case。一般来说,二叉树遍历的 base case 是 root ==null。这样一个条件判断其实有两个含义:一方面,这表示 root 指向的子树为空,不需要再往下遍历了。另一方面,在 root == null 的时候及时返回,可以让后面的 root.left 和 root.right 操作不会出现空指针异常。

对于网格上的 DFS,我们完全可以参考二叉树的 DFS,写出网格 DFS 的两个要素:

  • 网格结构中的格子有上下左右四个相邻结点。对于格子 (r, c) 来说(r 和 c 分别代表行坐标和列坐标),四个相邻的格子分别是 (r-1, c)、(r+1, c)、(r, c-1)、(r, c+1)。
    LeetCode200:岛屿数量_第3张图片

  • 网格 DFS 中的 base case是网格中不需要继续遍历、grid[r][c] 会出现数组下标越界异常的格子,也就是那些超出网格范围的格子。先往四个方向走一步再说,如果发现走出了网格范围再赶紧返回。这跟二叉树的遍历方法是一样的,先递归调用,发现 root == null 再返回。
    LeetCode200:岛屿数量_第4张图片
    网格 DFS 遍历的框架代码:

public void dfs(int[][] grid, int r, int c) {
    // 判断 base case
    // 如果坐标 (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 的时候,就知道这是遍历过的格子了。也就是说,每个格子可能取三个值:

0 —— 海洋格子
1 —— 陆地格子(未遍历过)
2 —— 陆地格子(已遍历过)

public class LeetCode200 {
    public int numIslands(char[][] grid) {
        //岛屿的数量
        int count = 0;
        //遍历整张表,grid.length行/grid[0].length列长度
        for (int i = 0; i < grid.length; i++) {
            for (int j = 0; j < grid[0].length; j++) {
                //遍历所有陆地
                if(grid[i][j] == '1'){
                    dfs(grid,i,j);
                    count ++; //统计岛屿
                }
            }
        }
        return count;
    }
    //DFS深度递归调用
    private void dfs(char[][] grid, int i, int j) {
        //判断是否越界,防止超出岛屿的网格范围
        if (i >= grid.length || i<0 || j >= grid[0].length || j<0){
            return;
        }
        //逻辑判断,如果不是陆地就直接返回
        if (grid[i][j] != '1'){
            return;
        }

        //避免循环遍历重复,做个标记
        grid[i][j] = '2';

        //上下左右递归遍历
        dfs(grid,i-1,j);
        dfs(grid,i+1,j);
        dfs(grid,i,j-1);
        dfs(grid,i,j+1);
    }
}

时间复杂度:O(ij) i代表行数j代表列数
空间复杂度:最坏的情况,都遍历过来O(ij)

你可能感兴趣的:(算法/LeetCode,深度优先,算法,图论,java,leetcode)