Number of Islands is one of the most frequently asked interview problems โ open the Companies tab and you'll see why. It looks like a grid problem, but it's really a graph traversal problem in disguise.
LeetCode 200. Number of Islands โ
You get a 2D grid where '1' is land and '0' is water. Count the islands โ groups of land connected up, down, left, right. No diagonals.
Input:
grid = [
["1","1","0","0","0"],
["1","1","0","0","0"],
["0","0","1","0","0"],
["0","0","0","1","1"]
]
Output: 3
The Core Idea
The whole algorithm is just this:
- Scan every cell.
- When you find land you haven't visited โ that's a new island. Count it.
- Run BFS (or DFS) from that cell to visit the entire island, so it never gets counted again.
So the number of BFS calls = the number of islands. That's it.
DFS or BFS?
Both are O(m ร n) in time, so correctness-wise either works. The difference is what happens under the hood.
- Recursive DFS โ shortest code, fastest to write. But recursion depth can hit O(m ร n) in the worst case. Imagine the whole grid being one snake-shaped island โ that's a deep call stack, and on large grids it can actually overflow.
- BFS โ uses an explicit queue on the heap, so it's safe. And for a grid, the queue peaks at only O(min(m, n)).
In an interview, recursive DFS is totally fine โ as long as you mention the stack depth risk. That one sentence is the difference between "solved it" and "understands the tradeoffs." I went with BFS.
BFS Solution
class Solution:
def numIslands(self, grid: List[List[str]]) -> int:
if not grid:
return 0
rows, cols = len(grid), len(grid[0])
visited = set()
result = 0
def bfs(r, c):
queue = collections.deque([(r, c)])
visited.add((r, c))
directions = [(-1, 0), (0, 1), (1, 0), (0, -1)]
while queue:
row, col = queue.popleft()
for dr, dc in directions:
newRow, newCol = row + dr, col + dc
if (0 <= newRow < rows and
0 <= newCol < cols and
grid[newRow][newCol] == '1' and
(newRow, newCol) not in visited):
visited.add((newRow, newCol))
queue.append((newRow, newCol))
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1' and (r, c) not in visited:
bfs(r, c)
result += 1
return resultTime: O(m ร n) / Space: O(m ร n) โ every cell is processed at most once, and the visited set holds up to every cell in the worst case.
The Detail That Actually Matters
Mark cells as visited when you add them to the queue, not when you take them out.
visited.add((newRow, newCol))
queue.append((newRow, newCol))If you mark on dequeue instead, the same cell can enter the queue multiple times before it gets processed. The answer still comes out right, but you burn time and memory โ and it's the kind of subtle mistake interviewers watch for.
Follow-ups Worth Knowing
- Can we mutate the input? โ Drop the visited set entirely and mark visited cells in place with
grid[r][c] = '0'. Space for bookkeeping goes to O(1). Asking the interviewer whether mutation is allowed is itself a good signal. - What if the grid is huge? โ This is where the DFS stack depth risk becomes real. BFS or iterative DFS with an explicit stack.
- What about counting the biggest island instead? โ That's LC 695. Max Area of Island โ same skeleton, but BFS returns a size instead of just marking cells.
The skeleton here โ scan, find unvisited land, flood it โ is the same one behind Max Area of Island, Flood Fill, Surrounded Regions, and Rotting Oranges. Learn it once, reuse it everywhere.