title: Number of Islands
slug: number-of-islands
difficulty: medium
leetcode_id: 200
leetcode_url: https://leetcode.com/problems/number-of-islands/
categories:
  - graphs
  - arrays
patterns:
  - dfs
  - bfs
  - matrix-traversal

function_signature: "def num_islands(grid: list[list[str]]) -> int:"

test_cases:
  visible:
    - input: { grid: [["1", "1", "1", "1", "0"], ["1", "1", "0", "1", "0"], ["1", "1", "0", "0", "0"], ["0", "0", "0", "0", "0"]] }
      expected: 1
    - input: { grid: [["1", "1", "0", "0", "0"], ["1", "1", "0", "0", "0"], ["0", "0", "1", "0", "0"], ["0", "0", "0", "1", "1"]] }
      expected: 3
  hidden:
    - input: { grid: [["1"]] }
      expected: 1
    - input: { grid: [["0"]] }
      expected: 0
    - input: { grid: [["1", "0", "1", "0", "1"]] }
      expected: 3
    - input: { grid: [["1", "1"], ["1", "1"]] }
      expected: 1
    - input: { grid: [["1"], ["0"], ["1"], ["0"], ["1"]] }
      expected: 3
    - input: { grid: [["0", "0", "0"], ["0", "0", "0"], ["0", "0", "0"]] }
      expected: 0
    - input: { grid: [["1", "0", "1"], ["0", "1", "0"], ["1", "0", "1"]] }
      expected: 5

description: |
  Given an `m × n` 2D binary grid `grid` which represents a map of `'1'`s (land) and `'0'`s (water), return *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 surrounded by water.

constraints: |
  - `m == grid.length`
  - `n == grid[i].length`
  - `1 <= m, n <= 300`
  - `grid[i][j]` is `'0'` or `'1'`

examples:
  - input: |
      grid = [
        ["1","1","1","1","0"],
        ["1","1","0","1","0"],
        ["1","1","0","0","0"],
        ["0","0","0","0","0"]
      ]
    output: "1"
    explanation: "All land cells are connected horizontally/vertically, forming one island."
  - input: |
      grid = [
        ["1","1","0","0","0"],
        ["1","1","0","0","0"],
        ["0","0","1","0","0"],
        ["0","0","0","1","1"]
      ]
    output: "3"
    explanation: "Three separate groups of connected land cells — three islands."

explanation:
  intuition: |
    Imagine looking at a map from above. Each `'1'` is a piece of land, and you want to count how many distinct landmasses (islands) exist. Two pieces of land belong to the same island if you can walk from one to the other without crossing water (moving only up, down, left, or right — not diagonally).

    Think of it like this: when you step onto a piece of land, you want to "explore" the entire island by visiting all connected land cells. Once you've seen the whole island, you mark it as "visited" so you don't count it again. Then you continue scanning the map for the next unvisited piece of land.

    This is the classic **connected components** problem on a grid. Each island is one connected component of `'1'`s. We count components by:
    1. Finding an unvisited land cell (new island found!)
    2. Exploring all connected land cells (mark the whole island as visited)
    3. Repeat until every cell has been processed

  approach: |
    We solve this using **DFS to Explore and Mark Islands**:

    **Step 1: Iterate through every cell**

    - Scan the grid row by row, column by column
    - We're looking for unvisited `'1'`s — each one represents a new island

    &nbsp;

    **Step 2: When land is found, count it and explore**

    - Increment the island count
    - Use DFS (or BFS) to visit all connected land cells
    - Mark each visited cell by changing `'1'` to `'0'` (this "sinks" the island to avoid recounting)

    &nbsp;

    **Step 3: DFS exploration**

    - From the current cell, recursively explore all four directions (up, down, left, right)
    - Stop when: out of bounds, or cell is water (`'0'`)
    - Mark the cell as visited **before** recursive calls to prevent infinite loops

    &nbsp;

    **Step 4: Return the count**

    - After processing all cells, return the island count

    &nbsp;

    This works because once we've explored an island, all its cells are marked as `'0'`, so we'll never trigger a new exploration from those cells again.

  common_pitfalls:
    - title: Not Marking Cells as Visited
      description: |
        Without marking visited cells, two things go wrong:
        1. You'll count the same island multiple times (each cell triggers a new count)
        2. DFS/BFS will revisit cells infinitely, causing a stack overflow or infinite loop

        The cleanest solution is to modify the grid itself — change `'1'` to `'0'` when visited. Alternatively, use a separate `visited` set, but this uses extra space.
      wrong_approach: "Not modifying grid or using visited set"
      correct_approach: "grid[r][c] = '0' immediately when visiting"

    - title: Including Diagonal Connections
      description: |
        The problem states islands connect **horizontally or vertically** only. Diagonal cells are NOT considered adjacent.

        Check only 4 directions: `(r+1,c), (r-1,c), (r,c+1), (r,c-1)`. Don't include the 4 diagonal directions.
      wrong_approach: "Exploring 8 directions including diagonals"
      correct_approach: "Explore only 4 orthogonal directions"

    - title: Boundary Check Errors
      description: |
        Before accessing `grid[r][c]`, always verify that `r` and `c` are within bounds:
        - `0 <= r < rows`
        - `0 <= c < cols`

        Missing these checks causes index-out-of-bounds errors.
      wrong_approach: "Accessing grid[r][c] without bounds check"
      correct_approach: "Check bounds first: if r < 0 or r >= rows or c < 0 or c >= cols: return"

  key_takeaways:
    - "**Grid = implicit graph**: Each cell is a node; adjacent cells are connected by edges"
    - "**DFS/BFS for connected components**: Classic technique for counting or exploring connected regions"
    - "**In-place marking**: Modifying input to track visited state saves space (when allowed)"
    - "**Foundation for many grid problems**: Flood fill, maze solving, region counting all use this pattern"

  time_complexity: "O(m × n). Each cell is visited at most once by the main loop and at most once by DFS/BFS."
  space_complexity: "O(m × n). In the worst case (all land), the DFS recursion stack or BFS queue can hold all cells."

