Start learning free

LESSON 322 OF 573 · HASH TABLE

Valid Sudoku in Kotlin: One Pass with HashSets

Practice O(81) Read the full explanation

A Sudoku board is a 9 x 9 grid of cells. Each cell holds a digit from '1' to '9', or '.' while it is empty. Rows 0-2, 3-5 and 6-8 and columns 0-2, 3-5 and 6-8 split the board into nine 3 x 3 boxes.

Given a partly filled board, return true if it is valid so far, and false otherwise. The board is valid when no digit appears twice:

  1. in the same row,
  2. in the same column,
  3. in the same 3 x 3 box.

Only the filled cells are checked; empty cells don't count. A valid board doesn't have to be solvable: you check the digits already on it, you don't solve the puzzle.

Rows are numbered from 0 at the top and columns from 0 at the left. In the examples, each line is one row of the board.

 

Example 1:



Input: board =
53..7....
6..195...
.98....6.
8...6...3
4..8.3..1
7...2...6
.6....28.
...419..5
....8..79
Output: true
Explanation: No row, column or box holds the same digit twice.

Example 2:

Input: board =
83..7....
6..195...
.98....6.
8...6...3
4..8.3..1
7...2...6
.6....28.
...419..5
....8..79
Output: false
Explanation: The board of Example 1 with an 8 instead of the 5 in the top left corner.
Column 0 now holds two 8s, in rows 0 and 3, and so does the top left box, in rows 0 and 2.

Example 3:

Input: board =
12345678.
.........
.........
.........
........9
.........
.........
.........
.........
Output: true
Explanation: No digit repeats, so the board is valid, although it has no solution:
the last cell of row 0 can only be a 9, and column 8 already has a 9 in row 4.

 

Constraints:

  • The board has 9 rows, and each row has 9 cells.
  • Each cell holds a digit from '1' to '9' or '.'.

Note:

Your algorithm should look at each cell once and check it in constant time: O(81) time and O(81) extra space for the 81 cells. Scanning the row, the column and the box of every cell again would also work on a board this small, but it looks at each cell many times.

