Skip to content

4.20.1 — Number of Islands

LeetCode 200 · Medium · ★ Blind 75

The problem

Count the islands in a grid of '1' (land) and '0' (water). Cells connect horizontally and vertically, not diagonally.

1 1 0 0 0
1 1 0 0 0
0 0 1 0 0        →  3
0 0 0 1 1

The pattern

Scan every cell. When you meet a piece of land that has not been seen yet, that is a new island — so add one to the count, then flood the entire island so its other cells are never counted again.

The flood is a DFS or BFS from that cell, marking everything it reaches.

The guard "only start from an unvisited cell" is the whole algorithm. It is the same idea as the run-start check in 4.4.9 Longest Consecutive Sequence: do the work only from one agreed member of each group.

The solution

python
class Solution:
    def numIslands(self, grid: List[List[str]]) -> int:
        if not grid:
            return 0
        rows, cols = len(grid), len(grid[0])

        def sink(r: int, c: int):
            if r < 0 or r >= rows or c < 0 or c >= cols:
                return
            if grid[r][c] != '1':
                return                       # water, or already sunk
            grid[r][c] = '0'                 # mark BEFORE recursing
            sink(r + 1, c)
            sink(r - 1, c)
            sink(r, c + 1)
            sink(r, c - 1)

        count = 0
        for r in range(rows):
            for c in range(cols):
                if grid[r][c] == '1':        # a new island
                    count += 1
                    sink(r, c)

        return count
ts
function numIslands(grid: string[][]): number {
  if (!grid.length) return 0;
  const rows = grid.length, cols = grid[0].length;

  function sink(r: number, c: number): void {
    if (r < 0 || r >= rows || c < 0 || c >= cols) return;
    if (grid[r][c] !== '1') return;
    grid[r][c] = '0';
    sink(r + 1, c); sink(r - 1, c); sink(r, c + 1); sink(r, c - 1);
  }

  let count = 0;
  for (let r = 0; r < rows; r++)
    for (let c = 0; c < cols; c++)
      if (grid[r][c] === '1') { count++; sink(r, c); }

  return count;
}

Mark before recursing. Setting grid[r][c] = '0' before the four calls is what stops the search coming straight back to this cell and looping forever.

Never restore it. A cell belongs to exactly one island; putting the '1' back would recount it. This is the point where backtracking habits go wrong — compare 4.18.6 Word Search, where the restore is mandatory because you are finding paths, not regions.

The grid is the visited set. No extra structure needed. If you must not modify the input, keep a separate visited array of the same shape — same algorithm, O(rows × cols) extra space.

The BFS version, and when you need it

DFS recursion depth can reach rows × cols. On a 1000×1000 grid that is a million stack frames, which overflows in Python (default limit around 1000) and in most languages.

BFS has no recursion at all:

python
from collections import deque

def bfs(r, c):
    queue = deque([(r, c)])
    grid[r][c] = '0'                                  # mark on ENQUEUE
    while queue:
        cr, cc = queue.popleft()
        for nr, nc in ((cr+1,cc), (cr-1,cc), (cr,cc+1), (cr,cc-1)):
            if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == '1':
                grid[nr][nc] = '0'                    # mark BEFORE enqueueing
                queue.append((nr, nc))

Marking on enqueue, not on dequeue, is essential. If you mark when you take a cell out, the same cell can be added several times before it is ever processed, and the queue grows far beyond the grid size.

Both are O(rows × cols). Use DFS for brevity, BFS when the grid is large.

Complexity

O(rows \times cols) time — every cell is examined a constant number of times, because once sunk it is never entered again.

Space is O(rows \times cols) in the worst case, for the recursion stack or the queue, when the grid is one giant island.

Where this goes next

Eight problems in this chapter are this flood fill with one thing changed:

problemchange
Max Area of Islandthe flood returns a count instead of nothing — 4.20.3
Surrounded Regionsflood from the border to mark survivors — 4.20.5
Pacific Atlanticflood from two edge sets and intersect — 4.20.4
Number of Distinct Islandsrecord each island's shape as a fingerprint
Number of Closed Islandsflood the border first, then count what remains
Number of Provincesthe same on an adjacency matrix instead of a grid
Flood Fill (LeetCode 733)the same, changing colours

Union-find also solves this: union each land cell with its land neighbours, and count the components. It is O(rows \times cols \times \alpha) and more code. It earns its place only when land cells are added over time and you must report the count after each addition — that is "Number of Islands II", and it is the version where DFS would have to re-scan everything after every addition.

What the interviewer will push on

"Why does counting from an unvisited cell work?" Every island is entered exactly once, from whichever of its cells the scan meets first.

"Do you restore the grid?" No. Regions are not paths.

"What if you cannot modify the input?" A separate visited array.

"What about stack overflow?" BFS, or an explicit stack. Give the million-frame figure.

"What if land is added one cell at a time?" Union-find, because a fresh flood fill after every addition is O(n) each time.

One thing to volunteer: say that the grid is the visited set and that you are deliberately not restoring it. Naming the difference from backtracking shows you know both patterns rather than one habit.

Next: 4.20.2 Clone Graph — a traversal that builds something as it goes.