给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
上下是0,左右是0即是岛屿
DFS(深度优先搜索)问题通常是在树或者图结构上进行的,岛屿问题是这类网格 DFS 问题的典型代表。
网格类问题的 DFS 遍历方法通用思路:
网格问题是由 m*n 个小方格组成一个网格,每个小方格与其上下左右四个方格认为是相邻的,要在这样的网格上进行某种搜索。
岛屿问题是一类典型的网格问题。每个格子中的数字可能是 0 或者 1。我们把数字为 0 的格子看成海洋格子,数字为 1 的格子看成陆地格子,这样相邻的陆地格子就连接成一个岛屿。
在这样一个设定下,就出现了各种岛屿问题的变种,包括岛屿的数量、面积、周长等。不过这些问题,基本都可以用 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,我们完全可以参考二叉树的 DFS,写出网格 DFS 的两个要素:
网格结构中的格子有上下左右四个相邻结点。对于格子 (r, c) 来说(r 和 c 分别代表行坐标和列坐标),四个相邻的格子分别是 (r-1, c)、(r+1, c)、(r, c-1)、(r, c+1)。
网格 DFS 中的 base case是网格中不需要继续遍历、grid[r][c] 会出现数组下标越界异常的格子,也就是那些超出网格范围的格子。先往四个方向走一步再说,如果发现走出了网格范围再赶紧返回。这跟二叉树的遍历方法是一样的,先递归调用,发现 root == null 再返回。
网格 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)