LeetCode: 36.有效的数独|刷题打卡

444 阅读6分钟

psu.jpg

一、题目描述

判断一个 9x9 的数独是否有效。只需要根据以下规则,验证已经填入的数字是否有效即可。

  1. 数字 1-9每一行只能出现一次。
  2. 数字 1-9每一列只能出现一次。
  3. 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。

image.png

上图是一个部分填充的有效的数独。

数独部分空格内已填入了数字,空白格用 '.' 表示。

示例 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. 数字 1-9每一行只能出现一次。
  2. 数字 1-9每一列只能出现一次。
  3. 数字 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 宫刚好可以划分成 93x3 宫每个的第一位的坐标都是 3 的倍数的。
image.png

3x3 宫每个的第一位的坐标都是很有特点都是 3 的倍数 image.png

这里我们用 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 春招闯关活动」, 点击查看 活动详情