200. Number of Islands
Problem Description
You are given a 2D grid of size m x n
where each cell contains either '1'
(representing land) or '0'
(representing water). Your task is to count the total number of islands in this grid.
An island is defined as a group of adjacent land cells ('1'
s) that are connected horizontally or vertically. Diagonal connections don't count - only cells that share an edge are considered connected. Each island is completely surrounded by water ('0'
s).
The entire grid is surrounded by water on all four edges (you can assume there's water beyond the grid boundaries).
For example, if you have a grid like:
1 1 0 0 0 1 1 0 0 0 0 0 1 0 0 0 0 0 1 1
This grid contains 3 islands:
- Island 1: The connected
'1'
s in the top-left corner (2x2 block) - Island 2: The single
'1'
in the middle - Island 3: The two connected
'1'
s in the bottom-right
The solution uses Depth-First Search (DFS) to traverse each island. When a '1'
is found, it triggers a DFS that marks all connected land cells as visited (by changing them to '0'
), effectively "sinking" the entire island. Each time a new unvisited '1'
is found in the main loop, it represents a new island, so the counter is incremented. The dirs
array (-1, 0, 1, 0, -1)
is used with pairwise
to generate the four directional movements: up (-1, 0)
, right (0, 1)
, down (1, 0)
, and left (0, -1)
.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The 2D grid can be viewed as a graph where each cell is a node, and adjacent cells (horizontally and vertically) are connected by edges. We need to find connected components of
'1'
s.
Is it a tree?
- No: The problem is not about a tree structure. We have a general graph with potential cycles (multiple land cells can form various shapes).
Is the problem related to directed acyclic graphs?
- No: The connections between adjacent cells are undirected (if cell A is adjacent to cell B, then B is also adjacent to A).
Is the problem related to shortest paths?
- No: We're not finding shortest paths between nodes. We're counting the number of distinct connected components (islands).
Does the problem involve connectivity?
- Yes: The core of the problem is identifying connected components - groups of
'1'
s that are connected horizontally or vertically.
Does the problem have small constraints?
- Yes: While the grid can be up to 300x300, we can afford to visit each cell, and the DFS approach is efficient enough for these constraints.
DFS/backtracking?
- Yes: DFS is perfect for exploring all cells in a connected component (island) and marking them as visited.
Conclusion: The flowchart suggests using DFS to solve this connectivity problem. DFS allows us to explore each island completely, marking all connected land cells as visited before moving on to find the next unvisited island.
Intuition
The key insight is to think of this problem as finding connected components in a graph. Each land cell ('1'
) is like a node, and two land cells are connected if they're adjacent horizontally or vertically.
When we encounter an unvisited land cell, we've discovered a new island. But how big is this island? We need to explore all land cells connected to it. This is where DFS comes in naturally - starting from one land cell, we want to visit all reachable land cells that form the same island.
The clever part is how we handle "visited" cells. Instead of maintaining a separate visited array, we can modify the grid itself - changing '1'
to '0'
as we visit each cell. This "sinks" the island as we explore it, preventing us from counting the same island multiple times.
The algorithm flow becomes simple:
- Scan through the entire grid
- When we find a
'1'
, we've found a new island - increment our island counter - Use DFS to explore and "sink" the entire island by turning all connected
'1'
s into'0'
s - Continue scanning - any
'1'
we find now must belong to a different island
The beauty of this approach is that each cell is visited at most twice - once during the main grid scan, and potentially once more during a DFS traversal. The DFS naturally handles islands of any shape or size, following the connections until all parts of an island are explored.
The dirs
array pattern (-1, 0, 1, 0, -1)
paired with pairwise
is an elegant way to generate the four directions: consecutive pairs give us (-1, 0)
for up, (0, 1)
for right, (1, 0)
for down, and (0, -1)
for left.
Solution Approach
The implementation uses DFS to traverse and mark connected land cells. Let's walk through the solution step by step:
Main Algorithm Structure:
-
Initialize counters and dimensions:
ans = 0
to count islandsdirs = (-1, 0, 1, 0, -1)
for direction vectorsm, n = len(grid), len(grid[0])
to get grid dimensions
-
DFS Helper Function:
def dfs(i, j): grid[i][j] = '0' # Mark current cell as visited (sink it) for a, b in pairwise(dirs): x, y = i + a, j + b if 0 <= x < m and 0 <= y < n and grid[x][y] == '1': dfs(x, y)
The DFS function:
- Takes coordinates
(i, j)
of a land cell - Immediately marks it as water
'0'
to prevent revisiting - Uses
pairwise(dirs)
to generate four direction pairs:(-1, 0)
,(0, 1)
,(1, 0)
,(0, -1)
- For each direction, calculates new coordinates
(x, y)
- Checks if the new position is:
- Within bounds:
0 <= x < m and 0 <= y < n
- Is land:
grid[x][y] == '1'
- Within bounds:
- Recursively calls DFS on valid land neighbors
- Takes coordinates
-
Main Loop:
for i in range(m): for j in range(n): if grid[i][j] == '1': dfs(i, j) ans += 1
- Iterates through every cell in the grid
- When a
'1'
is found (unvisited land):- Calls
dfs(i, j)
to explore and sink the entire island - Increments the island counter
- Calls
- Since DFS converts all connected
'1'
s to'0'
s, each island is counted exactly once
Data Structures Used:
- 2D Grid: Modified in-place to track visited cells
- Recursion Stack: Implicit stack used by DFS for traversal
- Direction Vector: Compact representation of four directions using
pairwise
Time Complexity: O(m Γ n)
where m and n are grid dimensions. Each cell is visited at most once.
Space Complexity: O(m Γ n)
in worst case for the recursion stack (when the entire grid is one island).
The elegance of this solution lies in its simplicity - by modifying the grid in-place and using DFS to explore each island completely, we avoid the need for additional visited tracking structures while ensuring each island is counted exactly once.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through a small example to illustrate how the DFS solution works:
Given Grid:
1 1 0 0 1 0 1 0 1
Step-by-step Execution:
Initial State:
ans = 0
(island counter)- Start scanning from position (0,0)
Step 1: Found '1' at (0,0)
- This is unvisited land, so we found a new island!
- Call
dfs(0, 0)
to explore this island - DFS exploration:
-
Mark (0,0) as '0' β Grid becomes:
0 1 0 0 1 0 1 0 1
-
Check 4 directions from (0,0):
- Up: out of bounds
- Right: (0,1) is '1' β Call
dfs(0, 1)
- Down: (1,0) is '0' β Skip
- Left: out of bounds
-
From (0,1), mark it as '0' β Grid becomes:
0 0 0 0 1 0 1 0 1
-
Check 4 directions from (0,1):
- Up: out of bounds
- Right: (0,2) is '0' β Skip
- Down: (1,1) is '1' β Call
dfs(1, 1)
- Left: (0,0) is '0' β Skip
-
From (1,1), mark it as '0' β Grid becomes:
0 0 0 0 0 0 1 0 1
-
Check 4 directions from (1,1):
- All neighbors are either '0' or out of bounds
-
DFS completes, returning to main loop
-
- Increment
ans = 1
Step 2: Continue scanning
- (0,1) is '0' β Skip
- (0,2) is '0' β Skip
- (1,0) is '0' β Skip
- (1,1) is '0' β Skip
- (1,2) is '0' β Skip
Step 3: Found '1' at (2,0)
- This is unvisited land, new island found!
- Call
dfs(2, 0)
- Mark (2,0) as '0' β Grid becomes:
0 0 0 0 0 0 0 0 1
- Check 4 directions: all are '0' or out of bounds
- Increment
ans = 2
Step 4: Continue scanning
- (2,1) is '0' β Skip
Step 5: Found '1' at (2,2)
- This is unvisited land, new island found!
- Call
dfs(2, 2)
- Mark (2,2) as '0' β Grid becomes:
0 0 0 0 0 0 0 0 0
- Check 4 directions: all are '0' or out of bounds
- Increment
ans = 3
Final Result: ans = 3
islands
The key insight: Each time we find a '1' in the main loop, we've discovered a new island. The DFS ensures we explore and "sink" the entire island before continuing, preventing double-counting.
Solution Implementation
1class Solution:
2 def numIslands(self, grid: List[List[str]]) -> int:
3 """
4 Count the number of islands in a 2D grid.
5 An island is formed by connecting adjacent lands ('1') horizontally or vertically.
6
7 Args:
8 grid: 2D grid of '1's (land) and '0's (water)
9
10 Returns:
11 Number of islands in the grid
12 """
13
14 def dfs(row: int, col: int) -> None:
15 """
16 Depth-first search to mark all connected land cells as visited.
17 Modifies the grid in-place by changing '1' to '0'.
18
19 Args:
20 row: Current row index
21 col: Current column index
22 """
23 # Mark current cell as visited (water)
24 grid[row][col] = '0'
25
26 # Check all 4 adjacent directions (up, right, down, left)
27 for dr, dc in [(-1, 0), (0, 1), (1, 0), (0, -1)]:
28 next_row, next_col = row + dr, col + dc
29
30 # If the adjacent cell is within bounds and is unvisited land
31 if (0 <= next_row < rows and
32 0 <= next_col < cols and
33 grid[next_row][next_col] == '1'):
34 # Recursively explore the adjacent land
35 dfs(next_row, next_col)
36
37 # Initialize island counter
38 island_count = 0
39
40 # Get grid dimensions
41 rows, cols = len(grid), len(grid[0])
42
43 # Traverse each cell in the grid
44 for i in range(rows):
45 for j in range(cols):
46 # If we find unvisited land, it's a new island
47 if grid[i][j] == '1':
48 # Explore and mark the entire island
49 dfs(i, j)
50 # Increment island count
51 island_count += 1
52
53 return island_count
54
1class Solution {
2 // Instance variables to store grid and its dimensions
3 private char[][] grid;
4 private int rows;
5 private int cols;
6
7 /**
8 * Counts the number of islands in a 2D grid.
9 * An island is surrounded by water and is formed by connecting adjacent lands horizontally or vertically.
10 *
11 * @param grid 2D grid map of '1's (land) and '0's (water)
12 * @return the number of islands
13 */
14 public int numIslands(char[][] grid) {
15 // Initialize grid dimensions
16 this.rows = grid.length;
17 this.cols = grid[0].length;
18 this.grid = grid;
19
20 int islandCount = 0;
21
22 // Traverse each cell in the grid
23 for (int row = 0; row < rows; row++) {
24 for (int col = 0; col < cols; col++) {
25 // If current cell is land ('1'), start DFS to mark entire island
26 if (grid[row][col] == '1') {
27 dfs(row, col);
28 islandCount++;
29 }
30 }
31 }
32
33 return islandCount;
34 }
35
36 /**
37 * Performs depth-first search to mark all connected land cells as visited.
38 * Marks visited land cells by changing them from '1' to '0'.
39 *
40 * @param row current row index
41 * @param col current column index
42 */
43 private void dfs(int row, int col) {
44 // Mark current land cell as visited by setting it to water ('0')
45 grid[row][col] = '0';
46
47 // Direction vectors for exploring 4 adjacent cells (up, right, down, left)
48 // Using pairs: (-1,0), (0,1), (1,0), (0,-1)
49 int[] directions = {-1, 0, 1, 0, -1};
50
51 // Explore all 4 adjacent cells
52 for (int i = 0; i < 4; i++) {
53 int newRow = row + directions[i];
54 int newCol = col + directions[i + 1];
55
56 // Check if adjacent cell is within bounds and is unvisited land
57 if (newRow >= 0 && newRow < rows &&
58 newCol >= 0 && newCol < cols &&
59 grid[newRow][newCol] == '1') {
60 // Recursively explore the adjacent land cell
61 dfs(newRow, newCol);
62 }
63 }
64 }
65}
66
1class Solution {
2public:
3 int numIslands(vector<vector<char>>& grid) {
4 // Get grid dimensions
5 int rows = grid.size();
6 int cols = grid[0].size();
7 int islandCount = 0;
8
9 // Direction vectors for exploring 4 adjacent cells (up, right, down, left)
10 // Using pairs: (-1,0), (0,1), (1,0), (0,-1)
11 int directions[5] = {-1, 0, 1, 0, -1};
12
13 // DFS function to mark all connected land cells as visited
14 function<void(int, int)> dfs = [&](int row, int col) {
15 // Mark current cell as visited by changing '1' to '0'
16 grid[row][col] = '0';
17
18 // Explore all 4 adjacent directions
19 for (int k = 0; k < 4; ++k) {
20 int nextRow = row + directions[k];
21 int nextCol = col + directions[k + 1];
22
23 // Check if the adjacent cell is within bounds and is unvisited land
24 if (nextRow >= 0 && nextRow < rows &&
25 nextCol >= 0 && nextCol < cols &&
26 grid[nextRow][nextCol] == '1') {
27 // Recursively visit the adjacent land cell
28 dfs(nextRow, nextCol);
29 }
30 }
31 };
32
33 // Traverse the entire grid
34 for (int i = 0; i < rows; ++i) {
35 for (int j = 0; j < cols; ++j) {
36 // If we find an unvisited land cell, it's a new island
37 if (grid[i][j] == '1') {
38 // Use DFS to mark all connected land cells
39 dfs(i, j);
40 // Increment island count
41 ++islandCount;
42 }
43 }
44 }
45
46 return islandCount;
47 }
48};
49
1/**
2 * Counts the number of islands in a 2D grid
3 * An island is surrounded by water and is formed by connecting adjacent lands horizontally or vertically
4 * @param grid - 2D array where '1' represents land and '0' represents water
5 * @returns The number of islands
6 */
7function numIslands(grid: string[][]): number {
8 const rowCount: number = grid.length;
9 const columnCount: number = grid[0].length;
10 let islandCount: number = 0;
11
12 /**
13 * Depth-first search to mark all connected land cells as visited
14 * @param row - Current row index
15 * @param column - Current column index
16 */
17 const markIslandVisited = (row: number, column: number): void => {
18 // Check if current cell is out of bounds or not a land cell
19 if (grid[row]?.[column] !== '1') {
20 return;
21 }
22
23 // Mark current land cell as visited by changing it to water
24 grid[row][column] = '0';
25
26 // Recursively visit all four adjacent cells (up, down, left, right)
27 markIslandVisited(row + 1, column); // Check cell below
28 markIslandVisited(row - 1, column); // Check cell above
29 markIslandVisited(row, column + 1); // Check cell to the right
30 markIslandVisited(row, column - 1); // Check cell to the left
31 };
32
33 // Iterate through each cell in the grid
34 for (let row: number = 0; row < rowCount; row++) {
35 for (let column: number = 0; column < columnCount; column++) {
36 // If we find an unvisited land cell, it's a new island
37 if (grid[row][column] === '1') {
38 // Mark all connected land cells as visited
39 markIslandVisited(row, column);
40 // Increment the island counter
41 islandCount++;
42 }
43 }
44 }
45
46 return islandCount;
47}
48
Time and Space Complexity
Time Complexity: O(m * n)
where m
is the number of rows and n
is the number of columns in the grid.
- The outer loops iterate through every cell in the grid, which takes
O(m * n)
time. - For each cell that contains '1', we perform a DFS traversal. In the worst case, if the entire grid is filled with '1's forming one large island, the DFS will visit every cell once.
- Each cell is visited at most twice: once by the outer loop iteration and potentially once during a DFS traversal from a neighboring cell.
- The
pairwise
function withdirs = (-1, 0, 1, 0, -1)
generates 4 direction pairs:[(-1, 0), (0, 1), (1, 0), (0, -1)]
, representing up, right, down, and left movements. This operation isO(1)
for each cell. - Overall, every cell is processed a constant number of times, resulting in
O(m * n)
time complexity.
Space Complexity: O(m * n)
in the worst case.
- The space complexity is dominated by the recursive call stack during DFS traversal.
- In the worst case, if the entire grid forms a single island with all cells containing '1', the DFS recursion depth could reach
O(m * n)
for a snake-like pattern that visits every cell sequentially. - The
dirs
variable takesO(1)
space. - The algorithm modifies the input grid in-place by marking visited cells as '0', so no additional space is used for tracking visited cells.
- Therefore, the space complexity is
O(m * n)
due to the recursion stack.
Common Pitfalls
1. Modifying the Original Grid
Problem: The solution modifies the input grid in-place by changing '1's to '0's. This destroys the original data, which might be needed later or violates constraints if the grid should remain unchanged.
Example Scenario:
grid = [['1','1','0'], ['0','1','0']] solution = Solution() count = solution.numIslands(grid) # grid is now [['0','0','0'], ['0','0','0']] - original data lost!
Solution: Create a separate visited tracking structure or make a deep copy:
def numIslands(self, grid: List[List[str]]) -> int:
# Option 1: Use a visited set
visited = set()
def dfs(row, col):
visited.add((row, col))
for dr, dc in [(-1, 0), (0, 1), (1, 0), (0, -1)]:
next_row, next_col = row + dr, col + dc
if (0 <= next_row < rows and
0 <= next_col < cols and
grid[next_row][next_col] == '1' and
(next_row, next_col) not in visited):
dfs(next_row, next_col)
# Option 2: Work with a copy
grid_copy = [row[:] for row in grid]
# Then use grid_copy in the original algorithm
2. Stack Overflow with Large Islands
Problem: For very large grids where one island covers most cells, the recursive DFS can cause stack overflow due to deep recursion.
Example: A 1000Γ1000 grid filled entirely with '1's would require recursion depth of up to 1,000,000.
Solution: Use iterative DFS with an explicit stack:
def dfs_iterative(row, col):
stack = [(row, col)]
while stack:
curr_row, curr_col = stack.pop()
if (curr_row < 0 or curr_row >= rows or
curr_col < 0 or curr_col >= cols or
grid[curr_row][curr_col] != '1'):
continue
grid[curr_row][curr_col] = '0'
# Add all 4 neighbors to stack
stack.extend([(curr_row-1, curr_col), (curr_row+1, curr_col),
(curr_row, curr_col-1), (curr_row, curr_col+1)])
3. Type Confusion with Grid Values
Problem: Mixing string '1'/'0' with integer 1/0 comparisons, leading to subtle bugs.
Example:
# Wrong - comparing string with integer if grid[row][col] == 1: # This will always be False! dfs(row, col) # Correct if grid[row][col] == '1': dfs(row, col)
Solution: Be consistent with data types and consider converting at the start:
# Convert to integers if needed for consistency
grid = [[int(cell) for cell in row] for row in grid]
# Then use integer comparisons throughout
4. Incorrect Boundary Checking Order
Problem: Accessing grid elements before checking boundaries causes IndexError.
Example:
# Wrong - accesses grid before boundary check if grid[next_row][next_col] == '1' and 0 <= next_row < rows: dfs(next_row, next_col) # IndexError if next_row is out of bounds! # Correct - check boundaries first if 0 <= next_row < rows and 0 <= next_col < cols and grid[next_row][next_col] == '1': dfs(next_row, next_col)
Solution: Always validate indices before accessing array elements, using short-circuit evaluation.
What's the output of running the following function using input [30, 20, 10, 100, 33, 12]
?
1def fun(arr: List[int]) -> List[int]:
2 import heapq
3 heapq.heapify(arr)
4 res = []
5 for i in range(3):
6 res.append(heapq.heappop(arr))
7 return res
8
1public static int[] fun(int[] arr) {
2 int[] res = new int[3];
3 PriorityQueue<Integer> heap = new PriorityQueue<>();
4 for (int i = 0; i < arr.length; i++) {
5 heap.add(arr[i]);
6 }
7 for (int i = 0; i < 3; i++) {
8 res[i] = heap.poll();
9 }
10 return res;
11}
12
1class HeapItem {
2 constructor(item, priority = item) {
3 this.item = item;
4 this.priority = priority;
5 }
6}
7
8class MinHeap {
9 constructor() {
10 this.heap = [];
11 }
12
13 push(node) {
14 // insert the new node at the end of the heap array
15 this.heap.push(node);
16 // find the correct position for the new node
17 this.bubble_up();
18 }
19
20 bubble_up() {
21 let index = this.heap.length - 1;
22
23 while (index > 0) {
24 const element = this.heap[index];
25 const parentIndex = Math.floor((index - 1) / 2);
26 const parent = this.heap[parentIndex];
27
28 if (parent.priority <= element.priority) break;
29 // if the parent is bigger than the child then swap the parent and child
30 this.heap[index] = parent;
31 this.heap[parentIndex] = element;
32 index = parentIndex;
33 }
34 }
35
36 pop() {
37 const min = this.heap[0];
38 this.heap[0] = this.heap[this.size() - 1];
39 this.heap.pop();
40 this.bubble_down();
41 return min;
42 }
43
44 bubble_down() {
45 let index = 0;
46 let min = index;
47 const n = this.heap.length;
48
49 while (index < n) {
50 const left = 2 * index + 1;
51 const right = left + 1;
52
53 if (left < n && this.heap[left].priority < this.heap[min].priority) {
54 min = left;
55 }
56 if (right < n && this.heap[right].priority < this.heap[min].priority) {
57 min = right;
58 }
59 if (min === index) break;
60 [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61 index = min;
62 }
63 }
64
65 peek() {
66 return this.heap[0];
67 }
68
69 size() {
70 return this.heap.length;
71 }
72}
73
74function fun(arr) {
75 const heap = new MinHeap();
76 for (const x of arr) {
77 heap.push(new HeapItem(x));
78 }
79 const res = [];
80 for (let i = 0; i < 3; i++) {
81 res.push(heap.pop().item);
82 }
83 return res;
84}
85
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Union Find Disjoint Set Union Data Structure Introduction Prerequisite Depth First Search Review problems dfs_intro Once we have a strong grasp of recursion and Depth First Search we can now introduce Disjoint Set Union DSU This data structure is motivated by the following problem Suppose we have sets of elements
Want a Structured Path to Master System Design Too? Donβt Miss This!