今天刷了一道「有效数独」的算法题,题目本身并不算难,主要考察的是对数据结构的选择和下标映射的理解。
刚开始做的时候,我第一反应也是直接上 map,但写着写着就发现,其实这题的数据范围非常固定:
数独就是 9×9,数字也只有 1~9。既然边界这么明确,就没有必要再上通用的哈希表了,直接用数组来记录状态会更合适。
最后我提交通过的代码如下:
func isValidSudoku(board [][]byte) bool {
var rowMap, colMap, cellMap [9][9]bool
for r := 0; r < 9; r++ {
for c := 0; c < 9; c++ {
val := board[r][c]
if val == '.' {
continue
}
idx := val - '1'
cellLocation := r/3*3 + c/3
if rowMap[r][idx] || colMap[c][idx] || cellMap[cellLocation][idx] {
return false
}
rowMap[r][idx] = true
colMap[c][idx] = true
cellMap[cellLocation][idx] = true
}
}
return true
}
这道题的关键点
1. 数据结构的选择:数组比通用 map 更合适
一开始我也习惯性地想用 map,但这题其实完全没必要。
因为数独的规则是固定的:
- 一共有 9 行
- 一共有 9 列
- 一共有 9 个九宫格
- 每个位置只会出现数字
1~9
这种场景本质上就是一个“范围非常清晰”的状态记录问题。
既然范围都固定了,那直接用数组来保存状态就行了。
我这里用了三个二维数组:
rowMap:记录某个数字是否已经出现在某一行colMap:记录某个数字是否已经出现在某一列cellMap:记录某个数字是否已经出现在某个九宫格
这样做有两个好处:
第一,访问更直接,时间效率更高。
第二,空间也更省,不需要额外维护哈希结构。
对于算法题来说,能用数组解决的地方,尽量别先想到 map,这也是我这次刷题最大的一个收获。
2. 字符到下标的映射
数独面板里存的不是整数,而是字符,所以这里就需要做一次简单的映射。
比如:
'1'对应下标0'2'对应下标1- ...
'9'对应下标8
所以我这里用了:
idx := val - '1'
这个写法很常见,本质上就是把字符转成从 0 开始的数组下标。
这个小技巧在很多题里都能用到,尤其是涉及字符统计、频次记录的时候,非常实用。
3. 九宫格位置的计算
这题里最容易绕一下的,其实是九宫格的定位。
数独一共 9 个小宫格,如何把当前位置映射到 0~8 的九宫格编号呢?
我这里用的是:
cellLocation := r/3*3 + c/3
它的含义是:
r/3决定当前在第几行宫格c/3决定当前在第几列宫格- 再通过
r/3*3 + c/3得到最终的九宫格编号
比如:
- 左上角九宫格是
0 - 上中是
1 - 上右是
2 - 中左是
3 - ...
- 右下是
8
一开始我在这里也写得不太对,主要就是下标映射没理顺,导致逻辑容易乱。
但其实只要把九宫格想成一个 3 × 3 的块状编号系统,这个公式就很好理解了。
4. 核心逻辑其实很简单
数据结构和下标映射确定好之后,剩下的代码就非常清晰了。
整体思路就是遍历整个 9×9 矩阵:
-
如果当前位置是
.,说明这个格子为空,直接跳过 -
如果当前位置是数字,就检查:
- 这一行是否出现过
- 这一列是否出现过
- 这个九宫格是否出现过
-
只要任意一个冲突,直接返回
false -
如果全部遍历完都没有冲突,返回
true
这就是典型的“边遍历边维护状态”的写法。
思路并不复杂,关键就在于你能不能把“行、列、宫格”这三个维度同时维护好。
这道题给我的几个感受
这题虽然不难,但对我来说还是挺有价值的,主要有三个点:
第一,不要一上来就默认用 map。
算法题里很多问题的数据范围其实很小,数组往往比 map 更直接、更高效。
第二,下标映射很重要。
很多题看起来是逻辑题,实际上考的是你能不能把问题转成数组下标来处理。
第三,刷题不只是为了 AC。
像这次我虽然最后做出来了,但中间也暴露了自己在数据结构选择、九宫格定位这些细节上的不足。
把这些问题总结下来,下一次再遇到类似题型时,思路就会更快更稳。
写在最后
这篇文章主要是记录我这次刷题的一个小复盘。
一方面是想通过总结来强化自己的思维逻辑,另一方面也是借这个过程继续熟悉 Go 语言的写法和习惯。
欢迎大佬们指点,有更好的写法或者优化思路,也非常欢迎交流。