// 二维矩阵遍历框架 voiddfs(int[][] grid, int i, int j, boolean[] visited){ int m = grid.length, n = grid[0].length; if (i < 0 || j < 0 || i >= m || j >= n) { // 超出索引边界 return; } if (visited[i][j]) { // 已遍历过 (i, j) return; } // 前序:进入节点 (i, j) visited[i][j] = true; dfs(grid, i - 1, j); // 上 dfs(grid, i 1, j); // 下 dfs(grid, i, j - 1); // 左 dfs(grid, i, j 1); // 右 // 后序:离开节点 (i, j) // visited[i][j] = true; } 因为二维矩阵本质上是一幅「图」,所以遍历的过程中需要一个visited布尔数组防止走回头路,如果你能理解上面这段代码,那么搞定所有岛屿问题都很简单。这里额外说一个处理二维数组的常用小技巧,你有时会看到使用「方向数组」来处理上下左右的遍历,和图遍历框架的代码很类似:// 方向数组,分别代表上、下、左、右 int[][] dirs = newint[][]{{-1,0}, {1,0}, {0,-1}, {0,1}};
voiddfs(int[][] grid, int i, int j, boolean[] visited){ int m = grid.length, n = grid[0].length; if (i < 0 || j < 0 || i >= m || j >= n) { // 超出索引边界 return; } if (visited[i][j]) { // 已遍历过 (i, j) return; }
// 进入节点 (i, j) visited[i][j] = true; // 递归遍历上下左右的节点 for (int[] d : dirs) { int next_i = i d[0]; int next_j = j d[1]; dfs(grid, next_i, next_j); } // 离开节点 (i, j) // visited[i][j] = true; } 这种写法无非就是用 for 循环处理上下左右的遍历罢了,你可以按照个人喜好选择写法。
岛屿数量
这是力扣第 200 题「岛屿数量」,最简单也是最经典的一道岛屿问题,题目会输入一个二维数组grid,其中只包含0或者1,0代表海水,1代表陆地,且假设该矩阵四周都是被海水包围着的。我们说连成片的陆地形成岛屿,那么请你写一个算法,计算这个矩阵grid中岛屿的个数,函数签名如下:intnumIslands(char[][] grid); 比如说题目给你输入下面这个grid有四片岛屿,算法应该返回 4:思路很简单,关键在于如何寻找并标记「岛屿」,这就要 DFS 算法发挥作用了,我们直接看解法代码:// 主函数,计算岛屿数量 intnumIslands(char[][] grid){ int res = 0; int m = grid.length, n = grid[0].length; // 遍历 grid for (int i = 0; i < m; i ) { for (int j = 0; j < n; j ) { if (grid[i][j] == '1') { // 每发现一个岛屿,岛屿数量加一 res ; // 然后使用 DFS 将岛屿淹了 dfs(grid, i, j); } } } return res; }
// 从 (i, j) 开始,将与之相邻的陆地都变成海水 voiddfs(char[][] grid, int i, int j){ int m = grid.length, n = grid[0].length; if (i < 0 || j < 0 || i >= m || j >= n) { // 超出索引边界 return; } if (grid[i][j] == '0') { // 已经是海水了 return; } // 将 (i, j) 变成海水 grid[i][j] = '0'; // 淹没上下左右的陆地 dfs(grid, i 1, j); dfs(grid, i, j 1); dfs(grid, i - 1, j); dfs(grid, i, j - 1); } 为什么每次遇到岛屿,都要用 DFS 算法把岛屿「淹了」呢?主要是为了省事,避免维护visited数组。因为dfs函数遍历到值为0的位置会直接返回,所以只要把经过的位置都设置为0,就可以起到不走回头路的作用。
上一题说二维矩阵四周可以认为也是被海水包围的,所以靠边的陆地也算作岛屿。力扣第 1254 题「统计封闭岛屿的数目」和上一题有两点不同:1、用0表示陆地,用1表示海水。2、让你计算「封闭岛屿」的数目。所谓「封闭岛屿」就是上下左右全部被1包围的0,也就是说靠边的陆地不算作「封闭岛屿」。函数签名如下:intclosedIsland(int[][] grid) 比如题目给你输入如下这个二维矩阵:算法返回 2,只有图中灰色部分的0是四周全都被海水包围着的「封闭岛屿」。那么如何判断「封闭岛屿」呢?其实很简单,把上一题中那些靠边的岛屿排除掉,剩下的不就是「封闭岛屿」了吗?有了这个思路,就可以直接看代码了,注意这题规定0表示陆地,用1表示海水:// 主函数:计算封闭岛屿的数量 intclosedIsland(int[][] grid){ int m = grid.length, n = grid[0].length; for (int j = 0; j < n; j ) { // 把靠上边的岛屿淹掉 dfs(grid, 0, j); // 把靠下边的岛屿淹掉 dfs(grid, m - 1, j); } for (int i = 0; i < m; i ) { // 把靠左边的岛屿淹掉 dfs(grid, i, 0); // 把靠右边的岛屿淹掉 dfs(grid, i, n - 1); } // 遍历 grid,剩下的岛屿都是封闭岛屿 int res = 0; for (int i = 0; i < m; i ) { for (int j = 0; j < n; j ) { if (grid[i][j] == 0) { res ; dfs(grid, i, j); } } } return res; }
// 从 (i, j) 开始,将与之相邻的陆地都变成海水 voiddfs(int[][] grid, int i, int j){ int m = grid.length, n = grid[0].length; if (i < 0 || j < 0 || i >= m || j >= n) { return; } if (grid[i][j] == 1) { // 已经是海水了 return; } // 将 (i, j) 变成海水 grid[i][j] = 1; // 淹没上下左右的陆地 dfs(grid, i 1, j); dfs(grid, i, j 1); dfs(grid, i - 1, j); dfs(grid, i, j - 1); } 只要提前把靠边的陆地都淹掉,然后算出来的就是封闭岛屿了。
这道岛屿题目的解法稍微改改就可以解决力扣第 1020 题「飞地的数量」,这题不让你求封闭岛屿的数量,而是求封闭岛屿的面积总和。其实思路都是一样的,先把靠边的陆地淹掉,然后去数剩下的陆地数量就行了,注意第 1020 题中1代表陆地,0代表海水:intnumEnclaves(int[][] grid){ int m = grid.length, n = grid[0].length; // 淹掉靠边的陆地 for (int i = 0; i < m; i ) { dfs(grid, i, 0); dfs(grid, i, n - 1); } for (int j = 0; j < n; j ) { dfs(grid, 0, j); dfs(grid, m - 1, j); }
// 数一数剩下的陆地 int res = 0; for (int i = 0; i < m; i ) { for (int j = 0; j < n; j ) { if (grid[i][j] == 1) { res = 1; } } }
return res; }
// 和之前的实现类似 voiddfs(int[][] grid, int i, int j){ // ... } 篇幅所限,具体代码我就不写了,我们继续看其他的岛屿问题。
岛屿的最大面积
这是力扣第 695 题「岛屿的最大面积」,0表示海水,1表示陆地,现在不让你计算岛屿的个数了,而是让你计算最大的那个岛屿的面积,函数签名如下:intmaxAreaOfIsland(int[][] grid) 比如题目给你输入如下一个二维矩阵:其中面积最大的是橘红色的岛屿,算法返回它的面积 6。这题的大体思路和之前完全一样,只不过dfs函数淹没岛屿的同时,还应该想办法记录这个岛屿的面积。我们可以给dfs函数设置返回值,记录每次淹没的陆地的个数,直接看解法吧:intmaxAreaOfIsland(int[][] grid){ // 记录岛屿的最大面积 int res = 0; int m = grid.length, n = grid[0].length; for (int i = 0; i < m; i ) { for (int j = 0; j < n; j ) { if (grid[i][j] == 1) { // 淹没岛屿,并更新最大岛屿面积 res = Math.max(res, dfs(grid, i, j)); } } } return res; }
// 淹没与 (i, j) 相邻的陆地,并返回淹没的陆地面积 intdfs(int[][] grid, int i, int j){ int m = grid.length, n = grid[0].length; if (i < 0 || j < 0 || i >= m || j >= n) { // 超出索引边界 return0; } if (grid[i][j] == 0) { // 已经是海水了 return0; } // 将 (i, j) 变成海水 grid[i][j] = 0;
return dfs(grid, i 1, j) dfs(grid, i, j 1) dfs(grid, i - 1, j) dfs(grid, i, j - 1) 1; } 解法和之前相比差不多,我也不多说了,接下来的两道岛屿问题是比较有技巧性的,我们重点来看一下。
子岛屿数量
如果说前面的题目都是模板题,那么力扣第 1905 题「统计子岛屿」可能得动动脑子了:这道题的关键在于,如何快速判断子岛屿?肯定可以借助 Union Find 并查集 算法来判断,不过本文重点在 DFS 算法,就不展开并查集算法了。什么情况下grid2中的一个岛屿B是grid1中的一个岛屿A的子岛?当岛屿B中所有陆地在岛屿A中也是陆地的时候,岛屿B是岛屿A的子岛。反过来说,如果岛屿B中存在一片陆地,在岛屿A的对应位置是海水,那么岛屿B就不是岛屿A的子岛。那么,我们只要遍历grid2中的所有岛屿,把那些不可能是子岛的岛屿排除掉,剩下的就是子岛。依据这个思路,可以直接写出下面的代码:intcountSubIslands(int[][] grid1, int[][] grid2){ int m = grid1.length, n = grid1[0].length; for (int i = 0; i < m; i ) { for (int j = 0; j < n; j ) { if (grid1[i][j] == 0