AlgoMaster Logo

Valid Sudoku

mediumFrequency14 min readUpdated June 23, 2026

Understanding the Problem

We have a 9x9 Sudoku grid, and we need to check whether the current state of the board violates any of the three Sudoku rules. We are not solving the puzzle or checking if it can be solved. We only verify that no digit appears twice in the same row, the same column, or the same 3x3 sub-box.

The board may have empty cells (represented by '.'), and those are ignored. The question is whether, among the filled cells, any digit repeats inside a row, a column, or a box.

The 3x3 sub-box check needs more care than the other two. Rows and columns are indexed directly by r and c, but each box covers a 3x3 block of cells, so we need a way to map a cell at (r, c) to the box that contains it.

Key Constraints

  • board.length == 9 and board[i].length == 9 means the board is always a fixed 9x9 grid, so there are always 81 cells. The input size is a constant, which means every approach below has the same asymptotic cost. The comparison between them is about practical overhead and code clarity, not Big-O.
  • board[i][j] is a digit 1-9 or '.', so we never have to handle invalid characters. Empty cells are marked with '.'.

Approach 1: Three Separate Passes

Intuition

Validate each constraint on its own. First scan every row and confirm no digit repeats, then scan every column, then scan every 3x3 sub-box. This matches how a person checks a Sudoku board by hand: read across one row looking for a repeat, move to the next row, and later repeat the process for columns and boxes.

To detect a repeat inside a single row, we walk the row and keep a set of digits seen so far. If a digit is already in the set when we reach it, the row has a duplicate and the board is invalid. The same set-based check works for columns and boxes.

Algorithm

  1. For each of the 9 rows, walk the row with a fresh set. Skip empty cells. If a digit is already in the set, return false; otherwise add it.
  2. For each of the 9 columns, do the same with a fresh set per column.
  3. For each of the 9 sub-boxes (identified by box-row 0-2 and box-column 0-2), iterate over its 3x3 region with a fresh set and apply the same check.
  4. If no pass finds a duplicate, return true.

Example Walkthrough

Take Example 2, which differs from Example 1 only in the top-left cell: board[0][0] is '8' instead of '5', and column 0 now reads 8, 6, ., 8, 4, 7, ., ., . from top to bottom.

The row pass runs first. Row 0 is 8 3 . . 7 . . . ., with filled digits 8, 3, 7, all distinct, so the set ends as {8, 3, 7} with no repeat. Rows 1 through 8 are likewise clean, so the row pass finishes without returning false.

The column pass runs next. Column 0 is processed top to bottom:

  • r=0: digit 8, set becomes {8}.
  • r=1: digit 6, set becomes {8, 6}.
  • r=2: cell is '.', skipped.
  • r=3: digit 8. The set already contains 8, so this is a duplicate. Return false.

The board fails the column rule because two 8s share column 0, and the function returns false without examining the remaining columns or any box.

Code

The three-pass approach is correct, and since the board is fixed at 9x9 there is no performance penalty. The drawback is repetition: the same duplicate-checking logic is written three times, and the board is read three times. The next approach folds all three checks into a single pass.

Approach 2: Single Pass with Hash Sets

Intuition

Check all three constraints during one walk over the board. Maintain a set for every row, every column, and every 3x3 box (27 sets in all). For each filled cell, check whether its digit already appears in its row's set, its column's set, or its box's set. If it appears in any of them, the board is invalid; otherwise add the digit to all three.

The row and column lookups are direct, but the box check needs a way to find which box a cell belongs to. The 9 boxes form a 3x3 grid of blocks. A cell's block row is r / 3 (integer division): rows 0-2 give 0, rows 3-5 give 1, rows 6-8 give 2. Its block column is c / 3 by the same logic. To store the 27 sets in flat arrays, flatten that (blockRow, blockCol) pair into a single index with the standard row-major formula blockRow * 3 + blockCol, which is (r / 3) * 3 + (c / 3). The * 3 is the number of block columns per block row, so the formula maps the nine (blockRow, blockCol) pairs to the distinct integers 0 through 8. For example, cell (5, 7) has block row 5 / 3 = 1 and block column 7 / 3 = 2, giving box index 1 * 3 + 2 = 5.

