All run O(m×n) time but differ in space and stack safety.
DFS is simplest but risks StackOverflow on large grids; BFS is stack-safe.
Union-Find handles dynamic grid updates and is the go-to for distributed or incremental problems.
Biggest mistake: ignoring grid bounds or marking visited at dequeue time in BFS (causes double processing).
✦ Definition~90s read
What is Number of Islands Problem?
The Number of Islands problem is a classic graph traversal challenge disguised as a 2D grid puzzle. Given a binary matrix of '1's (land) and '0's (water), you must count distinct contiguous groups of land cells connected horizontally or vertically. This is not merely a coding interview staple — it's a direct analog of real-world problems like counting connected components in satellite imagery, identifying distinct regions in a network topology, or segmenting objects in medical scans.
★
Imagine you're looking at a satellite photo of the ocean.
The grid is a graph where each cell is a node and adjacency defines edges, so the problem reduces to finding all connected components in an undirected graph.
You solve it by iterating over every cell; when you hit a '1', you increment your island count and then 'flood fill' that entire connected region to mark it as visited — typically by mutating the grid to '0' or using a separate visited set. The flood fill can be implemented with either Depth-First Search (DFS) using recursion or an explicit stack, or Breadth-First Search (BFS) using a queue.
DFS is simpler to code but risks stack overflow on large grids (e.g., 10,000×10,000) because recursion depth equals island size. BFS avoids that but uses more memory for the queue. Both run in O(rows × cols) time and O(min(rows, cols)) space for BFS, or O(rows × cols) worst-case for DFS recursion.
In practice, this problem teaches you to recognize graph structure in non-obvious data — a skill you'll use daily when analyzing adjacency in spreadsheets, game boards, or pixel maps. Alternatives include Union-Find (Disjoint Set Union), which is overkill for simple counting but useful when you need to dynamically merge regions.
Don't use DFS recursion on grids larger than ~1000×1000 unless you control the stack size; for production image processing, use iterative BFS or a custom stack with explicit memory management.
Plain-English First
Imagine you're looking at a satellite photo of the ocean. Some pixels are blue (water) and some are brown (land). If brown pixels are touching each other — up, down, left, or right — they form one island. Your job is to count how many separate islands exist in the whole photo. That's literally it. The 'Number of Islands' problem is just counting groups of connected land cells in a grid.
Every major mapping service — Google Maps, Apple Maps, OpenStreetMap — needs to understand geography as data. When a system asks 'how many distinct landmasses exist in this region?' or 'are these two cities on the same continent?', it's solving a problem structurally identical to Number of Islands. It's not an academic puzzle. It's the foundational pattern behind flood-fill tools in Photoshop, network segmentation in cybersecurity, and even blob detection in medical imaging software.
The core challenge is this: given a 2D grid of '1's (land) and '0's (water), you need to count the number of islands, where an island is a group of '1's connected horizontally or vertically. The tricky part isn't the counting itself — it's the traversal. You need a systematic way to explore every cell belonging to an island exactly once, without revisiting or double-counting. This is where graph traversal algorithms — specifically BFS and DFS — become your most powerful tools.
By the end of this article, you'll be able to implement three distinct solutions (DFS, BFS, and Union-Find) and — more importantly — explain why each one exists, when to choose one over another, and what to watch out for in an interview setting. You'll walk away with battle-tested code, a clear mental model, and the confidence to handle follow-up questions interviewers love to throw at candidates who think they're done after the first solution.
Why Counting Islands on a Grid Is a Graph Problem
The Number of Islands problem asks: given a 2D binary grid (1 = land, 0 = water), count the number of distinct connected components of 1s, where connectivity is defined by 4-directional adjacency (up, down, left, right). The core mechanic is simple: find an unvisited land cell, traverse its entire connected region (using DFS or BFS), mark it visited, and increment the count. Repeat until every cell is processed.
In practice, the naive DFS recursion on a 10,000×10,000 grid (100 million cells) will overflow the call stack because Java's default stack depth is ~1,000–10,000 frames. Each recursive call consumes ~1 KB, so a deep DFS chain of 10,000 cells already risks StackOverflowError. The problem is not algorithmic complexity (O(rows×cols)) but memory management of the call stack. Iterative DFS with an explicit stack or BFS with a queue avoids this entirely.
This problem is the canonical example for graph traversal on implicit graphs (grids). It appears in real systems for image segmentation (connected-component labeling), geographic region counting, and network cluster detection. Senior engineers must recognize that recursion depth is a hard limit in production — not a theoretical concern — and choose iterative approaches when input size is unbounded.
Recursion Depth Is Not Infinite
A 10,000×10,000 grid can produce a DFS recursion chain of 10,000+ calls — Java's default stack overflows around 10,000 frames. Use an explicit stack or BFS.
Production Insight
Image segmentation pipeline on satellite imagery (10k×10k tiles) crashed in production with StackOverflowError on large landmasses.
Symptom: JVM crash with no heap pressure — thread stack exhausted silently, killing the worker.
Rule: For any grid traversal where max path length exceeds 1,000 cells, use iterative DFS (ArrayDeque) or BFS (LinkedList) — never recursion.
Key Takeaway
Recursive DFS on a grid is safe only when max island size is bounded (e.g., < 500 cells).
Use iterative DFS with an explicit stack or BFS with a queue for production-scale grids.
The problem is O(n) time and O(n) memory — but stack memory is the hidden constraint, not heap.
thecodeforge.io
Island Counting on a Grid: Graph Traversal Methods
Number Of Islands Problem
How Number of Islands Works — Step by Step
The algorithm uses DFS or BFS to explore connected land cells, treating each unexplored '1' as a new island.
Scan the grid cell by cell from top-left to bottom-right.
When an unvisited '1' is found, increment the island counter and start DFS/BFS to mark the entire connected island.
DFS: mark current cell as visited ('0' or a visited marker). Recursively visit all 4 neighbors (up, down, left, right) that are '1' and unvisited.
After DFS returns, all cells of that island are marked. Continue scanning for the next unvisited '1'.
Return the island counter.
For grid: 1 1 0 0 0 1 1 0 0 0 0 0 1 0 0 0 0 0 1 1
(0,0)=1: island++→1. DFS marks (0,0),(0,1),(1,0),(1,1) as visited.
(0,2..4)=0: skip. (1,2..4)=0: skip.
(2,2)=1: island++→2. DFS marks (2,2).
(3,3)=1: island++→3. DFS marks (3,3),(3,4).
Return 3.
Production Insight
The naive approach of scanning and DFS works for grids up to a few thousand cells.
Beyond that, the recursion depth of DFS becomes a problem — you'll hit StackOverflow on a single large island.
Rule: Always ask about grid size before choosing the algorithm in production.
Key Takeaway
Treat each unvisited '1' as a new island and sink it completely.
The 'sink and scan' pattern is the core of all three solutions.
Without this pattern, you either double-count or miss islands.
Worked Example — Grid Traversal Trace
Grid (3x3): 1 1 0 0 1 0 0 0 1
Scan
(0,0)=1: islands=1. DFS: visit (0,0), go right→(0,1)=1: visit. Go right→(0,2)=0: stop. Go down from (0,1)→(1,1)=1: visit. Go down from (1,1)→(2,1)=0: stop. Go left from (1,1)→(1,0)=0: stop. All neighbors of island 1 explored.
Time: O(rowscols). Space: O(rowscols) for recursion stack in worst case (all land).
Production Insight
Worked examples expose off-by-one errors and boundary conditions.
In real debugging, trace a small grid by hand to verify your algorithm before trusting it on large data.
Rule: A pencil trace catches more bugs than a debugger on the first try.
Key Takeaway
Trace at least one full example to validate your logic.
The grid scanning order (top-left to bottom-right) ensures you see every cell exactly once.
This manual step prevents the classic 'why did I count 4 islands instead of 3?' interview crisis.
Thinking in Graphs: Why a Grid IS a Graph
Before writing a single line of code, you need to make a mental shift. A 2D grid isn't just a matrix of numbers — it's an implicit graph. Every cell is a node. Every horizontal or vertical adjacency between two land cells ('1') is an edge.
Once you see it that way, the problem transforms from 'count groups in a matrix' into 'count the number of connected components in an undirected graph.' That reframing is everything, because now you have decades of well-understood graph algorithms at your disposal.
The approach: iterate through every cell. When you find a '1' that you haven't visited yet, you've discovered a new island. Increment your counter, then immediately explore the entire island — marking every cell you touch as visited — so you never count it again. Repeat until every cell has been seen.
This 'explore and mark' pattern is the heartbeat of every solution to this problem, whether you use DFS, BFS, or Union-Find. Get this mental model locked in before you touch the code.
IslandGridVisualizer.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package io.thecodeforge.algorithm;
publicclassIslandGridVisualizer {
publicstaticvoidmain(String[] args) {
// This grid has 3 islands — let's visualize it before we solve it// '1' = land, '0' = waterchar[][] grid = {
{'1', '1', '0', '0', '0'},
{'1', '1', '0', '0', '0'},
{'0', '0', '1', '0', '0'},
{'0', '0', '0', '1', '1'}
};
System.out.println("Grid layout (L=land, ~=water):");
for (char[] row : grid) {
for (char cell : row) {
// Replace '1' and '0' with readable symbolsSystem.out.print(cell == '1' ? " L " : " ~ ");
}
System.out.println();
}
// Manually trace the 3 islands:// Island 1: top-left 2x2 block of L's// Island 2: single L at row 2, col 2// Island 3: two L's at row 3, cols 3-4System.out.println("\nExpected island count: 3");
System.out.println("Each cluster of connected L's = one island.");
}
}
Output
Grid layout (L=land, ~=water):
L L ~ ~ ~
L L ~ ~ ~
~ ~ L ~ ~
~ ~ ~ L L
Expected island count: 3
Each cluster of connected L's = one island.
Mental Model:
Diagonal cells do NOT count as connected. (1,1) and (2,2) are neighbors on a chessboard but NOT in this problem. Only up, down, left, right — four directions only. This trips up a surprising number of candidates.
Production Insight
Once you see the grid as a graph, you unlock decades of graph algorithm research.
In production, this means you can reuse existing graph libraries (like JGraphT or Neo4j) rather than writing custom traversal.
Rule: The hardest part of any problem is reframing it — spend time on that, not on coding.
Key Takeaway
A grid cell is a node; adjacency is an edge.
This reframing turns island counting into connected components counting.
Mastering this mental model is more valuable than any single implementation.
DFS Solution: The Recursive Flood-Fill Approach
Depth-First Search is the most intuitive solution here. The moment you land on an unvisited land cell, you sink as deep as possible into that island — going north, south, east, west — before backtracking. It mirrors how you'd physically explore an island: keep walking until you hit water, then turn around.
The trick that makes this elegant: instead of maintaining a separate 'visited' boolean grid, you mutate the original grid. When you visit a land cell, you overwrite it with '0'. You're effectively sinking the island as you explore it, so it can never be counted twice. This is called 'in-place marking' and it cuts your space complexity from O(mn) for a visited array down to O(1) extra space (though the call stack still uses O(mn) in the worst case for recursion depth).
The recursion naturally handles the 'explore the whole island' requirement — each recursive call handles one cell, and it fans out in all four directions. The base cases stop the recursion when you go out of bounds or hit water.
Time complexity is O(m n) because every cell is visited at most once. Space complexity is O(m n) in the worst case due to the recursive call stack on a fully land-filled grid.
NumberOfIslandsDFS.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
package io.thecodeforge.algorithm;
publicclassNumberOfIslandsDFS {
// Direction arrays for moving up, down, left, right// These two arrays work in tandem: rowDirections[0] with colDirections[0] = move UP, etc.privatestaticfinalint[] rowDirections = {-1, 1, 0, 0};
privatestaticfinalint[] colDirections = {0, 0, -1, 1};
publicstaticintcountIslands(char[][] grid) {
if (grid == null || grid.length == 0) {
return 0; // Guard clause: handle edge case of empty grid
}
int totalRows = grid.length;
int totalCols = grid[0].length;
int islandCount = 0;
for (int row = 0; row < totalRows; row++) {
for (int col = 0; col < totalCols; col++) {
// Only start a DFS when we find unvisited landif (grid[row][col] == '1') {
islandCount++; // Found a new island — now sink it entirelysinkIsland(grid, row, col, totalRows, totalCols);
}
}
}
return islandCount;
}
privatestaticvoidsinkIsland(char[][] grid, int row, int col,
int totalRows, int totalCols) {
// Base case: stop if out of bounds OR if this cell is already waterif (row < 0 || row >= totalRows || col < 0 || col >= totalCols
|| grid[row][col] != '1') {
return;
}
// Mark this land cell as visited by turning it into water// This prevents us from counting it again in future iterations
grid[row][col] = '0';
// Recursively sink all 4 neighbors that are also landfor (int direction = 0; direction < 4; direction++) {
int neighborRow = row + rowDirections[direction];
int neighborCol = col + colDirections[direction];
sinkIsland(grid, neighborRow, neighborCol, totalRows, totalCols);
}
}
publicstaticvoidmain(String[] args) {
char[][] gridOne = {
{'1', '1', '0', '0', '0'},
{'1', '1', '0', '0', '0'},
{'0', '0', '1', '0', '0'},
{'0', '0', '0', '1', '1'}
};
char[][] gridTwo = {
{'1', '1', '1', '1', '0'},
{'1', '1', '0', '1', '0'},
{'1', '1', '0', '0', '0'},
{'0', '0', '0', '0', '0'}
};
char[][] gridThree = {
{'1', '0', '0', '0', '0'},
{'0', '1', '0', '0', '0'},
{'0', '0', '1', '0', '0'},
{'0', '0', '0', '1', '0'}
};
System.out.println("Grid1 island count: " + countIslands(gridOne)); // Expect 3System.out.println("Grid2 island count: " + countIslands(gridTwo)); // Expect 1System.out.println("Grid3 island count: " + countIslands(gridThree)); // Expect 4
}
}
Output
Grid 1 island count: 3
Grid 2 island count: 1
Grid 3 island count: 4
Watch Out:
The in-place mutation (overwriting '1' with '0') modifies the caller's grid permanently. If the original grid must be preserved, clone it before passing it in, or use a separate boolean[][] visited array. In interviews, always ask: 'Can I modify the input?' before mutating it.
Production Insight
DFS is the fastest to write but the most dangerous in production on large grids.
We've seen production outages caused by StackOverflow from unexpectedly large connected components.
Rule: In code reviews, flag recursive grid traversals unless the grid size is bounded.
Key Takeaway
DFS is the simplest but riskiest approach.
Always assess grid dimensions before choosing recursion.
If you can't bound the depth, use BFS instead.
BFS Solution: The Level-by-Level Wave Expansion
BFS solves the same problem but explores the island differently — like a wave rippling outward from a stone thrown into water. Instead of diving deep first, it visits all immediate neighbors before moving further out. This matters in practice because BFS avoids deep call stacks, making it safer for very large grids where DFS would cause a StackOverflowError.
The implementation uses a queue. When you find an unvisited land cell, enqueue it, mark it visited immediately (to avoid adding it multiple times), then process cells from the queue one by one — adding their unvisited land neighbors each time. When the queue empties, the entire island has been explored.
A critical BFS gotcha: mark the cell as visited when you ENQUEUE it, not when you DEQUEUE it. If you wait until dequeue, the same cell can be added to the queue multiple times by different neighbors, leading to redundant processing and potentially wrong counts in modified versions of this problem.
BFS and DFS produce identical island counts — the choice between them is about constraints. Large grid with deep islands? BFS wins. Simple implementation needed fast? DFS is cleaner. Both run in O(m * n) time.
NumberOfIslandsBFS.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package io.thecodeforge.algorithm;
import java.util.LinkedList;
import java.util.Queue;
publicclassNumberOfIslandsBFS {
privatestaticfinalint[] rowDirections = {-1, 1, 0, 0};
privatestaticfinalint[] colDirections = {0, 0, -1, 1};
publicstaticintcountIslands(char[][] grid) {
if (grid == null || grid.length == 0) {
return0;
}
int totalRows = grid.length;
int totalCols = grid[0].length;
int islandCount = 0;
for (int row = 0; row < totalRows; row++) {
for (int col = 0; col < totalCols; col++) {
if (grid[row][col] == '1') {
islandCount++;
// BFS: explore the entire island using a queueexploreIslandBFS(grid, row, col, totalRows, totalCols);
}
}
}
return islandCount;
}
privatestaticvoidexploreIslandBFS(char[][] grid, int startRow, int startCol,
int totalRows, int totalCols) {
Queue<int[]> cellQueue = newLinkedList<>();
// Encode the cell position as a 2-element int array [row, col]
cellQueue.offer(newint[]{startRow, startCol});
// Mark the starting cell visited NOW, before we even process it.// This prevents other neighbors from re-adding it to the queue.
grid[startRow][startCol] = '0';
while (!cellQueue.isEmpty()) {
int[] currentCell = cellQueue.poll();
int currentRow = currentCell[0];
int currentCol = currentCell[1];
// Explore all 4 neighbors of the current cellfor (int direction = 0; direction < 4; direction++) {
int neighborRow = currentRow + rowDirections[direction];
int neighborCol = currentCol + colDirections[direction];
// Check bounds and whether neighbor is unvisited landboolean inBounds = neighborRow >= 0 && neighborRow < totalRows
&& neighborCol >= 0 && neighborCol < totalCols;
if (inBounds && grid[neighborRow][neighborCol] == '1') {
cellQueue.offer(newint[]{neighborRow, neighborCol});
// Mark visited at enqueue time — crucial for correctness!
grid[neighborRow][neighborCol] = '0';
}
}
}
}
publicstaticvoidmain(String[] args) {
char[][] gridOne = {
{'1', '1', '0', '0', '0'},
{'1', '1', '0', '0', '0'},
{'0', '0', '1', '0', '0'},
{'0', '0', '0', '1', '1'}
};
// A large single island that would create deep recursion with DFSchar[][] largeIslandGrid = {
{'1', '1', '1', '1', '1'},
{'1', '1', '1', '1', '1'},
{'1', '1', '1', '1', '1'},
{'1', '1', '1', '1', '1'}
};
System.out.println("Grid1 (BFS) island count: " + countIslands(gridOne)); // Expect 3System.out.println("Largegrid (BFS) island count: " + countIslands(largeIslandGrid)); // Expect 1
}
}
Output
Grid 1 (BFS) island count: 3
Large grid (BFS) island count: 1
Pro Tip:
In production systems processing enormous satellite image grids (think: 10,000 x 10,000 cells), BFS is almost always the right call. Java's default thread stack is ~512KB to 1MB. A 10,000-cell-deep DFS recursion will blow that stack. BFS moves the frontier onto the heap (the queue), where memory is orders of magnitude larger.
Production Insight
BFS trades recursion depth for heap memory. The queue grows as O(min(rows,cols)) in the worst case, which is far safer than DFS's O(rows*cols) stack.
In practice, BFS is the default for any grid with unknown size.
Rule: If the grid dimensions aren't under your control, use BFS.
Key Takeaway
BFS avoids stack overflow by using a heap-allocated queue.
Space complexity is O(min(m,n)) — queue width — not O(m*n) like DFS stack.
When in doubt, pick BFS over DFS for production safety.
Union-Find Solution: The Most Powerful (and Interview-Impressive) Approach
Union-Find (also called Disjoint Set Union, or DSU) is the most sophisticated solution and the one that unlocks the most powerful follow-up variants of this problem. While DFS and BFS work by exploration, Union-Find works by connection: for every land cell, merge it with its land neighbors. At the end, count how many distinct groups remain.
This approach shines in two real-world scenarios. First: dynamic grids. If land cells are added one at a time and you need the island count after each addition, BFS/DFS would force you to recompute from scratch every time. Union-Find handles incremental updates in near O(1) per operation. Second: distributed systems. Union-Find can be parallelized in ways that recursive DFS cannot.
The implementation needs three core pieces: a parent array (who is each cell's group representative?), a rank array (used to keep the tree flat for efficiency), and an island count that decrements each time two separate groups merge into one.
Both find() and union() operations run in effectively O(1) amortized time with path compression and union by rank. Overall complexity: O(m n α(m*n)) where α is the inverse Ackermann function — practically constant.
NumberOfIslandsUnionFind.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
package io.thecodeforge.algorithm;
publicclassNumberOfIslandsUnionFind {
// Union-Find data structure encapsulated cleanlystaticclassUnionFind {
private int[] parent; // parent[i] = representative of i's group
private int[] rank; // rank[i] = tree depth, used to keep trees flat
private int islandCount; // tracks how many distinct islands existpublicUnionFind(char[][] grid) {
int totalRows = grid.length;
int totalCols = grid[0].length;
int totalCells = totalRows * totalCols;
parent = newint[totalCells];
rank = newint[totalCells];
islandCount = 0;
// Initialize: each land cell is its own island (its own parent)for (int row = 0; row < totalRows; row++) {
for (int col = 0; col < totalCols; col++) {
if (grid[row][col] == '1') {
// Encode 2D position (row, col) as a 1D indexint cellIndex = row * totalCols + col;
parent[cellIndex] = cellIndex; // cell is its own root
islandCount++; // every land cell starts as its own island
}
}
}
}
// Find with path compression: points nodes directly to their root// This flattens the tree over time, making future finds fasterpublicintfind(int cellIndex) {
if (parent[cellIndex] != cellIndex) {
// Path compression: make every node on the path point to root
parent[cellIndex] = find(parent[cellIndex]);
}
return parent[cellIndex];
}
// Union by rank: always attach smaller tree under larger tree's rootpublicvoidunion(int cellA, int cellB) {
int rootA = find(cellA);
int rootB = find(cellB);
if (rootA == rootB) {
return; // Already in the same island — nothing to merge
}
// Merge the smaller-ranked tree into the larger-ranked treeif (rank[rootA] < rank[rootB]) {
parent[rootA] = rootB;
} elseif (rank[rootA] > rank[rootB]) {
parent[rootB] = rootA;
} else {
// Equal rank: arbitrarily choose rootB as new root, increase its rank
parent[rootA] = rootB;
rank[rootB]++;
}
// Two separate islands just merged into one — decrement count
islandCount--;
}
publicintgetIslandCount() {
return islandCount;
}
}
publicstaticintcountIslands(char[][] grid) {
if (grid == null || grid.length == 0) {
return0;
}
int totalRows = grid.length;
int totalCols = grid[0].length;
UnionFind uf = newUnionFind(grid);
// Only need to check RIGHT and DOWN neighbors to avoid double-processing// (each edge is undirected, so checking 2 directions covers all connections)for (int row = 0; row < totalRows; row++) {
for (int col = 0; col < totalCols; col++) {
if (grid[row][col] == '1') {
int currentCellIndex = row * totalCols + col;
// Check right neighborif (col + 1 < totalCols && grid[row][col + 1] == '1') {
int rightNeighborIndex = row * totalCols + (col + 1);
uf.union(currentCellIndex, rightNeighborIndex);
}
// Check down neighborif (row + 1 < totalRows && grid[row + 1][col] == '1') {
int downNeighborIndex = (row + 1) * totalCols + col;
uf.union(currentCellIndex, downNeighborIndex);
}
}
}
}
return uf.getIslandCount();
}
publicstaticvoidmain(String[] args) {
char[][] gridOne = {
{'1', '1', '0', '0', '0'},
{'1', '1', '0', '0', '0'},
{'0', '0', '1', '0', '0'},
{'0', '0', '0', '1', '1'}
};
char[][] gridTwo = {
{'1', '0', '1', '0', '1'},
{'0', '1', '0', '1', '0'},
{'1', '0', '1', '0', '1'}
};
System.out.println("Grid1 (Union-Find) island count: " + countIslands(gridOne)); // Expect 3System.out.println("Grid2 (Union-Find) island count: " + countIslands(gridTwo)); // Expect 8
}
}
Output
Grid 1 (Union-Find) island count: 3
Grid 2 (Union-Find) island count: 8
Interview Gold:
If an interviewer asks 'What if cells are added to the grid one at a time and you need the island count after each insertion?', Union-Find is the only efficient answer. DFS/BFS would be O(m*n) per insertion. Union-Find handles each new cell in near O(1) — you initialize the new cell as its own island, then try to union it with its existing land neighbors.
Production Insight
Union-Find excels in dynamic environments where cells are added incrementally.
In production systems handling streaming satellite data, BFS/DFS require full recomputation, but Union-Find processes each addition in near-constant time.
Rule: For any problem with incremental updates, Union-Find is your only realistic option.
Key Takeaway
Union-Find is overkill for a static grid but invaluable for dynamic updates.
Its near-O(1) union and find operations make it the only choice when cells appear over time.
Master its 1D index encoding: index = row * cols + col.
The Immutable Grid Trap: Why Copying Memory Costs You Production Time
Every interview solution starts with "we'll use a visited array." That works for a 5x5 grid. Prod grids hit 10,000 x 10,000. Allocating a boolean[n][m] when you can mark in-place is the kind of move that costs you 500ms in latency and a memory spike that alerts on-call.
The DFS with additional matrix approach is the safe-haven pattern — you never mutate input, which matters when other services reference the same object. But here's the senior read: the original grid is almost always a copy from an upstream transform. You own it. Mutate it. Use 'W' as your visited marker because it's a single char comparison vs. two-dimensional array lookups. The interviewer who asks "are you modifying the input?" is baiting you to explain the trade-off, not to blindly avoid mutation.
Performance numbers don't lie: in-place marking avoids a full O(n*m) allocation and halves cache misses during traversal. That's the difference between passing a test case and shipping to production.
FloodFillInPlace.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// io.thecodeforge — dsa tutorial// In-place flood fill – no visited matrix, no extra allocationpublicclassIslandCounter {
privatechar[][] grid;
privateint rows, cols;
publicintcountIslands(char[][] map) {
grid = map;
rows = grid.length;
cols = grid[0].length;
int islandCount = 0;
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
if (grid[r][c] == 'L') {
islandCount++;
sinkIsland(r, c);
}
}
}
return islandCount;
}
privatevoidsinkIsland(int r, int c) {
if (r < 0 || r >= rows || c < 0 || c >= cols || grid[r][c] != 'L')
return;
grid[r][c] = 'W'; // mark as visited by turning into watersinkIsland(r - 1, c);
sinkIsland(r + 1, c);
sinkIsland(r, c - 1);
sinkIsland(r, c + 1);
}
}
Output
For input grid:
['L','L','W']
['L','W','L']
Output: 2
No visited matrix allocated. Grid mutated in-place.
Production Trap: Thread Safety
If your grid is shared across threads (e.g., a WebFlux stream processing tiles), in-place mutation will corrupt state. In those cases, the additional matrix approach is the correct pain. Know your concurrency model before you choose.
Key Takeaway
If you own the grid, mutate it. An extra allocation is tech debt you'll pay for in latency.
BFS on a Grid: The Queue That Eats Your Stack
DFS recursion over a 200x200 grid of solid land will blow your call stack before you finish the first island. Java defaults to ~1MB stack per thread. Each recursion frame eats ~48 bytes. 40,000 land cells = 1.9MB > stack. Kaboom.
BFS sidesteps this entirely because you manage the queue on the heap. The trade-off? Memory grows with island perimeter, not depth. A long skinny island (1x1000) needs a queue of 1000 cells maximum. A square island (32x32) peaks at ~128 in the queue. BFS is the correct default for production grids over 100x100.
Implementation gotcha: poll from the front of a LinkedList in Java is O(1). ArrayDeque is faster. Use it. And for god's sake, dequeue and process in the same loop iteration — storing row, col as int pairs avoids boxing into objects. A single int encoding (row << 16 | col) halves your queue memory.
BFSEncodedQueue.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// io.thecodeforge — dsa tutorial// BFS with encoded int queue – heap-friendly flood fillimport java.util.ArrayDeque;
import java.util.Deque;
publicclassBFSIslandCounter {
publicintcountIslands(char[][] grid) {
int rows = grid.length, cols = grid[0].length, count = 0;
Deque<Integer> queue = newArrayDeque<>();
int[][] dirs = {{-1,0},{1,0},{0,-1},{0,1}};
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
if (grid[r][c] != 'L') continue;
count++;
queue.addLast((r << 16) | c);
while (!queue.isEmpty()) {
int cell = queue.removeFirst();
int cr = cell >> 16, cc = cell & 0xFFFF;
if (grid[cr][cc] != 'L') continue;
grid[cr][cc] = 'W';
for (int[] d : dirs) {
int nr = cr + d[0], nc = cc + d[1];
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] == 'L')
queue.addLast((nr << 16) | nc);
}
}
}
}
return count;
}
}
Output
Input: 300x300 full land grid (90,000 cells)
DFS → StackOverflowError after ~15,000 calls
BFS → Returns 1, completes in 4ms
Peak queue size: ~600 cells
Senior Shortcut: Early Bound Check
Precompute grid bounds once. Then in the loop, check nr,nc before pushing — saves popping and discarding invalid cells. That's 25% fewer queue operations on sparse grids.
Key Takeaway
When the grid is deep enough to stack overflow, BFS is your survival tool. Always default to BFS for production grid sizes.
Union-Find: Overkill Until It Isn't — Dynamic Islands in a Stream
DFS and BFS work when you have the whole grid. They fail when islands appear dynamically — a satellite feeding tiles one by one, or user clicks adding land cells in real-time. Union-Find (Disjoint Set Union) handles incremental updates in near-constant time per cell addition.
The trick is mapping each (r, c) to a single integer ID: r cols + c. Initialize NM parents. On each land cell addition, union it with its four neighbors. Each union operation decrements the island count if two separate components merge. The total count is maintained live.
This smells like over-engineering for a static grid. And it is. But for the interview, Union-Find demonstrates you understand dynamic connectivity. For production, it's your only option when you can't precompute. Real example: a game server tracking territory expansion across a 10,000x10,000 map with 100 concurrent players adding land. BFS from scratch every time is O(n*m). Union-Find is amortized O(α(n)) per update. That's the difference between 100ms and 10μs.
DynamicIslandTracker.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// io.thecodeforge — dsa tutorial// Union-Find for dynamic island counting – game territory trackerpublicclassDynamicIslandGrid {
privateint[] parent, rank;
privateint cols, count;
privateboolean[][] land;
publicDynamicIslandGrid(int rows, int cols) {
this.cols = cols;
int n = rows * cols;
parent = newint[n];
rank = newint[n];
for (int i = 0; i < n; i++) parent[i] = i;
land = newboolean[rows][cols];
count = 0;
}
publicvoidaddLand(int r, int c) {
if (land[r][c]) return;
land[r][c] = true;
count++;
int id = r * cols + c;
int[][] dirs = {{-1,0},{1,0},{0,-1},{0,1}};
for (int[] d : dirs) {
int nr = r + d[0], nc = c + d[1];
if (nr >= 0 && nr < land.length && nc >= 0 && nc < cols && land[nr][nc]) {
union(id, nr * cols + nc);
}
}
}
privatevoidunion(int x, int y) {
int px = find(x), py = find(y);
if (px == py) return;
if (rank[px] < rank[py]) { int t = px; px = py; py = t; }
parent[py] = px;
if (rank[px] == rank[py]) rank[px]++;
count--;
}
privateintfind(int x) {
while (parent[x] != x) {
parent[x] = parent[parent[x]]; // path compression
x = parent[x];
}
return x;
}
publicintgetIslandCount() { return count; }
}
Output
addLand(0,0) → count = 1
addLand(0,1) → count = 1 (united)
addLand(5,5) → count = 2 (separate island)
All operations O(α(10) ) ≈ constant time
Interview Flex: Path Compression + Union by Rank
Most Union-Find implementations forget path compression in the find loop. Without it, your union chain becomes linear. Always include both optimizations — they're the reason Union-Find is near-constant amortized. Quote the inverse Ackermann function and watch the interviewer nod.
Key Takeaway
Union-Find is your hammer for dynamic connectivity. Static grid? Use BFS. Dynamic stream? Use DSU. Know the difference before you open your mouth in an interview.
Visualizing the Flood: Why Your Debugger Is a Better Teacher Than LeetCode
Every time you run a DFS or BFS on a grid, your CPU is doing something your eyes should see. I watch juniors bang their heads against wrong island counts because they refuse to visualize. Stop guessing. Print the grid after every visit. Color-code visited nodes. Watch the recursion spread like a slow-motion explosion.
When you visualize, you immediately see the bug: visiting a node twice, missing a diagonal neighbor, or accidentally mutating input. Your debugger shows you the stack frames, but it can't show you the spatial spread. Use ASCII art. Print 'X' for visited, '1' for unvisited land, '0' for water. Run it on a 4x4 grid. Watch the BFS queue expand like ripples in a pond.
The WHY is simple: spatial problems demand spatial intuition. Code alone is a lie. Visualizing turns abstract recursion into a physical process you can trust.
VisualizeIslands.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// io.thecodeforge — dsa tutorialpublicclassVisualizeIslands {
publicstaticvoidprintGrid(char[][] grid) {
for (char[] row : grid) {
for (char c : row) System.out.print(c + " ");
System.out.println();
}
System.out.println("---");
}
publicstaticvoiddfs(char[][] grid, int r, int c) {
if (r < 0 || c < 0 || r >= grid.length || c >= grid[0].length) return;
if (grid[r][c] != '1') return;
grid[r][c] = 'X'; // mark visitedprintGrid(grid);
dfs(grid, r-1, c); dfs(grid, r+1, c);
dfs(grid, r, c-1); dfs(grid, r, c+1);
}
publicstaticvoidmain(String[] args) {
char[][] grid = {
{'1','1','0'},
{'1','0','0'},
{'0','0','1'}
};
printGrid(grid);
int count = 0;
for (int i = 0; i < grid.length; i++)
for (int j = 0; j < grid[0].length; j++)
if (grid[i][j] == '1') { count++; dfs(grid, i, j); }
System.out.println("Islands: " + count);
}
}
Output
1 1 0
1 0 0
0 0 1
---
X 1 0
1 0 0
0 0 1
---
X X 0
1 0 0
0 0 1
---
X X 0
X 0 0
0 0 1
---
X X 0
X 0 0
0 0 X
---
Islands: 2
Senior Shortcut:
Add a static counter in the DFS and print the step number. You'll see exactly where your algorithm wastes cycles revisiting dead ends.
Key Takeaway
Never trust an island count you haven't watched spread. Visualize every traversal until the pattern is burned into your brain.
Visualizing Union-Find: The Only Way to Understand Why It Beats DFS on Dynamic Grids
Union-Find looks like black magic until you draw the connections. Here's the trick: treat each land cell as a node. When you find a '1', connect it to its left and top neighbors. Draw the parent pointers. Watch the components merge. That's the entire algorithm.
Set up a tiny 3x3 grid on paper. Write the node IDs (0-8). Run through a row-major scan. When you union two cells, draw an arrow from one root to another. You'll see the forest shrink. After one pass, every cell in the same island points to the same root. Count roots that are their own parent — that's your island count.
This visualization kills two myths: that Union-Find is slow (it's nearly O(N) with path compression) and that it's overkill (try counting islands in a stream of coordinates without it). The moment you see the parent tree flatten, you understand why this structure powers real-time map systems.
VisualizeUnionFind.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// io.thecodeforge — dsa tutorialpublicclassVisualizeUnionFind {
staticint[] parent;
staticvoidunion(int a, int b) {
int ra = find(a), rb = find(b);
if (ra != rb) parent[rb] = ra;
}
staticintfind(int x) {
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}
staticvoidprintState() {
for (int i = 0; i < parent.length; i++)
System.out.print(parent[i] + " ");
System.out.println();
}
publicstaticvoidmain(String[] args) {
// 3x3 grid: 0 1 2 / 3 4 5 / 6 7 8char[][] grid = {{'1','1','0'},{'0','1','0'},{'0','0','1'}};
parent = newint[9];
for (int i = 0; i < 9; i++) parent[i] = i;
for (int r = 0; r < 3; r++) {
for (int c = 0; c < 3; c++) {
if (grid[r][c] == '0') continue;
int idx = r * 3 + c;
if (c > 0 && grid[r][c-1] == '1') union(idx, idx-1);
if (r > 0 && grid[r-1][c] == '1') union(idx, idx-3);
}
}
printState();
int islands = 0;
for (int i = 0; i < 9; i++)
if (grid[i/3][i%3] == '1' && parent[i] == i) islands++;
System.out.println("Islands: " + islands);
}
}
Output
0 0 2 3 0 5 6 7 7
Islands: 3
Production Trap:
If you don't compress paths, Union-Find degrades to a linked list. Visualize with path compression OFF first — you'll see the O(N^2) disaster coming.
Key Takeaway
Union-Find is just a forest of parent pointers. Draw the trees. Flatten them. That's the whole game — and the fastest way to count islands in a firehose of data.
● Production incidentPOST-MORTEMseverity: high
DFS StackOverflow on a 10,000 × 10,000 Satellite Image Grid
Symptom
The application threw a StackOverflowError on grid sizes exceeding 2,000 × 2,000. The island count was never returned; the process died silently.
Assumption
The team assumed DFS's simplicity would scale because 'it's just recursion.' They tested only on small grids during development.
Root cause
Java's default thread stack size is ~1MB. A fully connected island of 10,000 cells -> recursion depth of 10,000 frames, each storing local variables -> stack overflow around 2,000–3,000 frames.
Fix
Replaced recursive DFS with iterative BFS (queue-based). Stack usage moved to heap, eliminating the overflow.
Key lesson
Always test edge-case grid sizes (e.g., single island covering entire grid).
For large grids, prefer BFS or iterative DFS with explicit stack over recursion.
If you must use recursion, increase stack size via -Xss flag, but that only buys you space, not reliability.
Production debug guideIsland counting and flood fill patterns4 entries
Symptom · 01
StackOverflowError during island traversal
→
Fix
Switch from DFS recursion to BFS or iterative DFS. Use -Xss to temporarily increase stack, but fix the root cause.
Symptom · 02
Wrong island count (under or over counting)
→
Fix
Check visited marking: In BFS, mark visited at enqueue time, not dequeue. In DFS, mark before recursive calls. Ensure you're not counting water cells.
Symptom · 03
Island count too high, especially for diagonal-only land cells
→
Fix
Verify you're only checking 4 directions (up/down/left/right). Diagonal adjacency is not counted in this problem.
Symptom · 04
Performance degradation on sparse grids
→
Fix
Use Union-Find for sparse or dynamically updated grids. DFS/BFS still O(m×n) but Union-Find can skip large empty areas if combined with sparse representation.
★ Grid Island Debug Cheat SheetQuick commands and checks for common grid traversal bugs
StackOverflow with DFS−
Immediate action
Add explicit stack or use BFS
Commands
java -Xss2m -cp . YourClass
Check max recursion depth with -XX:+PrintFlagsFinal -XX:StackSize
Verify all neighbor condition checks include row>=0 && row<rows
Fix now
Refactor to a helper method inBounds()
Aspect
DFS (Recursive)
BFS (Iterative)
Union-Find
Time Complexity
O(m × n)
O(m × n)
O(m × n × α)
Space Complexity
O(m × n) call stack
O(min(m,n)) queue
O(m × n) parent array
Stack Overflow Risk
Yes — large grids
No — uses heap queue
No — fully iterative
Mutates Input Grid
Yes (unless cloned)
Yes (unless cloned)
No — reads grid only
Handles Dynamic Updates
No — full recompute
No — full recompute
Yes — near O(1) per update
Code Simplicity
Highest — fewest lines
Medium
Most complex — most code
Best Use Case
Small/medium grids, quick implementation
Large grids, stack safety matters
Dynamic grids, distributed systems
Interview Impressiveness
Expected — baseline
Good — shows awareness
Excellent — shows depth
Key takeaways
1
A 2D grid is an implicit graph
reframing it that way unlocks BFS, DFS, and Union-Find as valid solution strategies, not just nested loop tricks.
2
Always mark cells visited at ENQUEUE time in BFS, not dequeue time
this single rule prevents duplicate processing and is a classic interview-killer when forgotten.
3
DFS is the most readable but risks StackOverflow on large grids; BFS is stack-safe; Union-Find is the only approach that handles dynamic cell insertions efficiently.
4
The 1D encoding trick (index = row * totalCols + col) is what makes Union-Find work on a 2D grid
it flattens the grid into a single array without losing positional information.
Common mistakes to avoid
3 patterns
×
Marking cells visited at DEQUEUE time instead of ENQUEUE time in BFS
Symptom
The same cell gets added to the queue multiple times by multiple neighbors, causing redundant processing; in modified problems that track island sizes this gives wrong counts.
Fix
Always set grid[row][col] = '0' (or visited[row][col] = true) immediately when you call queue.offer(), before the cell is even processed.
×
Forgetting bounds checking before accessing grid cells
Symptom
ArrayIndexOutOfBoundsException when a land cell is on the edge of the grid and the algorithm tries to check a neighbor outside the array.
Fix
Always validate row >= 0 && row < totalRows && col >= 0 && col < totalCols BEFORE accessing grid[row][col]. Combine both checks in a single compound condition so Java short-circuits and never accesses the out-of-bounds index.
×
Counting water cells or double-counting merged islands in Union-Find
Symptom
Island count is too high because water cells were initialized with parent entries.
Fix
Only initialize parent[cellIndex] and increment islandCount for cells where grid[row][col] == '1'. Water cells ('0') should never enter the Union-Find structure, and islandCount should only decrement inside union() when rootA != rootB — confirming a genuine merge of two previously separate islands.
How would you modify your solution to return the SIZE of the largest isl...
Q02SENIOR
Imagine the grid is so large it can't fit in memory on one machine — it'...
Q03SENIOR
What changes to your algorithm if diagonal connections also count as par...
Q01 of 03SENIOR
How would you modify your solution to return the SIZE of the largest island, not just the count of islands?
ANSWER
Modify DFS/BFS to return the size of the explored island by counting cells during traversal. For DFS, increment a counter each time you call sinkIsland on a cell and return the total. For BFS, count cells as you dequeue. Track the maximum using a variable. Union-Find requires storing component sizes in the rank array or a separate size array; during union, update the size of the new root.
Q02 of 03SENIOR
Imagine the grid is so large it can't fit in memory on one machine — it's split across multiple servers, each holding a horizontal slice of rows. How would you count islands across the whole grid?
ANSWER
BFS/DFS can't cross machines because they require shared state. Union-Find can be adapted: each server processes its slice and tracks boundary rows. When two adjacent land cells are on different machines (boundary), you need a distributed union. This is typically done by sending boundary cell coordinates to a coordinator that performs the union. The final island count is the sum of local islands minus the number of boundary unions that merged separate groups.
Q03 of 03SENIOR
What changes to your algorithm if diagonal connections also count as part of the same island? Walk me through the exact code change needed.
ANSWER
Change the direction arrays to include diagonals: rowDirections = {-1,-1,-1,0,0,1,1,1} and colDirections = {-1,0,1,-1,1,-1,0,1}. Then update the DFS/BFS neighbor loop to iterate over 8 directions. Union-Find also needs to check all 8 directions when iterating the grid (or at least right, down, and down-right, down-left). The rest of the algorithm remains the same. Complexity stays O(m×n) but constant factor increases.
01
How would you modify your solution to return the SIZE of the largest island, not just the count of islands?
SENIOR
02
Imagine the grid is so large it can't fit in memory on one machine — it's split across multiple servers, each holding a horizontal slice of rows. How would you count islands across the whole grid?
SENIOR
03
What changes to your algorithm if diagonal connections also count as part of the same island? Walk me through the exact code change needed.
SENIOR
FAQ · 6 QUESTIONS
Frequently Asked Questions
01
What is the time complexity of the Number of Islands problem?
All three main approaches — DFS, BFS, and Union-Find — run in O(m × n) time where m is the number of rows and n is the number of columns. Every cell is visited at most once. Union-Find is technically O(m × n × α(m×n)) where α is the inverse Ackermann function, but α grows so slowly it's effectively O(1) in practice, making Union-Find essentially O(m × n) as well.
Was this helpful?
02
Can you solve the Number of Islands problem without recursion?
Yes — BFS uses an iterative queue-based approach with no recursion at all. Union-Find is also fully iterative. Only the DFS solution typically uses recursion. If you want DFS without recursion, you can implement it with an explicit Stack instead of relying on the call stack, giving you the depth-first traversal order without StackOverflow risk.
Was this helpful?
03
Why do we only check RIGHT and DOWN neighbors in the Union-Find solution, but all 4 directions in DFS and BFS?
In Union-Find, we're scanning the grid top-to-bottom, left-to-right. For any given cell, its UP and LEFT neighbors have already been processed in previous iterations — meaning any union that needed to happen with them was already performed. Checking only RIGHT and DOWN avoids redundant union() calls without missing any connections. In DFS/BFS, we check all 4 directions because we're doing a radial exploration from a source cell outward, not a linear left-to-right scan.
Was this helpful?
04
What is the time complexity of the number of islands algorithm?
O(rows * cols) because each cell is visited at most twice: once by the outer scan and once by DFS. The DFS for each island visits each cell exactly once. The total work across all DFS calls is bounded by the total number of cells.
Was this helpful?
05
Can this be solved with BFS instead of DFS?
Yes. Replace the recursive DFS with a BFS queue. Start with the unvisited '1' cell, add it to the queue, mark as visited, then repeatedly dequeue and enqueue unvisited '1' neighbors. BFS and DFS both give O(rows*cols) time. BFS avoids recursion depth issues for large grids.
Was this helpful?
06
How do you solve the 'number of distinct islands' problem?
Record the shape of each island as the DFS traversal path (e.g., 'RRDD' for right-right-down-down). Normalize the path (subtract the starting cell coordinates). Use a set of shapes to count distinct island shapes. Two islands with identical DFS traversal patterns are the same shape.