数独类题目算是比较经典的哈希表应用场景,今天来拆解 LeetCode 36 题------有效的数独,不仅给出可直接提交的最优解法,还会讲清思路由来、代码细节,以及为什么这种解法比常规数组计数更简洁高效,适合新手入门和进阶复盘。
一、题目回顾(清晰易懂版)
题目很明确:给一个 9x9 的数独棋盘 board,判断已经填入的数字是否有效(空白格用 '.' 表示),无需判断数独是否可解,只需要满足 3 个核心规则:
-
规则1:每一行的数字 1-9,不能重复出现;
-
规则2:每一列的数字 1-9,不能重复出现;
-
规则3:每一个 3x3 的粗线宫格(共 9 个,横向、纵向各 3 个)内,数字 1-9 不能重复出现。
举个简单例子:如果某一行出现了两个 '5',或者某一列有两个 '3',又或者某个 3x3 小宫格里有两个 '7',那么这个数独就是无效的,直接返回 false 即可。
二、解题核心思路(关键突破口)
这道题的核心痛点是「如何快速判断三个维度(行、列、3x3宫格)的数字是否重复」,最直观的思路是:为每个维度维护一个「容器」,遍历每个单元格时,检查当前数字是否已经在对应维度的容器中存在------存在则无效,不存在则加入容器,继续遍历。
这里的「容器」选择很关键,常见的有两种:数组和 Set,我们对比一下:
-
数组:需要初始化固定大小(比如 9 个元素,对应数字 1-9),通过「数字转索引」的方式判断重复,优点是访问快,缺点是需要做类型转换,代码稍繁琐;
-
Set:无需考虑索引转换,直接存储数字字符串,通过 has() 方法判断重复、add() 方法添加元素,API 更简洁,代码可读性更高,且时间复杂度和数组一致(均为 O(1) 单次操作)。
最终选择用 Set 来实现,原因很简单:代码更简洁,新手更容易理解,且无性能损耗,适合面试和刷题场景。
补充一个关键细节:3x3 宫格的索引计算。9 个宫格如何用一个一维 Set 数组表示?
假设当前单元格的坐标是 (i, j)(i 是行索引,0-8;j 是列索引,0-8),那么它所在的 3x3 宫格索引可以用公式计算:Math.floor(i / 3) * 3 + Math.floor(j / 3),计算结果是 0-8,刚好对应 9 个宫格,比三维数组更简洁。
三、完整代码(可直接提交,高效简洁)
先上代码,后面逐行拆解细节,确保每一步都能看懂:
typescript
function isValidSudoku(board: string[][]): boolean {
// 1. 初始化三个维度的Set数组,分别存储每一行、每一列、每一个3x3宫格的数字
const rowSets = new Array(9).fill(0).map(() => new Set<string>());
const colSets = new Array(9).fill(0).map(() => new Set<string>());
const subboxSets = new Array(9).fill(0).map(() => new Set<string>());
// 2. 双重循环,遍历9x9棋盘的每一个单元格
for (let i = 0; i < 9; i++) {
for (let j = 0; j < 9; j++) {
const c = board[i][j];
// 3. 跳过空白格(.),无需处理
if (c === '.') continue;
// 4. 计算当前单元格所在的3x3宫格索引(0-8)
const subboxIndex = Math.floor(i / 3) * 3 + Math.floor(j / 3);
// 5. 核心判断:只要任意一个维度存在重复数字,直接返回false
if (rowSets[i].has(c) || colSets[j].has(c) || subboxSets[subboxIndex].has(c)) {
return false;
}
// 6. 若不重复,将当前数字加入对应维度的Set中
rowSets[i].add(c);
colSets[j].add(c);
subboxSets[subboxIndex].add(c);
}
}
// 7. 遍历完成,无任何重复,返回true(数独有效)
return true;
}
四、代码逐行拆解(新手必看)
很多新手看代码会觉得「大概懂,但细节不清楚」,这里逐行拆解,把每个步骤的目的讲透,避免死记硬背:
步骤1:初始化三个 Set 数组
typescript
const rowSets = new Array(9).fill(0).map(() => new Set<string>());
const colSets = new Array(9).fill(0).map(() => new Set<string>());
const subboxSets = new Array(9).fill(0).map(() => new Set<string>());
-
rowSets:长度为 9,对应数独的 9 行,每一个元素都是一个 Set,用于存储对应行已出现的数字;
-
colSets:长度为 9,对应数独的 9 列,每一个元素都是一个 Set,用于存储对应列已出现的数字;
-
subboxSets:长度为 9,对应 9 个 3x3 宫格,每一个元素都是一个 Set,用于存储对应宫格已出现的数字。
注意:用 map(() => new Set()) 而不是直接 fill(new Set()),是因为 fill 会让所有元素引用同一个 Set,导致不同行/列/宫格的数字相互干扰,这是新手常踩的坑!
步骤2:双重循环遍历棋盘
typescript
for (let i = 0; i < 9; i++) {
for (let j = 0; j < 9; j++) {
const c = board[i][j];
if (c === '.') continue;
// ... 后续操作
}
}
-
i 是行索引(0-8),j 是列索引(0-8),双重循环刚好遍历 9x9=81 个单元格;
-
c = board[i][j]:获取当前单元格的内容(要么是 '.',要么是 '1'-'9' 的字符串);
-
if (c === '.') continue:空白格无需检查,直接跳过,进入下一次循环,减少无效操作。
步骤3:计算 3x3 宫格索引
typescript
const subboxIndex = Math.floor(i / 3) * 3 + Math.floor(j / 3);
这是整个代码的「点睛之笔」,举两个例子帮大家理解:
-
当 i=0、j=0 时:Math.floor(0/3)=0,Math.floor(0/3)=0 → 0*3+0=0 → 对应第 0 个宫格;
-
当 i=2、j=5 时:Math.floor(2/3)=0,Math.floor(5/3)=1 → 0*3+1=1 → 对应第 1 个宫格;
-
当 i=5、j=7 时:Math.floor(5/3)=1,Math.floor(7/3)=2 → 1*3+2=5 → 对应第 5 个宫格;
这样计算下来,9 个宫格刚好对应 0-8 的索引,完美适配 subboxSets 数组的长度。
步骤4:核心重复判断 + 加入 Set
typescript
if (rowSets[i].has(c) || colSets[j].has(c) || subboxSets[subboxIndex].has(c)) {
return false;
}
rowSets[i].add(c);
colSets[j].add(c);
subboxSets[subboxIndex].add(c);
-
先判断:检查当前数字 c,是否已经在「当前行的 Set」「当前列的 Set」「当前宫格的 Set」中存在,只要有一个存在,说明违反规则,直接返回 false;
-
再加入:如果三个维度都没有重复,就把 c 加入到对应的三个 Set 中,供后续单元格判断使用。
步骤5:遍历完成,返回 true
typescript
return true;
如果双重循环遍历完所有单元格,都没有发现重复数字,说明这个数独是有效的,返回 true 即可。
五、性能分析(面试常问)
这道题的性能很关键,也是面试中可能被追问的点,我们从时间复杂度和空间复杂度两方面分析:
1. 时间复杂度:O(1)
很多人会误以为是 O(9x9) = O(81),但严格来说,因为数独棋盘的大小是固定的(9x9,不会随输入变化),所以时间复杂度是 O(1)(常数时间)。
补充:如果是「通用 N 阶数独」(N 可变),时间复杂度才是 O(N²),但本题明确是 9x9,所以是 O(1)。
2. 空间复杂度:O(1)
三个 Set 数组的大小都是固定的(rowSets 和 colSets 各 9 个 Set,subboxSets 也是 9 个 Set),每个 Set 最多存储 9 个元素(数字 1-9),总存储空间是固定的,不随输入变化,因此空间复杂度也是 O(1)。
六、常见坑点 & 优化建议
坑点1:初始化 Set 数组时用 fill(new Set())
错误示例:const rowSets = new Array(9).fill(new Set())
后果:9 个元素引用同一个 Set,不同行的数字会相互干扰,导致判断错误;
正确做法:用 map(() => new Set()),为每一行/列/宫格创建独立的 Set。
坑点2:空白格判断错误
题目中空白格是 '.'(英文句号),不是 '0'、' '(空格)或 'c',很多新手会不小心写错,导致代码无法通过测试。
优化建议:可替换为数组计数(兼容低版本环境)
如果环境不支持 Set(极少数情况),可以用数组计数替代,核心思路一致,只是代码稍繁琐,比如用 new Array(9).fill(0) 存储每个数字的出现次数,判断次数是否大于 1 即可。
七、总结
LeetCode 36 题的核心是「利用哈希表(Set)快速判断重复」,解题关键在于:
-
为行、列、3x3 宫格分别维护独立的「去重容器」;
-
用简洁的公式计算 3x3 宫格索引,避免三维数组的繁琐;
-
跳过空白格,减少无效操作,提升代码运行效率。
这道题难度不算高,但非常经典,是哈希表应用的入门好题,掌握这种「多维度去重」的思路,后续遇到类似的「重复判断」问题(比如矩阵中的重复元素),都能举一反三。