Algorithm

Number of Islands

Depth-First Search (DFS) Pattern

Number of Islands

Given an m x n grid where each cell holds '1' for land or '0' for water, return the number of islands.

An island is a maximal group of land cells connected to each other horizontally or vertically — diagonal touching does not join two cells. A single isolated land cell counts as an island of its own. You may assume the grid is surrounded by water on all four sides, so no island wraps around an edge.

CONSTRAINTS
  • m == grid.length, n == grid[i].length
  • 1 <= m, n <= 300
  • grid[i][j] is the character '0' or '1'
  • Cells touching only at a corner are not connected.
  • The grid always has at least one cell, though it may be entirely water.
EXAMPLE 1
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 2×2 block in the top-left is one island. The lone cell at (2,2) is a second. The horizontal pair at (3,3) and (3,4) is a third. Note (2,2) and (3,3) touch only at a corner, which does not join them.
EXAMPLE 2
Input: grid = [['1','0'],['0','1']]
Output: 2
The two land cells sit diagonally from one another. Since only horizontal and vertical adjacency connects cells, neither can reach the other, and each is its own island. This is the case that pins down what "connected" means.
EXAMPLE 3
Input: grid = [['1','1','1','1','0'],['1','1','0','1','0'],['1','1','0','0','0'],['0','0','0','0','0']]
Output: 1
Every land cell here reaches every other by some horizontal and vertical route, even though the shape is irregular and wraps around the water at (1,2) and (2,2). One connected group means one island, whatever its shape or size.
EXAMPLE 4
Input: grid = [['0','0'],['0','0']]
Output: 0
No land exists at all, so there is nothing to group and the count is 0.
Do diagonally touching land cells belong to the same island?
No — only horizontal and vertical neighbours count. This changes answers substantially, so confirm it. A checkerboard of land cells is many islands under this rule and a single island under an 8-directional one.
Is the grid made of characters or integers?
Characters, '0' and '1', not the numbers 0 and 1. It is a small thing but comparing against an integer silently matches nothing and returns 0 for every grid.
Am I allowed to modify the grid while counting?
Usually yes, and it lets you record visited cells without allocating anything. Ask first — if the caller needs the grid intact afterwards you must keep a separate visited structure, which costs O(m × n) memory.
Can the grid be entirely land, and how large can a single island get?
Yes, and then the answer is 1 with an island of up to 90,000 cells. That size matters: a recursive traversal could nest 90,000 calls deep and overflow the call stack, so an iterative version may be necessary.

The word carrying all the weight here is connected. The grid does not tell you where the islands are; it tells you where the land is, and an island is whatever land happens to hang together. So the real question is how to group cells by connectivity, and then simply count the groups.

That structure — a graph split into separate clumps with no links between them — is called a set of connected components, and counting them is one of the most reusable tasks in all of graph work. The grid framing hides it, so it is worth naming: each land cell is a node, each pair of orthogonally adjacent land cells is an edge, and an island is exactly one connected component.

The approach that almost works

A first instinct is to give every land cell its own label, then repeatedly sweep the grid merging labels wherever two labelled cells touch, until a sweep changes nothing.

python
label = {}
next_id = 0
for r, c in all_cells:
    if grid[r][c] == '1':
        label[(r, c)] = next_id      # everyone starts in their own group
        next_id += 1

changed = True
while changed:                        # keep sweeping until labels settle
    changed = False
    for r, c in land_cells:
        for nr, nc in neighbours(r, c):
            if is_land(nr, nc) and label[(nr, nc)] != label[(r, c)]:
                # merge the two groups... and now what?
                changed = True

It is correct in spirit and painful in practice. Merging two labels means finding and rewriting every cell carrying the old label, and a long snaking island can require a fresh sweep for each link in the chain — so the sweeps multiply and the cost climbs toward O((m × n)²). Worse, the bookkeeping is genuinely fiddly, and fiddly bookkeeping under interview pressure is how bugs happen.

The waste is worth naming precisely: we are discovering connectivity in fragments and then repairing our earlier guesses. Each merge undoes a decision made before we had enough information.

The reframe: don't merge groups, consume them

Here is the shift. Rather than labelling everything and reconciling afterwards, walk the grid looking for land you have not seen before. The first time you touch a cell of some island, that island is brand new — so add one to the counter. Then, before continuing the scan, walk that entire island and mark every cell of it as seen, so that when the outer scan later passes over its other cells, they are no longer land as far as the scan is concerned.

Two loops, doing two completely different jobs:

- The outer scan exists only to find a starting point for an island not yet discovered. It increments the counter exactly once per island, at the moment of first contact.
- The inner traversal exists only to consume one island completely, so it can never be counted a second time.

No merging, no relabelling, no repeated sweeps. Each island is discovered once and erased once.

Recursion, from the ground up

The inner traversal is naturally recursive, so it is worth being precise about what that means rather than waving at "a chain reaction".

A recursive function is one that solves a problem by handling a small piece itself and handing the rest to another copy of itself. Two things must be true or it never finishes:

