Number of Islands — DFS StackOverflow on 10,000×10,000 Grid
Java's default 1MB stack overflows at ~2,500 recursion depth on 10,000×10,000 grid.
20+ years shipping performance-critical code where algorithms decide the bill. Notes here come from systems that actually shipped.
- ✓Solid grasp of fundamentals
- ✓Comfortable reading code examples
- ✓Basic production concepts
- Number of Islands counts connected '1's in a 2D grid using graph traversal.
- Three approaches: DFS (recursive flood-fill), BFS (queue-based wave), Union-Find (disjoint sets).
- 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).
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.
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.
Worked Example — Grid Traversal Trace
Grid (3x3): 1 1 0 0 1 0 0 0 1
- (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.
- (0,1) already visited. (0,2)=0: skip.
- (1,0)=0: skip. (1,1) already visited. (1,2)=0: skip.
- (2,0)=0: skip. (2,1)=0: skip. (2,2)=1: islands=2. DFS: visit (2,2). No unvisited '1' neighbors.
- Total: 2 islands.
Time: O(rowscols). Space: O(rowscols) for recursion stack in worst case (all land).
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.
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.
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.
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.
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.
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.
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.
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.
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.
DFS StackOverflow on a 10,000 × 10,000 Satellite Image Grid
- 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.
java -Xss2m -cp . YourClassCheck max recursion depth with -XX:+PrintFlagsFinal -XX:StackSize| File | Command / Code | Purpose |
|---|---|---|
| IslandGridVisualizer.java | public class IslandGridVisualizer { | Thinking in Graphs |
| NumberOfIslandsDFS.java | public class NumberOfIslandsDFS { | DFS Solution |
| NumberOfIslandsBFS.java | public class NumberOfIslandsBFS { | BFS Solution |
| NumberOfIslandsUnionFind.java | public class NumberOfIslandsUnionFind { | Union-Find Solution |
| FloodFillInPlace.java | public class IslandCounter { | The Immutable Grid Trap |
| BFSEncodedQueue.java | public class BFSIslandCounter { | BFS on a Grid |
| DynamicIslandTracker.java | public class DynamicIslandGrid { | Union-Find: Overkill Until It Isn't |
| VisualizeIslands.java | public class VisualizeIslands { | Visualizing the Flood |
| VisualizeUnionFind.java | public class VisualizeUnionFind { | Visualizing Union-Find |
Key takeaways
Practice These on LeetCode
Interview Questions on This Topic
How would you modify your solution to return the SIZE of the largest island, not just the count of islands?
Frequently Asked Questions
20+ years shipping performance-critical code where algorithms decide the bill. Notes here come from systems that actually shipped.
That's Graphs. Mark it forged?
8 min read · try the examples if you haven't