第十一章:图论part02
99. 岛屿数量 深搜
注意深搜的两种写法,熟练掌握这两种写法 以及 知道区别在哪里,才算掌握的深搜。
www.programmercarl.com/kamacoder/0…
99. 岛屿数量 广搜
注意广搜的两种写法,第一种写法为什么会超时, 如果自己做的录友,题目通过了,也要仔细看第一种写法的超时版本,弄清楚为什么会超时,因为你第一次 幸运 没那么想,第二次可就不一定了。
www.programmercarl.com/kamacoder/0…
在 LeetCode 200: Number of Islands 中,你需要计算一个由 '1'(陆地)和 '0'(水)组成的二维网格中有多少个岛屿。岛屿是由水平或垂直相邻的 '1' 组成,并且岛屿的周围都是水域。
你可以通过深度优先搜索(DFS)或广度优先搜索(BFS)来解决这个问题,两者都是遍历图中连通分量的常用方法。
深度优先搜索 (DFS)
DFS 是一种 递归 或 栈 实现的搜索方式。其特点是每次从当前节点延伸到最深处,然后再回溯。这种搜索方式适合在树或图中遍历所有相连的节点。
DFS 步骤:
- 遍历网格,找到第一个
'1'。 - 当找到
'1'时,将其标记为访问过(可以改为'0'或使用一个辅助的标记数组)。 - 使用递归(或栈)将与该
'1'相邻的所有'1'全部访问,标记为'0'。 - 每找到一个新的
'1',计数器增加 1,表示发现了一个新岛屿。 - 继续遍历,直到网格的所有节点都被访问。
DFS 实现代码:
var numIslands = function(grid) {
if (grid.length === 0) return 0;
let count = 0;
const rows = grid.length;
const cols = grid[0].length;
const dfs = (i, j) => {
if (i < 0 || j < 0 || i >= rows || j >= cols || grid[i][j] === '0') {
return;
}
// 标记为已访问
grid[i][j] = '0';
// 向四个方向扩展
dfs(i - 1, j); // 上
dfs(i + 1, j); // 下
dfs(i, j - 1); // 左
dfs(i, j + 1); // 右
};
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
if (grid[i][j] === '1') {
count++;
dfs(i, j); // 从找到的第一个 '1' 开始扩展
}
}
}
return count;
};
DFS 详解:
- 递归从当前节点延伸至四个方向(上、下、左、右),并标记已经访问过的节点为
'0'。 - 每次进入一个新的连通块(即找到一个新的
'1'),计数器增加,表示新的岛屿。
回溯
在 DFS 模板中,"回溯" 的概念指的是:在递归过程中,处理某个节点并继续深入递归,之后在递归完成时,撤销对当前节点的处理,回到上一步状态,以便在其他路径上继续探索。
来看你提到的岛屿问题 (LeetCode 200: Number of Islands),DFS 模板如下:
function dfs(grid, i, j) {
// 终止条件:超出边界或遇到水
if (i < 0 || j < 0 || i >= grid.length || j >= grid[0].length || grid[i][j] === '0') {
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 模板中,回溯的部分体现为:
- 每次在递归时,标记当前节点为已访问,比如这里将陆地
'1'标记为'0',表示该节点已经被处理。 - 递归调用完后,没有显式的“撤销”操作。这是因为一旦一个岛屿的某个部分被标记为
'0'(表示处理过),就不需要恢复原状态。换句话说,这道题的回溯是隐式的——我们不需要将处理后的'0'恢复为'1',因为岛屿只需要遍历一次,后续不需要再次访问这个节点。
这就是问题中回溯的表现:我们对每个节点处理后不会再重复访问它,因此也无需撤销。回溯操作通常会在我们需要探索所有可能的路径或组合时显式存在,而在岛屿问题中,每个节点只能访问一次,因此回溯的“撤销”操作不需要显式地进行。
举例说明:
例如,考虑下面的矩阵:
1 1 0
1 0 0
0 0 1
DFS 从 (0,0) 开始,将其标记为 '0',然后递归处理 (0,1),再处理 (1,0),这些相邻的 '1' 都会被标记为 '0'。在遍历完第一个岛屿后,我们不需要再撤销之前的修改,继续遍历网格,直到找到新的未访问的岛屿。
广度优先搜索 (BFS)
BFS 是使用 队列 实现的遍历方式,其特点是按层级扩展,先访问当前节点的所有相邻节点,然后再访问这些相邻节点的相邻节点。适合用于寻找最短路径等场景。
BFS 步骤:
- 遍历网格,找到第一个
'1'。 - 当找到
'1'时,将其标记为访问过(可以改为'0'或使用一个辅助的标记数组)。 - 将其放入队列,并不断扩展其四个方向(上、下、左、右),将相邻的
'1'加入队列。 - 每找到一个新的
'1',计数器增加 1,表示发现了一个新岛屿。 - 继续遍历,直到网格的所有节点都被访问。
BFS 实现代码:
var numIslands = function(grid) {
if (grid.length === 0) return 0;
let count = 0;
const rows = grid.length;
const cols = grid[0].length;
const bfs = (i, j) => {
const queue = [];
queue.push([i, j]);
grid[i][j] = '0'; // 标记为已访问
while (queue.length) {
const [x, y] = queue.shift();
// 向四个方向扩展
if (x - 1 >= 0 && grid[x - 1][y] === '1') {
queue.push([x - 1, y]);
grid[x - 1][y] = '0'; // 标记为已访问
}
if (x + 1 < rows && grid[x + 1][y] === '1') {
queue.push([x + 1, y]);
grid[x + 1][y] = '0'; // 标记为已访问
}
if (y - 1 >= 0 && grid[x][y - 1] === '1') {
queue.push([x, y - 1]);
grid[x][y - 1] = '0'; // 标记为已访问
}
if (y + 1 < cols && grid[x][y + 1] === '1') {
queue.push([x, y + 1]);
grid[x][y + 1] = '0'; // 标记为已访问
}
}
};
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
if (grid[i][j] === '1') {
count++;
bfs(i, j); // 从找到的第一个 '1' 开始扩展
}
}
}
return count;
};
BFS 详解:
- 使用队列存储当前节点,然后扩展它的四个方向。
- 每次进入一个新的连通块(即找到一个新的
'1'),将其所有相邻'1'节点标记为已访问。
DFS 与 BFS 的比较
- DFS:
- 递归实现,适合探索最深层的节点。
- 在网格遍历中每次找到一个
'1',立即将其所有相邻节点(上下左右)全部递归访问。 - 使用栈(或隐式的递归栈)实现。
- BFS:
- 队列实现,适合层级遍历所有相邻节点。
- 在网格遍历中每次找到一个
'1',层级地逐步扩展相邻的'1'。 - 更适合用于求最短路径问题。
- 时间复杂度:
- DFS 和 BFS 的时间复杂度都是
O(m * n),其中m是网格的行数,n是列数。
- DFS 和 BFS 的时间复杂度都是
- 空间复杂度:
- DFS 的空间复杂度在最坏情况下为
O(m * n)(递归栈深度)。 - BFS 的空间复杂度也是
O(m * n)(队列存储所有节点)。
- DFS 的空间复杂度在最坏情况下为
100. 岛屿的最大面积
本题就是基础题了,做过上面的题目,本题很快。
www.programmercarl.com/kamacoder/0… 在 LeetCode 第 695 题「岛屿的最大面积」中,问题可以通过深度优先搜索(DFS)或广度优先搜索(BFS)来解决。两种搜索方式的本质是一样的,都在查找连通的陆地区域,计算连通块的面积,找到最大的连通块。下面分别说明 DFS 和 BFS 的实现。
1. 深度优先搜索(DFS)
深度优先搜索通过递归的方式,在找到某个陆地后,优先向四周继续深入搜索,直到到达边界或遇到水。
DFS 代码实现:
/**
* @param {number[][]} grid
* @return {number}
*/
var maxAreaOfIsland = function(grid) {
if (!grid.length) return 0;
let maxArea = 0;
let rows = grid.length;
let cols = grid[0].length;
// 深度优先搜索函数
function dfs(i, j) {
if (i < 0 || j < 0 || i >= rows || j >= cols || grid[i][j] === 0) {
return 0;
}
grid[i][j] = 0; // 标记为已访问
let area = 1;
// 四个方向递归搜索
area += dfs(i - 1, j); // 上
area += dfs(i + 1, j); // 下
area += dfs(i, j - 1); // 左
area += dfs(i, j + 1); // 右
return area;
}
// 遍历网格
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
if (grid[i][j] === 1) { // 如果是陆地
maxArea = Math.max(maxArea, dfs(i, j)); // 更新最大岛屿面积
}
}
}
return maxArea;
};
深搜 DFS 思路:
- 遍历网格,当遇到
1(陆地)时,开始进行深度优先搜索。 - 每次搜索时,将当前的陆地面积累加,并向四个方向递归搜索。
- 将访问过的节点标记为
0,以避免重复访问。 - 记录搜索过程中最大的岛屿面积。
2. 广度优先搜索(BFS)
广度优先搜索通过使用队列,逐层扩展查找每个连通的陆地区域。每次从队列中取出一个节点,访问其四周未访问的陆地,直到搜索完所有的陆地。
BFS 代码实现:
/**
* @param {number[][]} grid
* @return {number}
*/
var maxAreaOfIsland = function(grid) {
if (!grid.length) return 0;
let maxArea = 0;
let rows = grid.length;
let cols = grid[0].length;
// 广度优先搜索函数
function bfs(i, j) {
let queue = [[i, j]];
grid[i][j] = 0; // 标记为已访问
let area = 0;
// 方向数组,上下左右
const directions = [[-1, 0], [1, 0], [0, -1], [0, 1]];
while (queue.length) {
const [x, y] = queue.shift();
area++;
// 遍历四个方向
for (let [dx, dy] of directions) {
const newX = x + dx;
const newY = y + dy;
// 如果新坐标在网格内,且为陆地
if (newX >= 0 && newX < rows && newY >= 0 && newY < cols && grid[newX][newY] === 1) {
queue.push([newX, newY]);
grid[newX][newY] = 0; // 标记为已访问
}
}
}
return area;
}
// 遍历网格
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
if (grid[i][j] === 1) { // 如果是陆地
maxArea = Math.max(maxArea, bfs(i, j)); // 更新最大岛屿面积
}
}
}
return maxArea;
};
广搜 BFS 思路:
- 每次遇到
1(陆地)时,开始进行广度优先搜索。 - 将当前节点入队列,并依次访问四周未访问的陆地。
- 使用队列逐层扩展,直到没有可以访问的陆地为止。
- 记录搜索过程中最大的岛屿面积。
深搜(DFS)与广搜(BFS)区别:
- DFS:通过递归函数向四周扩展,深入到某个方向的尽头后再回溯。这种方式适合用递归实现,栈的深度取决于岛屿的形状。
- BFS:使用队列,一层一层向四周扩展。这种方式适合用迭代实现,适合逐层遍历,队列的长度取决于扩展过程中某一层的节点数量。
选择 DFS 或 BFS 取决于具体需求,通常两者性能相近,只是实现方式和递归栈/队列处理不同。