Start learning free

LESSON 361 OF 414 · FREE CODING INTERVIEW QUESTIONS

Number of Islands in Scala: Depth-First Search on a 2D Array

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 14
    Ctrl + Enter to check

    SCALA · FREE CODING INTERVIEW QUESTIONS

    Number Of Islands in Scala, explained

    Problem overview

    The method numIslands receives a grid as an Array[Array[Int]]. A cell holding 1 is land and a cell holding 0 is water. An island is a group of land cells connected horizontally or vertically. The method returns how many islands the grid contains.

    In the first example, all the 1s are linked through side-by-side neighbours, so there is one island. In the second, the four 1s in the top-left block form one island, the lone 1 in row 2 forms a second, and the two 1s at the bottom right form a third. The answer is 3. Diagonal contact doesn't connect cells: the lone 1 in row 2 touches the top-left block only at a corner.

    Approach: count, then erase the island

    Scan the grid cell by cell. The first land cell you meet in an island means a new island. The difficulty is that an island has many cells, and you must not count it once per cell.

    The fix is to erase an island as soon as it is counted. When the scan reaches a 1, add one to the result and run a depth-first search (DFS) from that cell. The search visits every land cell connected to it and turns each into 0. When the scan continues, the rest of that island is already water, so it can't be counted again. Each island therefore adds exactly 1, at its first cell in scan order.

    The DFS is a recursive method. For one cell it asks whether the cell is land inside the grid. If not, it stops. If so, it turns the cell into water and does the same for the four neighbours: up, down, left and right.

    Step by step

    The lesson writes the main method first and assumes a helper exists, then writes the helpers.

    1. Declare result with var, set to 0. It counts the islands found so far and changes later, so it can't be a val.
    2. Loop over the rows with for (i <- 0 until grid.length). until excludes the upper bound, so i covers exactly the valid row indexes.
    3. Loop over the columns inside it with 0 until grid(0).length. Every row has the same length, so the first row's length is the column count.
    4. Check whether grid(i)(j) == 1. Scala indexes with parentheses, one pair per dimension.
    5. Increment result with += inside the if. This is the only place the count changes. A land cell reached here wasn't erased by an earlier search, so it belongs to a new island.
    6. Call dfs(grid, i, j) to erase the island. After the outer loop, end the method with result.
    7. Write dfs. It takes the grid, a row and a column, and returns Unit, which is Scala's "no useful value".
    8. Stop at invalid cells. If !isValidCell(grid, row, col), return right away. Water and off-grid neighbours both end the search in that direction.
    9. Mark the cell as water with grid(row)(col) = 0 before recursing. Without it, two neighbouring cells would keep sending the search back to each other forever.
    10. Describe the four moves as an array of (row change, column change) pairs: up (-1, 0), down (1, 0), left (0, -1) and right (0, 1).
    11. Recurse on each neighbour: loop over directions and call dfs on row + d(0) and col + d(1).
    12. Write isValidCell. It returns false if the position is outside the grid, and otherwise whether the cell equals 1. The bounds are tested first because reading outside an array throws.

    The solution in Scala

    class Solution {
        def isValidCell(grid: Array[Array[Int]], row: Int, col: Int): Boolean = {
            if (row < 0 || col < 0 || row >= grid.length || col >= grid(0).length) {
                return false
            }
            grid(row)(col) == 1
        }
        def dfs(grid: Array[Array[Int]], row: Int, col: Int): Unit = {
            if (!isValidCell(grid, row, col)) {
                return
            }
            grid(row)(col) = 0
            val directions = Array(Array(-1, 0), Array(1, 0), Array(0, -1), Array(0, 1))
            for (d <- directions) {
                dfs(grid, row + d(0), col + d(1))
            }
        }
        def numIslands(grid: Array[Array[Int]]): Int = {
            var result = 0
            for (i <- 0 until grid.length) {
                for (j <- 0 until grid(0).length) {
                    if (grid(i)(j) == 1) {
                        result += 1
                        dfs(grid, i, j)
                    }
                }
            }
            result
        }
    }
    

    The Scala details worth knowing:

    • Arrays of arrays. Array[Array[Int]] is an array whose elements are themselves arrays, so grid(row)(col) first picks a row, then a cell in it. Assigning works the same way: grid(row)(col) = 0 is a call to the array's update method, written as an assignment.
    • The grid is changed in place. Arrays are mutable objects passed by reference, so dfs modifies the caller's grid. After numIslands returns, every cell is 0, and calling it again on the same grid returns 0. Copy the grid first if you still need it.
    • return inside a Unit method. A bare return leaves dfs early. In isValidCell the early return false is followed by a last expression, grid(row)(col) == 1, whose value is the result when the if doesn't fire.
    • Short-circuit ||. The conditions are evaluated left to right and stop at the first true one, so grid(row) is never read with a bad row. Put the bounds test after the read and the method throws ArrayIndexOutOfBoundsException.
    • The ranges. 0 until grid.length yields 0 through grid.length - 1. Using 0 to grid.length would include one index too many and throw.
    • The directions array is rebuilt on every call. It is small, so it costs little, but it could live outside the method as a field.

    Complexity

    Let the grid have R rows and C columns.

    Time: O(R * C). The scan looks at every cell once. Each land cell is erased by exactly one dfs call that gets past the validity check, and every other call returns immediately. A cell is the target of at most four neighbour calls, so the number of calls is at most a constant times the number of cells.

    Space: O(R * C) in the worst case. The only extra memory is the call stack. One snaking island covering the whole grid makes the recursion as deep as the number of cells. A very large all-land grid, hundreds of rows by hundreds of columns, can overflow the stack and end the program; the exact size depends on the stack the JVM is given. An explicit stack or queue instead of recursion avoids that limit.

    Edge cases and common mistakes

    • All water. A grid of zeros returns 0.
    • Empty grid. Array[Array[Int]]() returns 0, because the outer loop never runs and grid(0) is never evaluated.
    • All land. Array(Array(1, 1), Array(1, 1)) is one island and returns 1.
    • Diagonal cells. A 3 by 3 grid with 1s at the four corners and the centre returns 5, because diagonals don't connect.
    • Incrementing inside dfs. The count would go up once per cell rather than once per island.
    • Marking the cell after the recursive calls. The search would revisit cells it is still working on and recurse until the stack overflows.
    • Using grid.length for the columns. That's the number of rows. The column count is grid(0).length, which goes wrong on a grid with different numbers of rows and columns.
    • Rows of different lengths. The solution assumes a rectangular grid. A shorter later row makes the check on grid(0).length let an out-of-range column through, and the read throws.

    Testing

    • The first example grid gives 1.
    • The second example grid gives 3.
    • A 3 by 3 grid with 1s at the corners and the centre gives 5.
    • Array(Array(0)) gives 0.
    • Array(Array(1, 1), Array(1, 1)) gives 1.
    Keep going with ScalaAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free