Go 刷题记录:有效数独,核心其实是下标映射

0 阅读4分钟

今天刷了一道「有效数独」的算法题,题目本身并不算难,主要考察的是对数据结构的选择和下标映射的理解。

刚开始做的时候,我第一反应也是直接上 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 语言的写法和习惯。

欢迎大佬们指点,有更好的写法或者优化思路,也非常欢迎交流。