一、题目描述
判断一个 9x9 的数独是否有效。只需要根据以下规则,验证已经填入的数字是否有效即可。
- 数字
1-9在每一行只能出现一次。 - 数字
1-9在每一列只能出现一次。 - 数字
1-9在每一个以粗实线分隔的 3x3 宫内只能出现一次。
上图是一个部分填充的有效的数独。
数独部分空格内已填入了数字,空白格用 '.' 表示。
示例 1:
输入:
[ ["5","3",".",".","7",".",".",".","."],
["6",".",".","1","9","5",".",".","."],
[".","9","8",".",".",".",".","6","."],
["8",".",".",".","6",".",".",".","3"],
["4",".",".","8",".","3",".",".","1"],
["7",".",".",".","2",".",".",".","6"],
[".","6",".",".",".",".","2","8","."],
[".",".",".","4","1","9",".",".","5"],
[".",".",".",".","8",".",".","7","9"]
]
输出: true
示例 2:
输入:
[ ["8","3",".",".","7",".",".",".","."],
["6",".",".","1","9","5",".",".","."],
[".","9","8",".",".",".",".","6","."],
["8",".",".",".","6",".",".",".","3"],
["4",".",".","8",".","3",".",".","1"],
["7",".",".",".","2",".",".",".","6"],
[".","6",".",".",".",".","2","8","."],
[".",".",".","4","1","9",".",".","5"],
[".",".",".",".","8",".",".","7","9"]
]
输出: true
解释: 除了第一行的第一个数字从 5 改为 8 以外,空格内其他数字均与 示例1 相同。
但由于位于左上角的 3x3 宫内有两个 8 存在, 因此这个数独是无效的。
说明:
- 一个有效的数独(部分已被填充)不一定是可解的。
- 只需要根据以上规则,验证已经填入的数字是否有效即可。
- 给定数独序列只包含数字 1-9 和字符 '.' 。
- 给定数独永远是
9x9形式的。
二、思路分析
首先依旧是不要想太多想太全,大多数的算法题一般人是不可能一次想出全局解的。
其次由说明和图片可以知道给定的数独永远都是一个 9x9 的二维数组,所以我们可以不用去做数组的边界判断。
最后我们逐步的按照一下条件来进行判断:
- 数字
1-9在每一行只能出现一次。 - 数字
1-9在每一列只能出现一次。 - 数字
1-9在每一个以粗实线分隔的 3x3 宫内只能出现一次。
最最后我们需要如何去判断数据是否重复?
这里有很多种方法,例如:字典、集合、数组、字符串、对象。
这里我采用 集合 的方式来去判断。
第一步:遍历每一行
首先我们来判断条件一是否符合要求,那么只需要按行进行遍历即可。
// 遍历行
var isValidSudoku = function(board) {
for(let i = 0; i < 9; i++) {
for(let j = 0; j < 9; j++) {
consolo.log(board[i][j])
}
}
}
↓
// 判断一行是否有重复的数值
var isValidSudoku = function(board) {
for(let i = 0; i < 9; i++) {
const rowSet = new Set()
for(let j = 0; j < 9; j++) {
const row = board[i][j]
// 判断是否是有效数字
if(row !== '.') {
// 判断是否已经记录过了,如果记录过了直接返回 false
if(rowSet.has(row)) return false
// 没记录过的时候记录该值
rowSet.add(row)
}
}
}
}
第二步:遍历每一列
其次我们来判断条件二是否符合要求,那么只需要按行进行遍历即可。
可能有部分同学在想列我们要怎么遍历。
在二维数组中我们遍历行的时候下标变化如下:
[0][0] -> [0][1] -> [0][2] -> [0][3] -> ...
而在二维数组中我们遍历列的时候下标变化如下:
[0][0] -> [1][0] -> [2][0] -> [3][0] -> ...
细心的同学就会发现,实际上就是左下标和右下标互换了而已。
// 遍历列
var isValidSudoku = function(board) {
for(let i = 0; i < 9; i++) {
for(let j = 0; j < 9; j++) {
consolo.log(board[j][i]) // 这里相对于行的时候 i 和 j 对调了
}
}
}
↓
// 判断一列是否有重复的数值
var isValidSudoku = function(board) {
for(let i = 0; i < 9; i++) {
const columnSet = new Set()
for(let j = 0; j < 9; j++) {
const column = board[j][i]
// 判断是否是有效数字
if(column !== '.') {
// 判断是否已经记录过了,如果记录过了直接返回 false
if(columnSet.has(column)) return false
// 没记录过的时候记录该值
columnSet.add(column)
}
}
}
}
细心的同学又会发现,遍历行和遍历列其实就是同一个循环,只是下标所使用的值不一样而已,我们可以把他们合并起来。
// 遍历行和列并且判断每行和每列的数值是否重复
var isValidSudoku = function(board) {
for(let i = 0; i < 9; i++) {
const columnSet = new Set()
const rowSet = new Set()
for(let j = 0; j < 9; j++) {
const row = board[i][j]
const column = board[j][i]
// 判断是否是有效数字
if(row !== '.') {
// 判断是否已经记录过了,如果记录过了直接返回 false
if(rowSet.has(row)) return false
// 没记录过的时候记录该值
rowSet.add(row)
}
// 判断是否是有效数字
if(column !== '.') {
// 判断是否已经记录过了,如果记录过了直接返回 false
if(columnSet.has(column)) return false
// 没记录过的时候记录该值
columnSet.add(column)
}
}
}
}
到此这一题我们完成了一大半了,接下来就是最绕的一个了
第三步:遍历 3x3 宫
很多同学可能会没有头绪如何去遍历每个 3x3 宫。别急且听我细细道来。
9x9 宫刚好可以划分成 9 个 3x3 宫每个的第一位的坐标都是 3 的倍数的。
3x3 宫每个的第一位的坐标都是很有特点都是 3 的倍数
这里我们用 x 代表左下标,用 y 代表右下标。
当我们用 0 - 8 的数除以 3 取整再乘上 3 会的到 000333666 刚好符合 x 的值。
当我们用 0 - 8 的数除以 3 取余再乘上 3 会的到 036036036 刚好符合 y 的值。
然后九宫格长宽都是 3 那么我们可以使 y + 1 直到 y 可以被 3 整除的时候 x + 1 加上 y - 3 恢复 y 的坐标来遍历第二行第三行,执行九次即可遍历一个 3x3 宫。
// 遍历 3x3 宫
var isValidSudoku = function(board) {
for(let i = 0; i < 9; i++) {
let x = parseInt(i / 3) * 3
let y = i % 3 * 3
const set = new Set()
for(let k = 0; k < 9; k++) {
const n = board[x][y]
consolo.log(n)
y++
if(y % 3 === 0) {
x++
y -= 3
}
}
}
}
↓
// 判断 3x3 宫的数值
var isValidSudoku = function(board) {
for(let i = 0; i < 9; i++) {
let x = parseInt(i / 3) * 3
let y = i % 3 * 3
const set = new Set()
for(let k = 0; k < 9; k++) {
const n = board[x][y]
if(n !== '.') {
if(set.has(n)) return false
set.add(n)
}
y++
if(y % 3 === 0) {
x++
y -= 3
}
}
}
}
那么细心的同学就会发现,遍历 3x3 宫的外循环和遍历行列的一样,那么就又可以把代码进行整合,整合后就是我们最终的代码了。
三、AC 代码
var isValidSudoku = function(board) {
for(let i = 0; i < 9; i++) {
const rowSet = new Set() // 行集合
const columnSet = new Set() // 列集合
for(let j = 0; j < 9; j++) {
const row = board[i][j]
const column = board[j][i]
// 判断是否是有效数字
if(row !== '.') {
// 判断是否已经记录过了,如果记录过了直接返回 false
if(rowSet.has(row)) return false
// 没记录过的时候记录该值
rowSet.add(row)
}
// 判断是否是有效数字
if(column !== '.') {
// 判断是否已经记录过了,如果记录过了直接返回 false
if(columnSet.has(column)) return false
// 没记录过的时候记录该值
columnSet.add(column)
}
}
let x = parseInt(i / 3) * 3 // 计算 x 的初始坐标
let y = i % 3 * 3 // 计算 y 的初始坐标
const gridSet = new Set() // 格子的集合
for(let k = 0; k < 9; k++) {
const n = board[x][y]
// 判断是否是有效数字
if(n !== '.') {
// 判断是否已经记录过了,如果记录过了直接返回 false
if(gridSet.has(n)) return false
// 没记录过的时候记录该值
gridSet.add(n)
}
// y 下标进一步
y++
// 当 y 坐标进了三步的时候重置 y 坐标且 x 坐标进一步
if(y % 3 === 0) {
x++
y -= 3
}
}
}
// 等到这里就代表着符合数独
return true
}
四、总结
不要一蹴而就,要分解问题,把一个复杂的问题分解成一个又一个的小问题来解决,最终合并到一起即可。
本文正在参与「掘金 2021 春招闯关活动」, 点击查看 活动详情