solutions:
  - approach_name: DFS
    is_optimal: true
    code: |
      def num_islands(grid: list[list[str]]) -> int:
          if not grid:
              return 0

          rows, cols = len(grid), len(grid[0])
          islands = 0

          def dfs(r: int, c: int) -> None:
              # Stop if out of bounds or water
              if r < 0 or r >= rows or c < 0 or c >= cols:
                  return
              if grid[r][c] != '1':
                  return

              # Mark as visited by "sinking" the land
              grid[r][c] = '0'

              # Explore all four directions
              dfs(r + 1, c)  # down
              dfs(r - 1, c)  # up
              dfs(r, c + 1)  # right
              dfs(r, c - 1)  # left

          # Scan every cell in the grid
          for r in range(rows):
              for c in range(cols):
                  if grid[r][c] == '1':
                      # Found new island! Count it and explore
                      islands += 1
                      dfs(r, c)

          return islands
    explanation: |
      **Time Complexity:** O(m × n) — Each cell visited at most twice (once by loop, once by DFS).

      **Space Complexity:** O(m × n) — Recursion stack in worst case (grid is all land in a snake pattern).

      When we find unvisited land, we increment our count and use DFS to "sink" the entire island by marking all connected land as water. This prevents recounting.

  - approach_name: BFS
    is_optimal: true
    code: |
      from collections import deque

      def num_islands(grid: list[list[str]]) -> int:
          if not grid:
              return 0

          rows, cols = len(grid), len(grid[0])
          islands = 0

          def bfs(start_r: int, start_c: int) -> None:
              queue = deque([(start_r, start_c)])
              grid[start_r][start_c] = '0'  # Mark starting cell

              while queue:
                  r, c = queue.popleft()

                  # Explore all four directions
                  for dr, dc in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
                      nr, nc = r + dr, c + dc

                      # Add unvisited land to queue
                      if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == '1':
                          grid[nr][nc] = '0'  # Mark before adding to queue
                          queue.append((nr, nc))

          for r in range(rows):
              for c in range(cols):
                  if grid[r][c] == '1':
                      islands += 1
                      bfs(r, c)

          return islands
    explanation: |
      **Time Complexity:** O(m × n) — Same as DFS.

      **Space Complexity:** O(min(m, n)) — Queue holds at most one "frontier" layer, which is bounded by the smaller dimension.

      BFS explores level by level rather than depth-first. Mark cells as visited **when adding to queue** (not when processing) to avoid adding duplicates.
