
📚 前言
亲爱的同学们,大家好!今天我们要一起探索一个非常经典且在面试中高频出现的算法问题——岛屿数量。这个问题不仅能够很好地检验我们对图论搜索算法的理解,还能锻炼我们的编程思维!✨
想象一下,你是一名探险家,手持一张由’0’(水)和’1’(陆地)组成的藏宝图。你的任务是数出这张图上有多少个岛屿(被水完全包围的陆地)。听起来简单,对吧?但当地图变得复杂,岛屿形状各异时,如何高效地完成这个任务呢?这就是我们今天要解决的问题!🧭
这个问题不仅是算法面试中的常客,更是理解深度优先搜索(DFS)和广度优先搜索(BFS)的绝佳案例。掌握了它,你将能够应对各种图论搜索问题,为你的算法之旅打下坚实基础。
让我们一起揭开"岛屿数量"这个经典问题的神秘面纱吧!👀
🧠 知识点说明
在深入问题之前,我们先来了解一些基础知识:
1. 问题描述
"岛屿数量"问题通常描述为:
给你一个由 '1'(陆地)和 '0'(水)组成的二维网格 grid,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
例如:
输入:grid = [
["1","1","1","1","0"],
["1","1","0","1","0"],
["1","1","0","0","0"],
["0","0","0","0","0"]
]
输出:1
输入:grid = [
["1","1","0","0","0"],
["1","1","0","0","0"],
["0","0","1","0","0"],
["0","0","0","1","1"]
]
输出:3
2. 深度优先搜索(DFS)的基本概念
深度优先搜索是一种用于遍历或搜索树或图的算法。它沿着树的深度遍历树的节点,尽可能深地搜索树的分支。当节点v的所有边都已被探寻过,搜索将回溯到发现节点v的那条边的起始节点。
DFS的基本思想是:
- 从起始节点开始,沿着一条路径一直走到底
- 当无法继续前进时,回溯到上一个节点,尝试另一条路径
- 重复上述过程,直到所有节点都被访问
在实现上,DFS通常使用递归或栈来实现。
3. 广度优先搜索(BFS)的基本概念
广度优先搜索是一种用于遍历或搜索树或图的算法。与DFS不同,BFS从起始节点开始,先访问起始节点的所有邻接节点,然后再访问这些邻接节点的邻接节点,以此类推。
BFS的基本思想是:
- 从起始节点开始,先访问所有距离为1的节点
- 然后访问所有距离为2的节点
- 以此类推,按照距离的递增顺序访问所有节点
在实现上,BFS通常使用队列来实现。
4. 网格问题的特点
在网格问题中,我们通常将每个单元格视为一个节点,相邻的单元格之间有边相连。在"岛屿数量"问题中:
- 节点是网格中的每个单元格
- 边连接水平或垂直相邻的单元格
- 我们只关心值为’1’的单元格(陆地)
🔍 重难点说明
解决"岛屿数量"问题有两种主要方法:DFS和BFS。让我们一一解析:
方法一:深度优先搜索(DFS)
DFS的思路是:
- 遍历整个网格
- 当找到一个值为’1’的单元格(陆地)时,将岛屿计数器加1
- 使用DFS将与当前陆地相连的所有陆地标记为已访问(通常通过将其值改为’0’或使用额外的访问数组)
- 继续遍历网格,重复步骤2和3,直到遍历完整个网格
这种方法的关键在于:每次发现一个新的陆地,我们就将与之相连的整个岛屿都标记为已访问,这样就不会重复计数。
方法二:广度优先搜索(BFS)
BFS的思路与DFS类似,但使用队列而不是递归或栈:
- 遍历整个网格
- 当找到一个值为’1’的单元格(陆地)时,将岛屿计数器加1
- 使用BFS将与当前陆地相连的所有陆地标记为已访问
- 继续遍历网格,重复步骤2和3,直到遍历完整个网格
难点解析
本题的难点在于:
- 理解如何使用DFS或BFS来标记整个岛屿
- 正确处理网格的边界条件
- 理解为什么我们可以直接修改原网格(将访问过的陆地改为水),而不需要额外的访问数组
下面我们将重点讲解DFS和BFS两种实现方法。
💻 核心代码说明
DFS实现
/**
* 岛屿数量 - DFS实现
*/
public class NumberOfIslandsDFS {
/**
* 计算岛屿数量
* @param grid 二维字符网格,'1'表示陆地,'0'表示水
* @return 岛屿的数量
*/
public static int numIslands(char[][] grid) {
// 边界条件检查
if (grid == null || grid.length == 0 || grid[0].length == 0) {
return 0;
}
int rows = grid.length;
int cols = grid[0].length;
int count = 0;
// 遍历整个网格
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
// 如果当前单元格是陆地
if (grid[i][j] == '1') {
// 岛屿数量加1
count++;
// 使用DFS将与当前陆地相连的所有陆地标记为已访问
dfs(grid, i, j, rows, cols);
}
}
}
return count;
}
/**
* DFS遍历与当前陆地相连的所有陆地,并标记为已访问
* @param grid 二维字符网格
* @param i 当前行索引
* @param j 当前列索引
* @param rows 网格的行数
* @param cols 网格的列数
*/
private static void dfs(char[][] grid, int i, int j, int rows, int cols) {
// 边界条件检查或当前单元格是水
if (i < 0 || i >= rows || j < 0 || j >= cols || grid[i][j] == '0') {
return;
}
// 将当前陆地标记为已访问(通过将其改为水)
grid[i][j] = '0';
// 递归访问上、下、左、右四个方向的相邻单元格
dfs(grid, i - 1, j, rows, cols); // 上
dfs(grid, i + 1, j, rows, cols); // 下
dfs(grid, i, j - 1, rows, cols); // 左
dfs(grid, i, j + 1, rows, cols); // 右
}
// 测试代码
public static void main(String[] args) {
// 测试用例1
char[][] grid1 = {
{'1', '1', '1', '1', '0'},
{'1', '1', '0', '1', '0'},
{'1', '1', '0', '0', '0'},
{'0', '0', '0', '0', '0'}
};
System.out.println("测试用例1的岛屿数量: " + numIslands(grid1)); // 预期输出: 1
// 测试用例2
char[][] grid2 = {
{'1', '1', '0', '0', '0'},
{'1', '1', '0', '0', '0'},
{'0', '0', '1', '0', '0'},
{'0', '0', '0', '1', '1'}
};
System.out.println("测试用例2的岛屿数量: " + numIslands(grid2)); // 预期输出: 3
}
}
BFS实现
import java.util.LinkedList;
import java.util.Queue;
/**
* 岛屿数量 - BFS实现
*/
public class NumberOfIslandsBFS {
/**
* 计算岛屿数量
* @param grid 二维字符网格,'1'表示陆地,'0'表示水
* @return 岛屿的数量
*/
public static int numIslands(char[][] grid) {
// 边界条件检查
if (grid == null || grid.length == 0 || grid[0].length == 0) {
return 0;
}
int rows = grid.length;
int cols = grid[0].length;
int count = 0;
// 遍历整个网格
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
// 如果当前单元格是陆地
if (grid[i][j] == '1') {
// 岛屿数量加1
count++;
// 使用BFS将与当前陆地相连的所有陆地标记为已访问
bfs(grid, i, j, rows, cols);
}
}
}
return count;
}
/**
* BFS遍历与当前陆地相连的所有陆地,并标记为已访问
* @param grid 二维字符网格
* @param i 当前行索引
* @param j 当前列索引
* @param rows 网格的行数
* @param cols 网格的列数
*/
private static void bfs(char[][] grid, int i, int j, int rows, int cols) {
// 创建队列,用于BFS
Queue<int[]> queue = new LinkedList<>();
// 将当前陆地加入队列
queue.offer(new int[]{i, j});
// 将当前陆地标记为已访问
grid[i][j] = '0';
// 定义四个方向的偏移量:上、下、左、右
int[][] directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
// BFS遍历
while (!queue.isEmpty()) {
int[] current = queue.poll();
int row = current[0];
int col = current[1];
// 检查四个方向的相邻单元格
for (int[] dir : directions) {
int newRow = row + dir[0];
int newCol = col + dir[1];
// 检查边界条件和是否是陆地
if (newRow >= 0 && newRow < rows && newCol >= 0 && newCol < cols && grid[newRow][newCol] == '1') {
// 将相邻的陆地加入队列
queue.offer(new int[]{newRow, newCol});
// 将相邻的陆地标记为已访问
grid[newRow][newCol] = '0';
}
}
}
}
// 测试代码
public static void main(String[] args) {
// 测试用例1
char[][] grid1 = {
{'1', '1', '1', '1', '0'},
{'1', '1', '0', '1', '0'},
{'1', '1', '0', '0', '0'},
{'0', '0', '0', '0', '0'}
};
System.out.println("测试用例1的岛屿数量: " + numIslands(grid1)); // 预期输出: 1
// 测试用例2
char[][] grid2 = {
{'1', '1', '0', '0', '0'},
{'1', '1', '0', '0', '0'},
{'0', '0', '1', '0', '0'},
{'0', '0', '0', '1', '1'}
};
System.out.println("测试用例2的岛屿数量: " + numIslands(grid2)); // 预期输出: 3
}
}
代码解析
DFS实现
-
主要思路:
- 遍历整个网格,当找到一个陆地时,使用DFS将与之相连的所有陆地标记为已访问
- 每次发现一个新的陆地(未被标记为已访问),就将岛屿计数器加1
-
关键函数:
numIslands:主函数,遍历网格并计数岛屿dfs:递归函数,标记与当前陆地相连的所有陆地为已访问
-
时间复杂度:O(M×N),其中M是网格的行数,N是网格的列数
- 最坏情况下,我们需要访问网格中的每个单元格一次
-
空间复杂度:O(M×N)
- 最坏情况下,整个网格都是陆地,递归调用栈的深度为M×N
BFS实现
-
主要思路:
- 与DFS类似,但使用队列而不是递归来遍历与当前陆地相连的所有陆地
- 每次发现一个新的陆地,就将岛屿计数器加1,并使用BFS标记整个岛屿
-
关键步骤:
- 创建队列,将当前陆地加入队列并标记为已访问
- 不断从队列中取出陆地,检查其四个方向的相邻单元格
- 如果相邻单元格是陆地,则将其加入队列并标记为已访问
-
时间复杂度:O(M×N),与DFS相同
-
空间复杂度:O(min(M,N))
- 最坏情况下,队列中最多存储min(M,N)个节点(当网格是一条对角线时)
DFS与BFS的比较
-
实现复杂度:
- DFS实现更简洁,使用递归
- BFS实现稍复杂,需要使用队列
-
空间复杂度:
- 在最坏情况下,DFS的空间复杂度可能更高(递归调用栈)
- BFS的空间复杂度通常更可控
-
适用场景:
- 如果网格较大且岛屿较小,DFS可能更高效
- 如果需要按照距离顺序访问单元格,BFS更合适
🌟 对Java初期学习的重要意义
学习"岛屿数量"这个问题对Java初学者有着多方面的重要意义:
1. 图论算法的入门
这个问题是图论搜索算法的经典应用,通过它可以理解DFS和BFS这两种基本的图遍历算法。这些算法在计算机科学中有广泛的应用,是算法学习的基础。🧭
2. 递归思想的实践
DFS实现中的递归调用是理解递归思想的绝佳例子。递归是解决许多复杂问题的强大工具,掌握它对于提升编程能力至关重要。🔄
3. 数据结构的应用
BFS实现中使用了队列这一基本数据结构,这有助于理解如何在实际问题中应用数据结构。选择合适的数据结构是解决算法问题的关键。📊
4. 二维数组的操作
这个问题涉及到二维数组的遍历和操作,这是Java编程中的基本技能。熟练掌握二维数组的操作对于解决矩阵、图像处理等问题非常重要。🔢
5. 面试高频题目
"岛屿数量"是技术面试中的常见题目,掌握它不仅能提高你的编程能力,还能增加你在面试中的竞争力。特别是当你能够同时解释DFS和BFS两种解法时,会给面试官留下深刻印象。💼
6. 实际应用场景
这个算法在实际开发中有很多应用场景,如:
- 图像处理中的连通区域标记
- 网络拓扑分析
- 地图中的区域识别
- 游戏开发中的地图生成和路径规划
学习这个算法有助于你在实际工作中解决类似问题。🌐
📝 总结
今天我们一起学习了"岛屿数量"这个经典算法问题。我们不仅了解了它的基本概念和实现方法,还探讨了DFS和BFS两种解法的异同。
通过这个问题,我们学到了以下几点:
-
问题抽象的能力:我们将现实问题(计算岛屿数量)抽象为图论问题(连通分量计数)。
-
DFS和BFS的应用:我们学习了如何使用这两种基本的图遍历算法来解决实际问题。
-
标记已访问节点的技巧:我们通过将访问过的陆地改为水,巧妙地避免了使用额外的访问数组。
-
边界条件的处理:我们学习了如何正确处理网格边界和无效输入。
这个问题虽然看起来简单,但它包含了图论算法的精髓。掌握了这个问题,你就掌握了DFS和BFS的基本应用,为学习更复杂的图算法打下了坚实基础。
希望通过这篇文章,你不仅学会了解决"岛屿数量"问题的方法,更重要的是理解了背后的算法思想。在编程的道路上,这些思想和技巧将会一直伴随着你,帮助你解决各种各样的挑战。💪
记住,算法学习是一个循序渐进的过程,今天的每一步都是为了明天的飞跃做准备。希望你能将今天学到的知识应用到实际问题中,不断提升自己的编程能力!🌈
如果你对这个问题还有任何疑问,或者想了解更多相关的算法和数据结构,欢迎在评论区留言交流!我们下次再见!👋
学习提示:尝试解决"岛屿的最大面积"问题,该问题要求计算网格中最大的岛屿面积。这将帮助你更深入地理解DFS和BFS在网格问题中的应用!
1709

被折叠的 条评论
为什么被折叠?