Algorithm

  1. Create 9 sets for rows, 9 for columns, and 9 for boxes.
  2. Iterate through every cell (r, c) on the board.
  3. If the cell is empty ('.'), skip it.
  4. Compute the box index as (r / 3) * 3 + (c / 3).
  5. If the digit is already in the row set, column set, or box set, return false.
  6. Otherwise, add the digit to all three sets.
  7. If the walk finishes without a duplicate, return true.

Example Walkthrough

Use the same Example 2 board, whose first row is 8 3 . . 7 . . . . and whose column 0 reads 8, 6, ., 8, .... Here the cells are visited in row-major order, so the failure is caught at a different point than in the three-pass version.

  • (0,0) digit 8, box index 0. None of rows[0], cols[0], boxes[0] contain 8. Add it: rows[0]={8}, cols[0]={8}, boxes[0]={8}.
  • (0,1) digit 3, box index 0. Not seen. Add: rows[0]={8,3}, cols[1]={3}, boxes[0]={8,3}.
  • (0,4) digit 7, box index 1. Not seen. Add: rows[0]={8,3,7}, cols[4]={7}, boxes[1]={7}.
  • Row 0 finishes; the empty cells were skipped. Move to row 1.
  • (1,0) digit 6, box index 0. Not seen. Add: cols[0]={8,6}, boxes[0]={8,3,6}.
  • (1,3) digit 1, (1,4) digit 9, (1,5) digit 5 all added without conflict.
  • Rows 1 and 2 finish cleanly. Move to row 3.
  • (3,0) digit 8, box index 3. Check cols[0], which is {8, 6} and already contains 8. Return false.

The duplicate 8 in column 0 is detected the moment the second 8 is read, so the function returns false at cell (3,0) without scanning the rest of the board.

Code

Maintaining 27 separate sets works but takes a few lines of setup. A common variation collapses them into one set by encoding each constraint as a distinct string key, so a single container tracks rows, columns, and boxes at once.

Approach 3: Single HashSet with Composite Keys

Intuition

Three separate sets per group exist only to keep the row, column, and box namespaces from colliding. A 5 in row 0 and a 5 in column 0 are unrelated facts, so they must not be confused. Instead of using three containers, give each fact a label that makes it unique inside one set.

When we reach a cell (r, c) with digit d, it produces three facts: digit d is in row r, in column c, and in box (r/3)*3 + (c/3). Encode each as a string such as "row 0-5", "col 0-5", and "box 0-5". Because the prefix distinguishes the three namespaces and the numbers distinguish which row, column, or box, every string is unique to one fact. Add all three to a single set; if any of the three is already present, that fact has been recorded before, which means a duplicate. The logic is identical to Approach 2, with the namespace baked into the key instead of into the choice of container.

Algorithm

  1. Create one set to hold all keys.
  2. Iterate through every cell (r, c) on the board.
  3. If the cell is empty, skip it.
  4. Build three keys for the digit d: "row r-d", "col c-d", and "box (r/3)*3+(c/3)-d".
  5. If any of the three keys is already in the set, return false.
  6. Otherwise, add all three keys.
  7. If the walk finishes without a duplicate, return true.

Example Walkthrough

Use Example 2 once more. Cells are visited in row-major order, and each filled cell adds three keys.

  • (0,0) digit 8: keys row 0-8, col 0-8, box 0-8. None present. Add all three.
  • (0,1) digit 3: keys row 0-3, col 1-3, box 0-3. None present. Add all three.
  • (0,4) digit 7: keys row 0-7, col 4-7, box 1-7. None present. Add all three.
  • Row 0 finishes. Row 1: (1,0) digit 6 adds row 1-6, col 0-6, box 0-6. (1,3), (1,4), (1,5) add their keys without conflict.
  • Rows 1 and 2 finish cleanly. Row 3: (3,0) digit 8. Its keys are row 3-8, col 0-8, box 3-8. The key col 0-8 was added back at (0,0), so it is already in the set. Return false.