STEP 1 OF 11
    Ctrl + Enter to check

    KOTLIN · HASH TABLE

    Valid Sudoku in Kotlin, explained

    Problem overview

    The method receives a 9 x 9 board as Array<CharArray>. Each cell holds a digit from '1' to '9' or '.' for an empty cell. It returns true when no digit appears twice in a row, in a column or in one of the nine 3 x 3 boxes. Only the filled cells count, and the board does not have to be solvable: you check the digits that are already there and do not solve anything.

    The first example, the well-known board starting with 53..7...., is valid. Replace its top-left 5 with an 8 and it becomes invalid: column 0 then holds two 8s (rows 0 and 3), and so does the top-left box (rows 0 and 2). The third example, with 12345678. in row 0 and a single 9 at row 4, column 8, is valid even though it can't be completed, because no digit repeats.

    Approach

    For each filled cell you could scan its row, its column and its box again and look for a copy of the digit. On a 9 x 9 board that is fast enough, but it looks at every cell many times.

    A faster way is to remember what you have seen. Keep one set of digits for each of the 9 rows, one for each of the 9 columns and one for each of the 9 boxes: 27 sets. Visit every cell once. If its digit is already in the set of its row, its column or its box, a digit repeats and the board is invalid. Otherwise add the digit to all three sets. Each cell takes constant time, so the whole board takes O(81) time and O(81) space, which is a fixed amount for a fixed board.

    Step by step

    1. Row sets. val rows = Array(9) { HashSet<Char>() } builds an array of 9 sets. rows[r] holds the digits seen so far in row r.
    2. Column and box sets. Build cols and boxes the same way.
    3. Visit every cell with two nested loops: r over 0 until 9 and, inside it, c over 0 until 9. This goes row by row.
    4. Read the cell into val digit = board[r][c].
    5. Skip empty cells. If digit == '.', continue with the next cell. Empty cells can never break a rule.
    6. Find the box. r / 3 is the box's row (0, 1 or 2) and c / 3 its column. Boxes are numbered left to right, then top to bottom, so the number is (r / 3) * 3 + c / 3.
    7. Check for a repeat. If rows[r], cols[c] or boxes[box] already contains digit, return false at once.
    8. Remember the digit by adding it to the three sets, so the cells after this one find it.
    9. If both loops finish, no digit repeated: return true.

    The solution in Kotlin

    class Solution {
        fun isValidSudoku(board: Array<CharArray>): Boolean {
            val rows = Array(9) { HashSet<Char>() }
            val cols = Array(9) { HashSet<Char>() }
            val boxes = Array(9) { HashSet<Char>() }
            for (r in 0 until 9) {
                for (c in 0 until 9) {
                    val digit = board[r][c]
                    if (digit == '.') continue
                    val box = (r / 3) * 3 + c / 3
                    if (rows[r].contains(digit) || cols[c].contains(digit) || boxes[box].contains(digit)) {
                        return false
                    }
                    rows[r].add(digit)
                    cols[c].add(digit)
                    boxes[box].add(digit)
                }
            }
            return true
        }
    }
    

    Kotlin details worth knowing:

    • Array(9) { HashSet<Char>() } runs the lambda once per place, so every place gets its own set. Filling the array with one shared set would make all rows, columns or boxes use the same digits and report false repeats. The array reference is a val, but the sets inside it still change.
    • Digits are Chars, not numbers. '5' is a character, and comparing with '.' uses single quotes. The code never turns the characters into numbers: a Char works fine as a set element.
    • Integer division truncates. 7 / 3 is 2 (the remainder is dropped), while 7.0 / 3 is 2.3333333333333335. The box formula relies on this.
    • 0 until 9 excludes 9, so the indexes run 0 to 8.
    • continue inside the inner loop skips to the next c, not to the next row.
    • The || chain stops at the first true check, so the later sets are not even read once a repeat is found.

    The box number

    The formula maps each cell to one of the nine boxes. Some values it gives:

    Cell (row, column)Box
    (0, 0)0
    (0, 8)2
    (3, 2)3
    (5, 3)4
    (4, 7)5
    (8, 8)8

    For (4, 7): 4 / 3 is 1 and 7 / 3 is 2, so the box is 1 * 3 + 2 = 5, the right box of the middle band.

    Complexity

    Time: O(81). The loops visit 81 cells, and each visit does at most three set lookups and three insertions, each constant on average. Because the board size is fixed, this is constant time; for an n x n board it would be O(n²).

    Space: O(81). The 27 sets hold at most 9 digits each, so at most 243 entries in total.

    Edge cases and common mistakes

    • Adding to the sets before checking. If you add the digit first, the check finds the digit you just added and every cell looks like a repeat.
    • Treating '.' as a digit. Without the continue, the second empty cell in a row would count as a repeated '.' and the empty board would be rejected.
    • Forgetting the box check. A board can have no repeats in any row or column and still be wrong. For example, a 5 at row 0, column 0 and another at row 1, column 1 share a box but no row or column.
    • Using the wrong box formula. r / 3 + c / 3 looks plausible but gives 2 both for (0, 8) and for (6, 2), two cells in different boxes. The row must be multiplied by 3.
    • Trying to solve the puzzle. The third example is valid but has no solution. The method only looks at the digits already placed.

    Testing

    These boards were run against the solution above. Each board is written row by row, and the dots are empty cells.

    BoardResult
    Example 1 (53..7.... and so on)true
    Example 1 with an 8 in the top-left cornerfalse
    Example 3 (12345678., one 9 at row 4, column 8)true
    A 5 at (0, 0) and another at (3, 0): same columnfalse
    A 5 at (0, 0) and another at (1, 1): same box onlyfalse
    A 5 at (0, 0) and another at (0, 2): same row and boxfalse
    A 5 at (0, 0) and another at (8, 4): nothing sharedtrue
    An empty boardtrue
    Keep going with Kotlin500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free