今天我们来深入探讨LeetCode上经典的"岛屿数量"问题(编号200)。这道题在各大科技公司的面试中出现频率极高,是考察图遍历和搜索算法的绝佳案例。作为一位经历过多次算法面试的老手,我想分享一些实战经验和优化技巧。
岛屿数量问题的核心是:给定一个由'1'(陆地)和'0'(水)组成的二维网格,计算其中岛屿的数量。岛屿的定义是水平或垂直相邻的陆地组成的区域,且被水完全包围。理解这个定义很关键 - 对角线相邻的'1'不算作同一个岛屿。
DFS是解决这类连通性问题最直观的方法。基本思路是:遍历网格,当遇到'1'时,启动DFS将所有相连的'1'标记为已访问(通常直接改为'0'),这样每个DFS启动就对应一个岛屿。
cpp复制class Solution {
public:
int numIslands(vector<vector<char>>& grid) {
if(grid.empty() || grid[0].empty()) return 0;
int count = 0;
for(int i = 0; i < grid.size(); ++i) {
for(int j = 0; j < grid[0].size(); ++j) {
if(grid[i][j] == '1') {
dfs(grid, i, j);
++count;
}
}
}
return count;
}
private:
void dfs(vector<vector<char>>& grid, int i, int j) {
if(i < 0 || j < 0 || i >= grid.size() || j >= grid[0].size() || 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); // 左
}
};
注意:在DFS前检查网格是否为空是个好习惯,可以避免潜在的运行时错误。
使用方向数组可以使代码更简洁,也便于后续修改搜索方向:
cpp复制class Solution {
public:
int numIslands(vector<vector<char>>& grid) {
if(grid.empty() || grid[0].empty()) return 0;
int count = 0;
for(int i = 0; i < grid.size(); ++i) {
for(int j = 0; j < grid[0].size(); ++j) {
if(grid[i][j] == '1') {
dfs(grid, i, j);
++count;
}
}
}
return count;
}
private:
const int dirs[4][2] = {{-1,0}, {1,0}, {0,-1}, {0,1}};
void dfs(vector<vector<char>>& grid, int i, int j) {
if(i < 0 || j < 0 || i >= grid.size() || j >= grid[0].size() || grid[i][j] != '1')
return;
grid[i][j] = '0';
for(const auto& dir : dirs) {
dfs(grid, i + dir[0], j + dir[1]);
}
}
};
BFS同样适用于这个问题,特别适合大规模网格的情况,可以避免递归栈溢出的风险。
cpp复制class Solution {
public:
int numIslands(vector<vector<char>>& grid) {
if(grid.empty() || grid[0].empty()) return 0;
int m = grid.size(), n = grid[0].size();
int count = 0;
for(int i = 0; i < m; ++i) {
for(int j = 0; j < n; ++j) {
if(grid[i][j] == '1') {
++count;
queue<pair<int, int>> q;
q.push({i, j});
grid[i][j] = '0';
while(!q.empty()) {
auto curr = q.front(); q.pop();
for(const auto& dir : dirs) {
int x = curr.first + dir[0];
int y = curr.second + dir[1];
if(x >=0 && x < m && y >=0 && y < n && grid[x][y] == '1') {
grid[x][y] = '0';
q.push({x, y});
}
}
}
}
}
}
return count;
}
private:
const int dirs[4][2] = {{-1,0}, {1,0}, {0,-1}, {0,1}};
};
在实际应用中:
并查集是解决连通性问题的另一利器,特别适合需要动态处理连接的场景。
cpp复制class UnionFind {
public:
UnionFind(vector<vector<char>>& grid) {
count = 0;
int m = grid.size(), n = grid[0].size();
parent.resize(m * n);
rank.resize(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;
}
}
}
int find(int i) {
if(parent[i] != i) {
parent[i] = find(parent[i]); // 路径压缩
}
return parent[i];
}
void unite(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;
}
}
int getCount() const {
return count;
}
private:
vector<int> parent;
vector<int> rank;
int count;
};
class Solution {
public:
int numIslands(vector<vector<char>>& grid) {
if(grid.empty() || grid[0].empty()) return 0;
int m = grid.size(), n = grid[0].size();
UnionFind uf(grid);
int zero_count = 0;
for(int i = 0; i < m; ++i) {
for(int j = 0; j < n; ++j) {
if(grid[i][j] == '1') {
grid[i][j] = '0';
if(i - 1 >= 0 && grid[i-1][j] == '1') {
uf.unite(i * n + j, (i-1) * n + j);
}
if(j - 1 >= 0 && grid[i][j-1] == '1') {
uf.unite(i * n + j, i * n + (j-1));
}
}
}
}
return uf.getCount();
}
};
| 算法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| DFS | O(M×N) | O(M×N) | 中等网格 |
| BFS | O(M×N) | O(min(M,N)) | 大型网格 |
| 并查集 | O(M×N×α(M×N)) | O(M×N) | 动态连接 |
每种变种都可以基于我们讨论的基础解法进行扩展,核心思路仍然是图的遍历和连通性分析。