DFS and BFS Time and Space complexities of 'Number of islands' on Leetcode
Asked Answered
P

4

26

Here is the question description. The first 2 suggested solutions involve DFS and BFS. This question refers to the 1st two approaches: DFS and BFS.

I have included the problem statement here for easier reading.

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

I am unclear as to why the time complexity for both DFS and BFS is O(rows * columns) for both. I see how this is the case where the grid is just full of 0's - we simply have to check each cell. However, doesn't the DFS approach add more time to the search? Even if we mark the cells we visited by changing them to 0 in the dfs methods, we still would revisit all the cells because of the two outer loops. If dfs could be have time complexity of O(n) in the case of a big grid with large row and column numbers, wouldn't the time complexity be O(rows * columns * max[rows, cols])? Moreover, isn't the same case with the BFS approach where it is O(rows * cols * possibleMaxSizeOfQueue) where possibleMaxSizeOfQueue could again be 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);
        }
      }
    }

How is DFS's space complexity O(rows*cols)? Is it not possible/common to consider the call stack space as freed when a recursion branch returns? How is the space complexity for BFS O(min(rows, cols))? The way I see it, the queue could be full of all elements in the case of a grid with just 1's thereby giving O(rows*cols) for BFS space complexity.

DFS Solution

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;
  }
}

Time complexity : O(M×N) where M is the number of rows and N is the number of columns.

Space complexity : worst case O(M×N) in case that the grid map is filled with lands where DFS goes by M×N deep.

BFS Solution

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;
  }
}

Time complexity : O(M×N) where M is the number of rows and N is the number of columns.

Space complexity : O(min(M,N)) because in worst case where the grid is filled with lands, the size of queue can grow up to min(M,N).

Pawsner answered 17/6, 2018 at 23:17 Comment(3)
Where are you getting the BFS space complexity of O(min(rows, cols))? I would think it's O(rows * cols) because you need to store data for each grid cell. If it's in the linked solution, I can't see it without creating an account.X
I also can't see the solutions themselves so I can't analyze their space/time complexity. Please edit them into your question.X
@pkpnd, I don't know why you can't see them but I edited the question to include the code and time complexities.Pawsner
B
34

DFS' time complexity is proportional to the total number of vertexes and edges of the graph visited. In that case, there are N*M vertexes and slightly less than 4*N*M edges, their sum is still O(N*M).

Why so: because we process each edge exactly once in each direction. Situation where recursive call is immediately terminated does not matter as time spent for that call can be accounted for on the call site; and there is at most once call for each directed edge, hence O(N*M).

BFS' time complexity is quite similar. Maximal length of the queue does not matter at all because at no point we examine it in a whole. Queue only gets "append" and "remove first" queries, which can be processed in constant time regardless of queue's size. If we need to check whether a vertex was already visited, we do so in constant time.

Worst-case space complexity for DFS is Theta(N*M): just take any "snake-wise" maze:

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

Here DFS will be forced to traverse the path in whole before it stops and starts freeing up the stack. However, in no situation there will be more than N*M+1 elements on the stack.

Worst-case space complexity for BFS is indeed not O(max(N, M)): it's Theta(N*M) even when we're considering simple grid. Here is an example from math.stackexchange.com:

enter image description here

If we start BFS in the red point, it will end up with a queue which contains all leafs of the tree, their number is proportional to N*M. One can also truncate 3/4rd of the example and make the red dot appear in the upper-left corner.

Looks like the solution you've read is wrong in respect to BFS' worst case memory consumption.

Barnyard answered 18/6, 2018 at 15:0 Comment(7)
Great answer, that image in particular is just what I was looking for when I realized I didn't have a good answer to the BFS worst-case space complexity.X
Cool, your answer is what I'm looking for.Discography
When you say the TC of DFS is O(N*M), are you talking about the dfs() function (only), or the entire solution? IMO the former.Appetitive
@Appetitive I believe there is no difference. The entire solution consists of multiple calls to dfs() functions which cover whole area exactly once. There may be some additional O(N*M) loop which tries to find starting points for dfs, but it's still O(N*M).Barnyard
The best answer I had ever seen for this question! Thanks you.Marcy
Shouldn't big O notation be used when talking about the worst-case space complexity? My understanding was that big O describes the evolution for the worst-case, and Theta describes the evolution for all casesSather
@Sather Worst-case as in "the algorithm uses less than X bytes" - yes. Worst-case as in "the test case requires the algorithm to use at least X bytes" - no. Big-O is for upper bounds, big-omega is for lower bounds, big-theta is for exact bounds.Barnyard
H
4

@yeputons: I don't think the space complexity for BFS will be proportional to N * M. When you say a queue could have at max all the leaf elements (when starting at center) that actually means 2*(N+M) elements at Max.

And when starting from one of the corners it indeed is O(min(m, n)), because number of elements being added to the queue are constrained.

Hulett answered 9/5, 2020 at 20:26 Comment(2)
There may be O(N*M) leaf elements. Take a look at the picture in my answer: approx. a 1/4 of all points are leafs. If you want to start from a corner, just take a corresponding quarter of the picture, the structure will be preserved.Barnyard
This isn't correct. Imagine drawing an L-shaped path from the top-left corner to the centre. The leaves on the right-hand-side of the graph will be equidistant from the top-left corner in the adjusted graph. There are O(mn) such leaves.Nilgai
I
0

This is a classic example of BFS implementation. Only catch here is that we don't need extra space to mark a node as visited. Existing grid matrix can be reused to identify a visited node. I have made a small video explaining this logic. Please check this out

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

Improbability answered 25/2, 2022 at 4:49 Comment(2)
Thanks for leaving the video link. But your answer only covers the BFS implementation. The author of the original post has requested for both the BFS and the DFS implementations. It would be excellent if you could update your answer to cover the DFS implementation too.Fourchette
While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. - From ReviewInaction
A
0

I think the space complexity of BFS is indeed O(min(M, N)) where M is the row length and N is the column length. Please see the below examples:

  1. When you start traversing a matrix from the corner, the maximum number of cells/nodes you can have in the queue is k where k is the number of cells on a diagonal line in the matrix, which means k = min(M, N).

  2. When you start traversing a matrix from the centre, the maximum number of cells/nodes you can have in the queue is {1, 4, 8, 12, 16, ..., 4i} where i is the i-th layer. And such cells fit in a matrix of min size {1, 4, 9, 16, 25, ..., i*i} respectively. Please see the below scratch: enter image description here We know that i is min(M, N), so yet again we have space complexity of O(4 * min(M, N)) which is O(min(M,N)).

Below is my attempt at arguing against @yeputon's answer:

I think the space complexity of BFS in the answer from @yeputons is not applicable to matrix traversal. The plot shown in that answer is a plot of a binary tree laid out in a matrix, but we are traversing in a ternary tree fashion except for the first step where we branch out to 4 branches. The traversal is more like what's described here in a different answer to the Maths Stack Exchange question quoted by @yeputon (https://math.stackexchange.com/a/2219954/758829). But I feel that this is still not the same, because when traversing a matrix in a BFS way, we go from the starting point outwards only. Traversal in both answers to the Maths Stack Exchange question goes recursively and that means it's not strictly outwards from the starting point.

Please correct me if I'm wrong. Thank you!

Amarillo answered 11/2, 2023 at 17:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.