1. 岛屿数量问题解析
岛屿数量问题是一个经典的图论问题,也是算法面试中的高频考点。题目描述很简单:给定一个由'1'(陆地)和'0'(水)组成的二维网格,计算网格中岛屿的数量。岛屿被水包围,并且通过水平或垂直方向上相邻的陆地连接形成。
1.1 问题理解与建模
这个问题可以抽象为图的连通分量问题。每个'1'代表图中的一个节点,相邻的'1'之间存在边。我们需要计算这个图中连通分量的数量。
举个例子:
code复制输入:
[
['1','1','0','0','0'],
['1','1','0','0','0'],
['0','0','1','0','0'],
['0','0','0','1','1']
]
输出:3
这个网格中有三个岛屿:
- 左上角的四个'1'组成一个岛屿
- 中间的单个'1'是一个岛屿
- 右下角的两个'1'组成第三个岛屿
1.2 解题思路分析
解决这个问题主要有两种思路:
- 深度优先搜索(DFS)
- 广度优先搜索(BFS)
两种方法的核心思想都是:遍历整个网格,当遇到一个'1'时,就将其所有相连的'1'标记为已访问,这样它们就不会被重复计数。
2. DFS解法详解
2.1 算法流程
DFS解法的主要步骤如下:
- 遍历网格中的每一个单元格
- 如果当前单元格是'1',则岛屿数量加1,并启动DFS
- 在DFS过程中,将访问过的'1'标记为'0'(或其他标记)
- 递归访问当前单元格的上下左右四个方向的相邻单元格
2.2 Java实现代码
java复制public int numIslands(char[][] grid) {
if (grid == null || grid.length == 0) {
return 0;
}
int numIslands = 0;
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[i].length; j++) {
if (grid[i][j] == '1') {
numIslands++;
dfs(grid, i, j);
}
}
}
return numIslands;
}
private void dfs(char[][] grid, int i, int j) {
if (i < 0 || i >= grid.length || j < 0 || j >= grid[i].length || grid[i][j] != '1') {
return;
}
grid[i][j] = '0'; // 标记为已访问
dfs(grid, i + 1, j); // 下
dfs(grid, i - 1, j); // 上
dfs(grid, i, j + 1); // 右
dfs(grid, i, j - 1); // 左
}
2.3 复杂度分析
时间复杂度:O(M×N),其中M是网格的行数,N是网格的列数。我们最多访问每个单元格两次(一次在主循环中,一次在DFS中)。
空间复杂度:O(M×N),在最坏情况下(网格全是陆地),DFS的递归深度可能达到M×N。
3. BFS解法详解
3.1 算法流程
BFS解法使用队列来实现:
- 遍历网格中的每一个单元格
- 如果当前单元格是'1',则岛屿数量加1,并启动BFS
- 将当前单元格加入队列,并标记为已访问
- 从队列中取出单元格,检查其四个方向的相邻单元格
- 如果是'1',则加入队列并标记为已访问
- 重复步骤4-5直到队列为空
3.2 Java实现代码
java复制public int numIslands(char[][] grid) {
if (grid == null || grid.length == 0) {
return 0;
}
int numIslands = 0;
int rows = grid.length;
int cols = grid[0].length;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
if (grid[i][j] == '1') {
numIslands++;
grid[i][j] = '0'; // 标记为已访问
Queue<int[]> queue = new LinkedList<>();
queue.add(new int[]{i, j});
while (!queue.isEmpty()) {
int[] cell = queue.poll();
int row = cell[0];
int col = cell[1];
// 检查四个方向
if (row - 1 >= 0 && grid[row-1][col] == '1') {
queue.add(new int[]{row-1, col});
grid[row-1][col] = '0';
}
if (row + 1 < rows && grid[row+1][col] == '1') {
queue.add(new int[]{row+1, col});
grid[row+1][col] = '0';
}
if (col - 1 >= 0 && grid[row][col-1] == '1') {
queue.add(new int[]{row, col-1});
grid[row][col-1] = '0';
}
if (col + 1 < cols && grid[row][col+1] == '1') {
queue.add(new int[]{row, col+1});
grid[row][col+1] = '0';
}
}
}
}
}
return numIslands;
}
3.3 复杂度分析
时间复杂度:O(M×N),与DFS相同,每个单元格最多被处理一次。
空间复杂度:O(min(M,N)),在最坏情况下,队列的大小可以达到网格的宽度或高度的最小值。
4. 算法优化与变种
4.1 并查集解法
并查集(Union-Find)是解决连通性问题的另一种有效方法。我们可以将每个'1'视为一个独立的集合,然后合并相邻的'1'所在的集合。
java复制class UnionFind {
int count;
int[] parent;
int[] rank;
public UnionFind(char[][] grid) {
count = 0;
int m = grid.length;
int n = grid[0].length;
parent = new int[m * n];
rank = new int[m * n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == '1') {
parent[i * n + j] = i * n + j;
count++;
}
rank[i * n + j] = 0;
}
}
}
public int find(int i) {
if (parent[i] != i) parent[i] = find(parent[i]);
return parent[i];
}
public void union(int x, int y) {
int rootx = find(x);
int rooty = find(y);
if (rootx != rooty) {
if (rank[rootx] > rank[rooty]) {
parent[rooty] = rootx;
} else if (rank[rootx] < rank[rooty]) {
parent[rootx] = rooty;
} else {
parent[rooty] = rootx;
rank[rootx] += 1;
}
count--;
}
}
public int getCount() {
return count;
}
}
public int numIslands(char[][] grid) {
if (grid == null || grid.length == 0) {
return 0;
}
int nr = grid.length;
int nc = grid[0].length;
UnionFind uf = new UnionFind(grid);
for (int r = 0; r < nr; r++) {
for (int c = 0; c < nc; c++) {
if (grid[r][c] == '1') {
grid[r][c] = '0';
if (r - 1 >= 0 && grid[r-1][c] == '1') {
uf.union(r * nc + c, (r-1) * nc + c);
}
if (r + 1 < nr && grid[r+1][c] == '1') {
uf.union(r * nc + c, (r+1) * nc + c);
}
if (c - 1 >= 0 && grid[r][c-1] == '1') {
uf.union(r * nc + c, r * nc + c - 1);
}
if (c + 1 < nc && grid[r][c+1] == '1') {
uf.union(r * nc + c, r * nc + c + 1);
}
}
}
}
return uf.getCount();
}
4.2 复杂度分析
并查集解法的时间复杂度可以近似为O(M×N×α(M×N)),其中α是反阿克曼函数,增长极其缓慢,可以认为是常数时间。
空间复杂度:O(M×N),用于存储父节点和秩数组。
5. 常见问题与优化技巧
5.1 边界检查的简化
在DFS/BFS实现中,我们可以通过以下方式简化边界检查:
- 使用辅助函数来检查坐标是否有效
- 在网格周围添加一圈'0'作为哨兵,避免边界检查
5.2 标记方法的优化
除了将'1'改为'0',还可以使用其他标记方法:
- 使用额外的visited数组
- 使用特殊字符如'2'来标记已访问
5.3 性能优化建议
- 对于大型网格,BFS通常比DFS更节省内存,因为递归深度可能很大
- 并行处理:可以将网格分块,分别计算后再合并结果
- 在实际应用中,可以考虑使用位图来压缩存储空间
5.4 常见错误
- 忘记标记已访问的单元格,导致无限循环
- 边界条件处理不当,导致数组越界
- 在BFS实现中,没有在入队时立即标记为已访问,导致重复入队
6. 实际应用场景
岛屿数量问题不仅仅是算法题,它在实际中有很多应用:
- 图像处理中的连通区域分析
- 地理信息系统中的岛屿、湖泊统计
- 游戏开发中的地图生成与分析
- 社交网络中的社群发现
理解这个问题的解法有助于解决更复杂的连通性问题,如:
- 统计岛屿的周长
- 找出最大的岛屿面积
- 计算封闭岛屿数量
- 岛屿的最大距离问题等