Leetcode上“岛屿的数量”问题的DFS和BFS时间和空间复杂度

25

这里是问题描述。前两个建议的解决方案涉及DFS和BFS。这个问题涉及到前两种方法:DFS和BFS。

我已经在这里包含了问题陈述以便更容易阅读。

Given a 2d grid map of '1's (land) and '0's (water), count the number of 
islands. An island is surrounded by water and is formed by connecting adjacent
lands horizontally or vertically. You may assume all four edges of the grid are 
all surrounded by water.

    Example 1:

    Input:
    11110
    11010
    11000
    00000

    Output: 1


    Example 2:

    Input:
    11000
    11000
    00100
    00011

    Output: 3

我不清楚为什么DFS和BFS的时间复杂度都是O(rows * columns)。我知道当网格只包含0时,这种情况就会发生 - 我们只需检查每个单元格即可。但是,DFS方法是否会增加搜索时间呢?即使我们通过在dfs方法中将它们更改为0来标记访问过的单元格,由于两个外部循环,我们仍然会重新访问所有单元格。如果在具有大行数和列数的大网格的情况下,DFS的时间复杂度可以达到O(n),那么时间复杂度不应该是O(rows * columns * max[rows, cols])吗?此外,BFS方法的情况也是一样的,其时间复杂度为O(rows * cols * possibleMaxSizeOfQueue),其中possibleMaxSizeOfQueue可能再次是max[rows,cols]

for (int r = 0; r < nr; ++r) {
      for (int c = 0; c < nc; ++c) {
        if (grid[r][c] == '1') {
          ++num_islands;
          dfs(grid, r, c);
        }
      }
    }

DFS的空间复杂度为O(rows*cols),为什么不能/不常考虑递归分支返回时调用栈空间被释放呢?

BFS的空间复杂度为O(min(rows, cols))。就我所见,在网格只有1的情况下,队列可能会装满所有元素,因此BFS的空间复杂度为O(rows*cols)

DFS解决方案

class Solution {
  void dfs(char[][] grid, int r, int c) {
    int nr = grid.length;
    int nc = grid[0].length;

    if (r < 0 || c < 0 || r >= nr || c >= nc || grid[r][c] == '0') {
      return;
    }

    grid[r][c] = '0';
    dfs(grid, r - 1, c);
    dfs(grid, r + 1, c);
    dfs(grid, r, c - 1);
    dfs(grid, r, c + 1);
  }

  public int numIslands(char[][] grid) {
    if (grid == null || grid.length == 0) {
      return 0;
    }

    int nr = grid.length;
    int nc = grid[0].length;
    int num_islands = 0;
    for (int r = 0; r < nr; ++r) {
      for (int c = 0; c < nc; ++c) {
        if (grid[r][c] == '1') {
          ++num_islands;
          dfs(grid, r, c);
        }
      }
    }

    return num_islands;
  }
}

时间复杂度:O(M×N),其中M是行数,N是列数。

空间复杂度:最坏情况下为O(M×N),即当网格图中全部为陆地时,DFS深度为M×N。

BFS解法

class Solution {
  public int numIslands(char[][] grid) {
    if (grid == null || grid.length == 0) {
      return 0;
    }

    int nr = grid.length;
    int nc = grid[0].length;
    int num_islands = 0;

    for (int r = 0; r < nr; ++r) {
      for (int c = 0; c < nc; ++c) {
        if (grid[r][c] == '1') {
          ++num_islands;
          grid[r][c] = '0'; // mark as visited
          Queue<Integer> neighbors = new LinkedList<>();
          neighbors.add(r * nc + c);
          while (!neighbors.isEmpty()) {
            int id = neighbors.remove();
            int row = id / nc;
            int col = id % nc;
            if (row - 1 >= 0 && grid[row-1][col] == '1') {
              neighbors.add((row-1) * nc + col);
              grid[row-1][col] = '0';
            }
            if (row + 1 < nr && grid[row+1][col] == '1') {
              neighbors.add((row+1) * nc + col);
              grid[row+1][col] = '0';
            }
            if (col - 1 >= 0 && grid[row][col-1] == '1') {
              neighbors.add(row * nc + col-1);
              grid[row][col-1] = '0';
            }
            if (col + 1 < nc && grid[row][col+1] == '1') {
              neighbors.add(row * nc + col+1);
              grid[row][col+1] = '0';
            }
          }
        }
      }
    }

    return num_islands;
  }
}

时间复杂度:O(M×N),其中M是行数,N是列数。

空间复杂度:O(min(M,N)),因为在最坏情况下,即网格被陆地填满时,队列的大小可能增长到min(M,N)。


你从哪里得出BFS空间复杂度为O(min(rows, cols))的结论?我认为它应该是O(rows * cols),因为你需要为每个网格单元存储数据。如果这个结论在链接的解决方案中,我没有账号就无法查看。 - k_ssb
我也看不到解决方案本身,因此无法分析它们的空间/时间复杂度。请将它们编辑到您的问题中。 - k_ssb
@pkpnd,我不知道为什么你看不到它们,但我编辑了问题以包括代码和时间复杂度。 - heretoinfinity
4个回答

33

