LeetCode 36|有效的数独:一次遍历,如何同时校验行、列和九宫格?

14 阅读3分钟

数独这道题,乍一看像是「规则判断题」,但真正写代码的时候,很多人会卡在一个问题上:

如何在遍历棋盘的同时,快速判断当前数字在“行 / 列 / 九宫格”里是否重复?

这篇笔记我会从:

  • 题目要求
  • 直觉思路 → 数据结构设计
  • 九宫格下标的推导
  • 完整代码
  • 逐行代码拆解
  • 总结

一步一步把这道题讲清楚。


一、题目要求回顾

给定一个 9 × 9 的数独棋盘 board,判断当前状态是否合法。

规则如下:

  1. 每一行,数字 1 ~ 9 不能重复
  2. 每一列,数字 1 ~ 9 不能重复
  3. 每一个 3 × 3 的九宫格中,数字 1 ~ 9 不能重复
  4. '.' 表示空格,不参与判断

注意:
题目只要求判断“当前状态是否合法”,不要求这个数独能被解出来。


二、最核心的问题:怎么“同时”判断三种约束?

如果每遇到一个数字,就:

  • 扫一遍当前行
  • 再扫一遍当前列
  • 再扫一遍九宫格

时间复杂度会变高,而且代码会非常乱。

更合理的思路是:

在遍历棋盘的过程中,用空间换时间,把“是否出现过”记录下来。


三、数据结构设计:三个二维数组

我们分别记录:

  • row[i][num]:第 i 行是否出现过数字 num
  • col[j][num]:第 j 列是否出现过数字 num
  • box[k][num]:第 k 个九宫格是否出现过数字 num
boolean[][] row = new boolean[9][9];
boolean[][] col = new boolean[9][9];
boolean[][] box = new boolean[9][9];

这里的 num 范围是 0 ~ 8,对应数字 '1' ~ '9'


四、九宫格编号是怎么来的?

这是很多人第一次写这题时最迷惑的地方。

九宫格的分布

0 | 1 | 2
--+---+--
3 | 4 | 5
--+---+--
6 | 7 | 8

推导过程

  • 每 3 行是一排宫格 → i / 3
  • 每 3 列是一列宫格 → j / 3
  • 行优先编号 → 行号 * 3 + 列号

所以九宫格编号就是:

int boxIndex = (i / 3) * 3 + j / 3;

这个公式不是记忆出来的,而是结构推导出来的


五、完整代码

class Solution {
    public boolean isValidSudoku(char[][] board) {
        boolean[][] row = new boolean[9][9];
        boolean[][] col = new boolean[9][9];
        boolean[][] box = new boolean[9][9];

        for (int i = 0; i < 9; i++) {
            for (int j = 0; j < 9; j++) {
                char c = board[i][j];

                // 1. 空格直接跳过
                if (c == '.') continue;

                // 2. 将字符 '1'~'9' 映射为 0~8
                int num = c - '1';

                // 3. 计算当前格子所在的九宫格编号
                int boxIndex = (i / 3) * 3 + j / 3;

                // 4. 如果在行 / 列 / 宫格中已出现,直接返回 false
                if (row[i][num] || col[j][num] || box[boxIndex][num]) {
                    return false;
                }

                // 5. 标记当前数字已出现
                row[i][num] = true;
                col[j][num] = true;
                box[boxIndex][num] = true;
            }
        }
        return true;
    }
}

六、逐行拆解代码在做什么

1.为什么 c == '.' 直接跳过?

空格不代表任何数字,也不参与合法性判断。

如果不跳过,c - '1' 会得到非法下标。


2. int num = c - '1' 的意义

字符 '1' ~ '9' → 映射为数组下标 0 ~ 8

这是把「数独数字」转为「数组索引」的关键一步。


3.三个判断为什么能同时做?

if (row[i][num] || col[j][num] || box[boxIndex][num])

含义非常直白:

  • 这个数字在当前行出现过?
  • 在当前列出现过?
  • 在当前九宫格出现过?

只要有一个为 true,就违反规则。


4.为什么只需要一次遍历?

因为:

  • 每个数字只会被访问一次
  • 状态一旦被标记,后续再遇到就能 O(1) 判断
  • 不需要回头、不需要额外扫描

时间复杂度是 O(81) ,空间换时间,非常稳。


七、这道题的本质

这不是一道“数独题”,而是一道:

多维约束状态记录题

关键能力是:

  • 把规则转化为「是否出现过」
  • 把二维问题映射到一维编号
  • 用布尔数组做快速判重

八、总结

  • 行、列、九宫格的约束可以并行判断
  • 九宫格编号 (i / 3) * 3 + j / 3 是结构推导结果
  • 使用 boolean[][] 能把判断降到 O(1)
  • 代码短,但逻辑非常“工程化”