Start learning free

LESSON 361 OF 414 · FREE CODING INTERVIEW QUESTIONS

Number of Islands in Rust: Counting with Depth-First Search

Practice O(n * m) Read the full explanation

Given a 2d grid map of 1s (land) and 0s (water), count the number of islands. An island is surrounded by water and is formed by connecting adjacent lands horizontally or vertically. You may assume all four edges of the grid are all surrounded by water.

Example 1:

Input:
1 1 1 1 0
1 1 0 1 0
1 1 0 0 0
0 0 0 0 0

Output: 1

Example 2:

Input:
1 1 0 0 0
1 1 0 0 0
0 0 1 0 0
0 0 0 1 1

Output: 3
STEP 1 OF 19
    Ctrl + Enter to check

    RUST · FREE CODING INTERVIEW QUESTIONS

    Number Of Islands in Rust, explained

    Problem overview

    The function num_islands receives a grid of 1s (land) and 0s (water) as a Vec<Vec<i32>>. It returns how many islands the grid contains. An island is a group of land cells connected horizontally or vertically; diagonal neighbours don't connect. The edges of the grid are treated as water.

    In the first example the grid is:

    1 1 1 1 0
    1 1 0 1 0
    1 1 0 0 0
    0 0 0 0 0
    

    All the 1s are linked, so the answer is 1. In the second example:

    1 1 0 0 0
    1 1 0 0 0
    0 0 1 0 0
    0 0 0 1 1
    

    there is a 2×2 block, a lone cell at row 2 and a pair at the bottom right. The lone cell doesn't touch the pair, because it only meets it diagonally, so the answer is 3.

    Approach: count a start, then erase the island

    Scan the grid cell by cell. The first land cell you meet in a new island is the one that gets counted. The difficulty is not counting the other cells of the same island again.

    The fix is to erase the island as soon as you count it. When the scan finds a 1 that nothing has erased, it adds one to the result and starts a depth-first search (DFS) from that cell. The DFS turns the cell into 0, then visits each of its four neighbours, which turn their own land neighbours into 0, and so on, until the whole island is water. When the scan continues, every remaining 1 must belong to an island that hasn't been seen.

    Overwriting the grid works as the "visited" marker, so no extra structure is needed. The cost is that the input is modified. That is fine here, because the function takes ownership of the grid (the mut grid parameter), so the caller never sees the changes.

    Step by step

    1. Declare result with let mut, starting at 0.
    2. Read the number of rows and handle an empty grid. If grid.len() is 0, there are no islands, so return 0. This also makes grid[0] safe in the next step.
    3. Read the number of columns as grid[0].len().
    4. Loop over every cell with two nested for loops over 0..rows and 0..cols.
    5. When grid[i][j] is 1, add 1 to result and call Self::dfs(&mut grid, i, j). The cell belongs to an island nothing has erased yet.
    6. Return result after the loops, as the final expression.
    7. Write dfs. It takes the grid by mutable reference plus a row and a col, and returns nothing.
    8. Stop at water. If the cell is 0, it is water or already erased, so return.
    9. Erase the cell by setting it to 0 before exploring its neighbours. If this were done after the recursive calls, two neighbouring cells could keep calling each other forever.
    10. Read the grid's dimensions inside dfs, so the moves can be checked against them.
    11. Visit the four neighbours, but only when each one exists: row > 0 for up, row + 1 < rows for down, col > 0 for left and col + 1 < cols for right.

    The solution in Rust

    impl Solution {
        fn dfs(grid: &mut Vec<Vec<i32>>, row: usize, col: usize) {
            if grid[row][col] == 0 {
                return;
            }
            grid[row][col] = 0;
            let rows = grid.len();
            let cols = grid[0].len();
            if row > 0 {
                Self::dfs(grid, row - 1, col);
            }
            if row + 1 < rows {
                Self::dfs(grid, row + 1, col);
            }
            if col > 0 {
                Self::dfs(grid, row, col - 1);
            }
            if col + 1 < cols {
                Self::dfs(grid, row, col + 1);
            }
        }
    
        fn num_islands(mut grid: Vec<Vec<i32>>) -> i32 {
            let mut result = 0;
            let rows = grid.len();
            if rows == 0 {
                return 0;
            }
            let cols = grid[0].len();
            for i in 0..rows {
                for j in 0..cols {
                    if grid[i][j] == 1 {
                        result += 1;
                        Self::dfs(&mut grid, i, j);
                    }
                }
            }
            result
        }
    }
    

    The Rust details worth a closer look:

    • mut grid in the signature. The parameter owns its vector (the caller's vector is moved in), and mut lets the function change it. The caller can't use its own variable afterwards.
    • &mut grid versus grid. In num_islands you lend the grid with &mut grid. Inside dfs, grid already is a &mut Vec<Vec<i32>>, so the recursive calls pass it on as it is. Rust reborrows it for each call, so the same reference can be passed several times in a row.
    • usize and the neighbour checks. Indices are usize, which can't be negative. If row is 0, row - 1 is not -1: in an optimized build it wraps to the largest usize, and indexing with it panics with "index out of bounds". In a debug build the subtraction itself panics. That is why row > 0 is checked before subtracting. The check row + 1 < rows is written as an addition so that no subtraction can underflow.
    • Self::dfs. dfs has no self parameter, so it is an associated function, called through the type.
    • Double indexing. grid[row][col] indexes the outer vector, then the inner one. Both are bounds-checked.
    • No return value in dfs. A function without -> returns (), and the bare return; exits early.

    Complexity

    Time: O(rows × cols). The scan visits each cell once. The DFS also touches each land cell once, because it erases a cell the first time it reaches it, and every later arrival returns immediately. Each cell is therefore handled a constant number of times in total.

    Space: O(rows × cols) in the worst case. The grid is modified in place, so there is no visited array. The recursion's call stack is the extra cost: on a grid that is all land, the depth can reach the number of cells.

    Edge cases and common mistakes

    • Empty grid. vec![] returns 0. Without the early return, grid[0] would panic.
    • All water. A 2×2 grid of zeros returns 0.
    • One big island. A 2×3 grid of 1s returns 1.
    • Diagonals don't connect. The grid [[1, 0], [0, 1]] has 2 islands, and a 3×3 checkerboard with land at the corners and the centre has 5.
    • Marking after recursing. Erasing the cell after the four recursive calls makes neighbouring cells visit each other again and again until the stack overflows.
    • Unchecked moves. Skipping the bounds checks causes a panic at the first land cell on an edge.
    • Very deep recursion. A huge single island, such as a 1,000 × 1,000 grid of land, can exhaust the usual 8 MB stack of a program's main thread, because each cell adds a stack frame. For grids that large, an explicit stack stored in a Vec is safer.

    Testing

    • The first example grid gives 1, and the second gives 3.
    • [[1]] gives 1.
    • [[0, 0], [0, 0]] gives 0.
    • [[1, 0, 1, 0, 1]] gives 3.
    • [[1, 0, 1], [0, 1, 0], [1, 0, 1]] gives 5.
    Keep going with RustAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free