- A base case — a situation handled immediately, with no further calls. Here: if the position is off the grid, or the cell is water, there is nothing to do, so return.
- Progress toward the base case — each call must make the remaining problem strictly smaller. Here: every call converts one land cell to water before recursing, so the amount of land left strictly decreases every time. It cannot decrease forever, so the recursion must stop.

Now the part people find uncomfortable. When sink(r, c) calls sink(r+1, c), do not attempt to trace where that call goes. Assume it does its job — it will sink everything reachable from below and return. This is not hand-waving; it is an induction argument. The base case is correct by inspection, and if every call on a smaller amount of land is correct, then a call that sinks its own cell and delegates the four directions is correct too. So every call is correct.

Mechanically, each in-progress call gets a stack frame holding its local values and the point to resume at. The frames pile up while the traversal descends and unwind as calls return. That pile is why a 90,000-cell island can exhaust the interpreter's stack — the recursion is 90,000 frames deep at its worst.

python
def num_islands(grid):
    if not grid or not grid[0]:
        return 0
    rows, cols = len(grid), len(grid[0])

    def sink(r, c):
        if r < 0 or r >= rows or c < 0 or c >= cols or grid[r][c] != '1':
            return                       # base case: off-grid or not land
        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':        # untouched land: a new island
                count += 1
                sink(r, c)
    return count
Crucial Notethe cell is set to '0' before the four recursive calls, never after. Reverse the two and the algorithm hangs forever. Cell A calls its neighbour B, B is still marked as land so it calls back into A, A is still land so it calls B again, and the two bounce until the stack overflows. Marking first is what makes the recursion's progress guarantee real — the moment a cell is entered it stops being land, so nothing can re-enter it.
Why the nested loops are not quadratic

The code has a doubly nested loop with a traversal inside it, which looks expensive, but it is linear and the reason is worth being able to state.

Count how often each cell is touched. The outer scan visits every cell exactly once. The inner traversal only ever enters a cell that is currently land, and it immediately converts it to water, so no cell can be entered by a traversal more than once. Each cell therefore costs a constant amount of work overall, and the total is O(m × n) time.

Space is the recursion depth: O(m × n) in the worst case, a single island snaking through every cell.

Worked example

Grid:

text
1 1 0
1 0 1
0 1 1

The scan proceeds left to right, top to bottom.

- (0,0) is land. New island, so the count becomes 1. Sinking starts. (0,0) becomes water, then recurses down to (1,0) which sinks and finds only water and edges below and left; back up, (0,0) recurses right to (0,1), which sinks and finds (0,2) is water and (1,1) is water. The island {(0,0), (0,1), (1,0)} is gone.
- (0,1) and (0,2) — the scan reaches them and both read as water now. (0,1) was sunk a moment ago; this is exactly the mechanism preventing a second count of the same island.
- (1,0) is water (sunk). (1,1) is water originally.
- (1,2) is land. New island, count becomes 2. Sinking: (1,2) goes, then down to (2,2), which goes, then left to (2,1), which goes and finds (2,0) is water. The island {(1,2), (2,2), (2,1)} is gone.
- Row 2 is now entirely water to the scan.

Answer 2. Notice (1,0) and (2,1) look close on the page but are diagonal to each other, and no route of horizontal and vertical steps joins them — which is why this is two islands and not one.

The pattern, extracted

Strip away the islands and a shape remains that recurs constantly: scan for an unvisited node, count it, then consume its whole component. Filling it in for a new problem means answering three questions.

- What marks a cell as visited? Overwriting the grid is cheapest; a separate set of coordinates works when the input must survive.
- What counts as a neighbour? Four directions here, eight in some variants, and in non-grid problems whatever the adjacency rule says.
- What do you record per component? Here, just the fact that one existed. Record its size instead and you have Max Area of Island; record which cells it touched and you have Surrounded Regions.

Recognition signals: the words how many groups, how many regions, number of connected components, number of provinces, count the clusters. Any question about how many separate clumps exist is this pattern, whether the input is a grid, a friendship matrix, or a list of edges.

Depth-first search is the natural fit because the question asks nothing about distance. Breadth-first search with a queue would visit exactly the same cells and give exactly the same count — the choice only matters when the answer depends on how far apart things are. DFS is shorter to write; BFS avoids the deep recursion, which on a 300 × 300 grid is a genuine concern rather than a theoretical one.

There is a third route worth naming since interviewers ask for it: union-find, a structure that keeps a representative for each group and merges two groups in near-constant time. It rescues the failed first approach by making merging cheap instead of catastrophic, and it is the right tool when land is added over time and the count must be reported after each addition — a traversal from scratch on every update would be far too slow.

Train the reflex: whenever a question asks how many separate groups exist, do not try to merge things as you go. Find one unvisited member, claim its entire group, and count how many times you had to start over.

Interactive Strategy Visualization
Visual Logic
DFS Flood Fill
Islands Found
0
1
1
0
0
1
0
0
1
0
0
1
1
0
0
0
0

Initial Grid: 1 = Land, 0 = Water.

STEP 1/11
O((M × N)²) Connectivity Test Per Cell
O(M × N) Scan Plus Component Traversal
O(M × N × α) Union-Find