LeetCode 36. 有效的数独:Set实现哈希表最优解

数独类题目算是比较经典的哈希表应用场景,今天来拆解 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)快速判断重复」,解题关键在于:

  1. 为行、列、3x3 宫格分别维护独立的「去重容器」;

  2. 用简洁的公式计算 3x3 宫格索引,避免三维数组的繁琐;

  3. 跳过空白格,减少无效操作,提升代码运行效率。

这道题难度不算高,但非常经典,是哈希表应用的入门好题,掌握这种「多维度去重」的思路,后续遇到类似的「重复判断」问题(比如矩阵中的重复元素),都能举一反三。

相关推荐
weixin_395448913 小时前
main.c_cursor_0129
前端·网络·算法
CS创新实验室4 小时前
《计算机网络》深入学:路由算法与路径选择
网络·计算机网络·算法
一条大祥脚4 小时前
ABC357 基环树dp|懒标记线段树
数据结构·算法·图论
tod1134 小时前
力扣高频 SQL 50 题阶段总结(四)
开发语言·数据库·sql·算法·leetcode
naruto_lnq4 小时前
C++中的桥接模式
开发语言·c++·算法
苦藤新鸡4 小时前
50.腐烂的橘子
数据结构·算法
想进个大厂4 小时前
代码随想录day32 动态规划01
算法·动态规划
2401_859049084 小时前
git submodule update --init --recursive无法拉取解决
前端·chrome·git
j445566114 小时前
C++中的职责链模式高级应用
开发语言·c++·算法