Number of Islands
in the wild Connected-component segmentation on an occupancy grid (which clumps of obstacles form one object?), region-counting in image masks, flood-fill in paint tools.
Problem
You're given an m × n grid where each cell is '1' (land) or '0' (water). Two land cells belong to the same island if they're adjacent horizontally or vertically (not diagonally). Land at the grid's edge is bordered by water. Return the number of islands.
Examples
[['1','1','1','1','0'],
['1','1','0','1','0'],
['1','1','0','0','0'],
['0','0','0','0','0']] → 1 (all land is connected)
[['1','1','0','0','0'],
['1','1','0','0','0'],
['0','0','1','0','0'],
['0','0','0','1','1']] → 3
[['0']] → 0
Why this is the canonical "grid as graph" problem
A grid is just a graph in disguise: every cell is a vertex; adjacent cells (4-connectivity) are connected by edges. "Count the islands" = "count the connected components".
Standard recipe for counting connected components:
- Walk every vertex.
- When you hit one that's unvisited and qualifies (=
'1'), increment your counter and flood-fill from it (DFS or BFS), marking every reachable land cell as visited. - Continue. Each flood-fill colors exactly one component.
Each cell is touched a constant number of times → O(m · n). The choice between DFS and BFS is taste — DFS recursion is shorter; BFS uses an explicit queue and avoids stack overflow on huge grids.
This template generalizes to: "find the largest island", "perimeter of an island", "color all enclosed regions" (LeetCode "Surrounded Regions" — same flood-fill, different starting points), and "count distinct islands by shape" (canonicalize each flood-fill's coordinate set).
Hints
Hint 1 — what is the underlying graph?
Stop thinking "2D grid". Think: each land cell is a node; an edge exists between two land cells iff they're vertically or horizontally adjacent. What classical graph question equals "how many islands"?
Hint 2 — flood fill
Once you've spotted a land cell that hasn't been visited, you want to mark all land cells connected to it as one island. The two ways: recursive DFS (4 neighbors), or BFS with a queue. Either is fine.
Hint 3 — mutate the grid as your visited marker
You don't need a separate visited array. Just write '0' over each visited cell. Saves space; simplifies code. (If the caller doesn't want the grid mutated, copy first.)
Solution
def num_islands(grid: list[list[str]]) -> int:
if not grid or not grid[0]:
return 0
m, n = len(grid), len(grid[0])
def dfs(r: int, c: int) -> None:
if r < 0 or r >= m or c < 0 or c >= n or grid[r][c] != "1":
return
grid[r][c] = "0" # mark visited
dfs(r + 1, c); dfs(r - 1, c); dfs(r, c + 1); dfs(r, c - 1)
count = 0
for r in range(m):
for c in range(n):
if grid[r][c] == "1":
count += 1
dfs(r, c)
return count
def deep_copy(g): return [row[:] for row in g]
cases = [
([['1','1','1','1','0'],
['1','1','0','1','0'],
['1','1','0','0','0'],
['0','0','0','0','0']], 1),
([['1','1','0','0','0'],
['1','1','0','0','0'],
['0','0','1','0','0'],
['0','0','0','1','1']], 3),
([['0']], 0),
]
for grid, expected in cases:
got = num_islands(deep_copy(grid))
mark = "✓" if got == expected else "✗"
print(f"{mark} num_islands(...) = {got} (expected {expected})")function numIslands(grid) {
if (!grid.length || !grid[0].length) return 0;
const m = grid.length, n = grid[0].length;
function dfs(r, c) {
if (r < 0 || r >= m || c < 0 || c >= n || grid[r][c] !== '1') return;
grid[r][c] = '0';
dfs(r + 1, c); dfs(r - 1, c); dfs(r, c + 1); dfs(r, c - 1);
}
let count = 0;
for (let r = 0; r < m; r++)
for (let c = 0; c < n; c++)
if (grid[r][c] === '1') { count++; dfs(r, c); }
return count;
}
const deepCopy = g => g.map(row => row.slice());
const cases = [
[[['1','1','1','1','0'],
['1','1','0','1','0'],
['1','1','0','0','0'],
['0','0','0','0','0']], 1],
[[['1','1','0','0','0'],
['1','1','0','0','0'],
['0','0','1','0','0'],
['0','0','0','1','1']], 3],
[[['0']], 0],
];
for (const [grid, expected] of cases) {
const got = numIslands(deepCopy(grid));
console.log(`${got === expected ? "✓" : "✗"} numIslands(...) = ${got} (expected ${expected})`);
}pub fn num_islands(grid: &mut Vec<Vec<char>>) -> i32 {
if grid.is_empty() || grid[0].is_empty() { return 0; }
let (m, n) = (grid.len(), grid[0].len());
// Iterative DFS — avoids Rust's recursion + mut-borrow dance.
fn flood(grid: &mut Vec<Vec<char>>, r: usize, c: usize, m: usize, n: usize) {
let mut stack = vec![(r, c)];
while let Some((r, c)) = stack.pop() {
if grid[r][c] != '1' { continue; }
grid[r][c] = '0';
if r + 1 < m { stack.push((r + 1, c)); }
if r > 0 { stack.push((r - 1, c)); }
if c + 1 < n { stack.push((r, c + 1)); }
if c > 0 { stack.push((r, c - 1)); }
}
}
let mut count = 0;
for r in 0..m {
for c in 0..n {
if grid[r][c] == '1' {
count += 1;
flood(grid, r, c, m, n);
}
}
}
count
}
fn main() {
let mut g1: Vec<Vec<char>> = vec![
"11110".chars().collect(),
"11010".chars().collect(),
"11000".chars().collect(),
"00000".chars().collect(),
];
let mut g2: Vec<Vec<char>> = vec![
"11000".chars().collect(),
"11000".chars().collect(),
"00100".chars().collect(),
"00011".chars().collect(),
];
let mut g3: Vec<Vec<char>> = vec![vec!['0']];
println!("{} num_islands(g1) = {} (expected 1)", if num_islands(&mut g1) == 1 { "✓" } else { "✗" }, "...");
println!("{} num_islands(g2) = {} (expected 3)", if num_islands(&mut g2) == 3 { "✓" } else { "✗" }, "...");
println!("{} num_islands(g3) = {} (expected 0)", if num_islands(&mut g3) == 0 { "✓" } else { "✗" }, "...");
}#include <iostream>
#include <vector>
void dfs(std::vector<std::vector<char>>& grid, int r, int c, int m, int n) {
if (r < 0 || r >= m || c < 0 || c >= n || grid[r][c] != '1') return;
grid[r][c] = '0';
dfs(grid, r + 1, c, m, n);
dfs(grid, r - 1, c, m, n);
dfs(grid, r, c + 1, m, n);
dfs(grid, r, c - 1, m, n);
}
int num_islands(std::vector<std::vector<char>>& grid) {
if (grid.empty() || grid[0].empty()) return 0;
int m = (int)grid.size(), n = (int)grid[0].size(), count = 0;
for (int r = 0; r < m; ++r)
for (int c = 0; c < n; ++c)
if (grid[r][c] == '1') { ++count; dfs(grid, r, c, m, n); }
return count;
}
int main() {
std::vector<std::vector<char>> g1 = {
{'1','1','1','1','0'},
{'1','1','0','1','0'},
{'1','1','0','0','0'},
{'0','0','0','0','0'} };
std::vector<std::vector<char>> g2 = {
{'1','1','0','0','0'},
{'1','1','0','0','0'},
{'0','0','1','0','0'},
{'0','0','0','1','1'} };
std::vector<std::vector<char>> g3 = {{'0'}};
std::cout << (num_islands(g1) == 1 ? "✓" : "✗") << " num_islands(g1) = " << "1\n";
std::cout << (num_islands(g2) == 3 ? "✓" : "✗") << " num_islands(g2) = " << "3\n";
std::cout << (num_islands(g3) == 0 ? "✓" : "✗") << " num_islands(g3) = " << "0\n";
}#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int rows, cols;
static void dfs(char** grid, int r, int c) {
if (r < 0 || r >= rows || c < 0 || c >= cols || grid[r][c] != '1') 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);
}
int num_islands(char** grid, int m, int n) {
rows = m; cols = n;
int count = 0;
for (int r = 0; r < m; r++)
for (int c = 0; c < n; c++)
if (grid[r][c] == '1') { count++; dfs(grid, r, c); }
return count;
}
static char** clone(const char* lines[], int m) {
char** g = malloc(m * sizeof(char*));
for (int i = 0; i < m; i++) g[i] = strdup(lines[i]);
return g;
}
static void freegrid(char** g, int m) { for (int i = 0; i < m; i++) free(g[i]); free(g); }
int main(void) {
const char* l1[] = {"11110","11010","11000","00000"};
const char* l2[] = {"11000","11000","00100","00011"};
const char* l3[] = {"0"};
char** g1 = clone(l1, 4); printf("%s num_islands(g1) = %d (expected 1)\n",
num_islands(g1, 4, 5) == 1 ? "✓" : "✗", 1); freegrid(g1, 4);
char** g2 = clone(l2, 4); printf("%s num_islands(g2) = %d (expected 3)\n",
num_islands(g2, 4, 5) == 3 ? "✓" : "✗", 3); freegrid(g2, 4);
char** g3 = clone(l3, 1); printf("%s num_islands(g3) = %d (expected 0)\n",
num_islands(g3, 1, 1) == 0 ? "✓" : "✗", 0); freegrid(g3, 1);
return 0;
}The Rust version is iterative because nested mutable borrows make recursion painful — a real-world Rust solution would either restructure with unsafe or accept the iterative form, which is cleaner anyway. Everywhere else, recursion is the natural fit.
Complexity
- Time
- O(m · n) — each cell is visited at most twice (once by the outer scan, once by a flood-fill).
- Space
- O(m · n) in the worst case — a snake-shaped giant island fills the recursion stack with every cell. Use an explicit stack/queue or Union-Find to bound this.
- Union-Find variant
- Walk the grid; for each land cell, union with each land neighbor. Final answer = number of distinct roots among land cells. Same big-O, often preferred when the grid is huge or streams in incrementally.
In the wild
- Robot perception. Connected-component labeling on a thresholded image gives "candidate objects". This algorithm is the entire
cv2.connectedComponentspipeline (with 8-connectivity instead of 4). - Occupancy-grid clumping. "How many distinct obstacle clumps are in front of the robot?" → exactly this.
- Map-painting tools (Photoshop's magic-wand selection, Tiled level editor's bucket fill). The flood-fill kernel here is their core algorithm.
- Mine sweepers / flood-fill auto-reveal in puzzle games.
Variations
- Max area of island. Same flood-fill; return the largest area seen, not the count.
- Number of distinct islands. During each flood-fill record the relative-coordinate trace (canonical form); hash to a set of seen shapes; final count = set size.
- Islands on a torus. Wrap-around edges — same code, just modular index arithmetic.
- 8-connectivity (include diagonals). Just add 4 more directions to the neighbor list.