The shared col 0-8 key flags the repeated 8 in column 0, and the function returns false at (3,0), matching the other approaches.

Code

This packs the bookkeeping into one container, at the cost of building and hashing a string for every fact. Approach 2 avoids the string work, and the next approach removes the hashing entirely by replacing each set with a single integer used as a bitmask, turning the membership check into one bitwise operation.

Approach 4: Bitmask (Optimal Space Efficiency)

Intuition

With only nine possible digits, the set of digits seen in any row, column, or box fits in a single integer used as a bitmask. Each digit owns one bit. Setting a bit marks its digit as seen, and testing a bit checks whether the digit has appeared.

We want nine bits, one per digit, and the cheapest mapping packs them into the lowest nine bits. The character '1' should map to bit 0, '2' to bit 1, and so on through '9' to bit 8. Subtracting the character '1' from a digit character yields that 0-based offset directly: '1' - '1' = 0, '5' - '1' = 4, '9' - '1' = 8. So the mask for a digit is 1 << (digit - '1'). Subtracting '0' instead would map '1' to bit 1 and '9' to bit 9, wasting bit 0 and needing ten bits; subtracting '1' keeps the masks in bits 0 through 8.

To test whether a digit is already present, AND the group's integer with the digit's mask: a non-zero result means the bit is set and the digit is a duplicate. To record the digit, OR the mask into the integer. Both operations are single instructions, with no hashing or allocation.

Algorithm

  1. Create three arrays of 9 integers: rows, cols, and boxes, all initialized to 0.
  2. Iterate through every cell (r, c) on the board.
  3. If the cell is empty, skip it.
  4. Compute mask = 1 << (digit - '1') for this digit.
  5. Compute the box index as (r / 3) * 3 + (c / 3).
  6. If rows[r] & mask, cols[c] & mask, or boxes[boxIdx] & mask is non-zero, the digit is a duplicate. Return false.
  7. Otherwise, set the bit in all three: rows[r] |= mask, cols[c] |= mask, boxes[boxIdx] |= mask.
  8. If the walk finishes without a duplicate, return true.

Example Walkthrough

Use Example 2 again so the bitmask mechanics line up against the earlier traces. Integers are shown in binary with bit 0 on the right; recall '8' sets bit 7 (1 << ('8' - '1') = 1 << 7) and '6' sets bit 5.

  • (0,0) digit 8, mask = 1 << 7 = 10000000, box index 0. rows[0], cols[0], boxes[0] are all 0, so the AND is 0. Set the bit: rows[0] = cols[0] = boxes[0] = 10000000.
  • (0,1) digit 3, mask = 1 << 2 = 00000100, box index 0. No bit set yet in rows[0] (10000000) at that position. Set it: rows[0] = 10000100, boxes[0] = 10000100, cols[1] = 00000100.
  • (0,4) digit 7, mask = 1 << 6 = 01000000, box index 1. Not set. Update rows[0] = 11000100, cols[4], boxes[1].
  • Row 0 done. Row 1: (1,0) digit 6, mask = 1 << 5 = 00100000, box index 0. cols[0] is 10000000; AND with 00100000 is 0. Set it: cols[0] = 10100000, boxes[0] = 10100100.
  • Cells (1,3), (1,4), (1,5) set their bits without conflict. Rows 1 and 2 finish.
  • (3,0) digit 8, mask = 1 << 7 = 10000000, box index 3. Test cols[0] & mask: cols[0] is 10100000, and 10100000 & 10000000 = 10000000, which is non-zero. The bit for 8 is already set in column 0. Return false.

The result matches the previous approaches: the second 8 in column 0 fails the column rule, and the function returns false at cell (3,0).

Code