DFS的时间复杂度与遍历的图的总顶点数和边数成正比。在这种情况下,有N*M个顶点和略少于4*N*M条边,它们的和仍然是O(N*M)

为什么会这样:因为我们每条边都会按照相同方向处理一次。立即终止递归调用的情况并不重要,因为该调用消耗的时间可以计入调用站点;而每个有向边最多只有一次调用,因此时间复杂度为O(N*M)

BFS的时间复杂度非常类似。队列的最大长度根本不重要,因为我们从未整体检查过它。队列只会接收“添加”和“删除第一个”查询,而无论队列的大小如何,这些操作都可以在常数时间内完成。如果需要检查某个顶点是否已经访问过,则可以在常数时间内完成。

DFS的最坏空间复杂度Theta(N*M):只需要取任意一个“蛇形”迷宫:

......
#####.
......
.#####
......

在DFS中,它需要在停止并开始释放堆栈之前完整地遍历路径。然而,在任何情况下,堆栈上的元素不会超过N * M + 1个。

BFS的最坏空间复杂度确实不是O(max(N, M)):即使考虑简单的网格,它也是Theta(N*M)。这里有一个来自math.stackexchange.com的例子:

enter image description here

如果我们从红点开始BFS,它将结束时包含所有叶子节点的队列,它们的数量与N*M成比例。可以截取示例的3/4并使红点出现在左上角。

看起来你所读的解决方案在BFS的最坏情况内存消耗方面是错误的。


1
非常好的回答,特别是那张图片正是我在意识到我没有一个好的答案来解决 BFS 最坏情况空间复杂度时所需要的。 - k_ssb
很棒,你的答案正是我在寻找的。 - Hiep Mai Thanh
1
@某人 我相信没有区别。整个解决方案由多个对“dfs()”函数的调用组成,这些函数恰好覆盖了整个区域一次。可能会有一些额外的“O(N * M)”循环,它尝试找到“dfs”的起始点,但仍然是“O(N * M)”。 - yeputons
1
我曾经见过的这个问题的最佳答案!谢谢你。 - 林鼎棋
@totooooo 最坏情况是指“算法使用的字节数少于X” - 是的。最坏情况是指“测试用例要求算法至少使用X个字节” - 不是。大O表示上界,大Ω表示下界,大Θ表示精确界限。 - undefined
显示剩余2条评论

4

@yeputons:我不认为BFS的空间复杂度与N * M成正比。 当你说队列最多可以拥有所有叶子元素时(从中心开始),实际上最多只有2 *(N + M)个元素。

而且,当从其中一个角落开始时,它确实是O(min(m,n)),因为被添加到队列中的元素数量是受限制的。


可能会有O(N*M)个叶节点。请看我回答中的图片:大约四分之一的点是叶子结点。如果你想从一个角落开始,只需取相应的四分之一图片,结构将被保留。 - yeputons
这不正确。想象一条从左上角到中心的L形路径。在调整后的图中,右侧的叶子将与左上角等距离。有O(mn)个这样的叶子。 - MathematicsStudent1122

0
这是BFS实现的经典示例。唯一需要注意的是,我们不需要额外的空间来标记节点是否已被访问。现有的网格矩阵可以被重复使用来识别已访问的节点。我制作了一个小视频来解释这个逻辑,请查看。

https://www.youtube.com/watch?v=GkG4cQzyFoU


感谢您留下视频链接。但是您的答案只涵盖了BFS实现。原帖的作者请求提供BFS和DFS两种实现方式。如果您能更新您的答案,包括DFS实现,那就太好了。 - abbasdgr8
1
虽然这个链接可能回答了问题,但最好在此处包含答案的基本部分并提供参考链接。如果链接页面更改,仅有链接的答案可能会失效。- 来自审查 - AsadSMalik

0

我认为BFS的空间复杂度确实是O(min(M, N)),其中M是行长度,N是列长度。请看下面的例子:

  1. 当您从角落开始遍历矩阵时,在队列中最多可以有k个单元格/节点,其中k是矩阵中对角线上的单元格数,这意味着k = min(M, N)。

  2. 当您从中心开始遍历矩阵时,在队列中最多可以有{1, 4, 8, 12, 16, ..., 4i}个单元格/节点,其中i是第i层。这些单元格分别适合大小为{1, 4, 9, 16, 25, ..., i*i}的矩阵。请参见下面的草图: enter image description here 我们知道i是min(M, N),因此我们再次具有O(4 * min(M, N))的空间复杂度,即O(min(M,N))。

以下是我反驳@yeputon答案的尝试:

我认为@yeputons答案中BFS的空间复杂度并不适用于矩阵遍历。该答案中显示的图表是一个二叉树在矩阵中展开的结果,但我们按照三叉树的方式遍历,除了第一步分支成4个之外。遍历更像在Maths Stack Exchange问题的另一个答案中所描述的(https://math.stackexchange.com/a/2219954/758829)。但我仍然感觉这不完全相同,因为按照BFS的方式遍历矩阵时,我们只朝着起始点往外走。而Maths Stack Exchange问题的两个答案中的遍历是递归进行的,这意味着严格来说并不是从起点向外遍历。

如果我有错,请指正。谢谢!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接