LeetCode 36. 有效的数独
问题描述
判断一个 9x9
的数独是否有效。需要满足以下规则:
- 数字
1-9
在每一行只能出现一次 - 数字
1-9
在每一列只能出现一次 - 数字
1-9
在每一个3x3
宫格内只能出现一次
注意:
- 空白格用
'.'
表示 - 只需验证已填入数字,不要求数独可解
- 部分填充的数独可能有效
示例:
输入:board =
[["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
算法思路
哈希表/数组标记法:
- 数据结构:
- 创建三个二维数组(或哈希表):
rows[9][9]
:记录每行数字出现情况cols[9][9]
:记录每列数字出现情况boxes[9][9]
:记录每个宫格数字出现情况
- 创建三个二维数组(或哈希表):
- 宫格索引计算:
- 对于位置
(i, j)
,宫格索引 =(i/3)*3 + j/3
- 对于位置
- 遍历数独:
- 遍历每个单元格:
- 遇到数字时,计算其在数组中的索引(
num = char - '1'
) - 检查该数字在对应行、列、宫格中是否已存在
- 若存在则返回无效,否则标记为存在
- 遇到数字时,计算其在数组中的索引(
- 遍历每个单元格:
- 返回结果:遍历完成后返回有效
代码实现
class Solution {
public boolean isValidSudoku(char[][] board) {
// 初始化记录数组
boolean[][] rows = new boolean[9][9]; // 记录每行数字出现情况
boolean[][] cols = new boolean[9][9]; // 记录每列数字出现情况
boolean[][] boxes = new boolean[9][9]; // 记录每个宫格数字出现情况
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
char c = board[i][j];
// 跳过空白格
if (c == '.') continue;
// 将字符转换为数字索引(0-8)
int num = c - '1'; // '1'->0, '2'->1, ..., '9'->8
// 计算宫格索引(0-8)
int boxIndex = (i / 3) * 3 + j / 3;
// 检查数字是否已存在
if (rows[i][num] || cols[j][num] || boxes[boxIndex][num]) {
return false;
}
// 标记数字已出现
rows[i][num] = true;
cols[j][num] = true;
boxes[boxIndex][num] = true;
}
}
return true;
}
}
算法分析
- 时间复杂度:O(1)
- 固定遍历 81 个单元格(9x9)
- 空间复杂度:O(1)
- 使用固定大小的数组(3x9x9=243 个布尔值)
算法过程
示例输入:
- 遍历位置 (0,0):
'5'
num = '5'-'1' = 4
boxIndex = (0/3)*3 + 0/3 = 0
- 标记:
rows[0][4]=true
,cols[0][4]=true
,boxes[0][4]=true
- 遍历位置 (0,1):
'3'
num = 2
boxIndex = 0
- 标记:
rows[0][2]=true
,cols[1][2]=true
,boxes[0][2]=true
- 遍历位置 (1,0):
'6'
num = 5
boxIndex = (1/3)*3 + 0/3 = 0
- 标记:
rows[1][5]=true
,cols[0][5]=true
,boxes[0][5]=true
- 继续遍历:所有数字均无重复,返回
true
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1:标准有效数独
char[][] board1 = {
{'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'}
};
System.out.println("Test 1: " + solution.isValidSudoku(board1)); // true
// 测试用例2:行重复
char[][] board2 = {
{'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'}
};
System.out.println("Test 2: " + solution.isValidSudoku(board2)); // false
// 测试用例3:列重复
char[][] board3 = {
{'5','3','.','.','7','.','.','.','.'},
{'6','.','.','1','9','5','.','.','.'},
{'.','9','8','.','.','.','.','6','.'},
{'5','.','.','.','6','.','.','.','3'},
{'4','.','.','8','.','3','.','.','1'},
{'7','.','.','.','2','.','.','.','6'},
{'.','6','.','.','.','.','2','8','.'},
{'.','.','.','4','1','9','.','.','5'},
{'.','.','.','.','8','.','.','7','9'}
};
System.out.println("Test 3: " + solution.isValidSudoku(board3)); // false
// 测试用例4:宫格重复
char[][] board4 = {
{'5','3','.','.','7','.','.','.','.'},
{'6','5','.','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'}
};
System.out.println("Test 4: " + solution.isValidSudoku(board4)); // false
// 测试用例5:空白数独
char[][] board5 = {
{'.','.','.','.','.','.','.','.','.'},
{'.','.','.','.','.','.','.','.','.'},
{'.','.','.','.','.','.','.','.','.'},
{'.','.','.','.','.','.','.','.','.'},
{'.','.','.','.','.','.','.','.','.'},
{'.','.','.','.','.','.','.','.','.'},
{'.','.','.','.','.','.','.','.','.'},
{'.','.','.','.','.','.','.','.','.'},
{'.','.','.','.','.','.','.','.','.'}
};
System.out.println("Test 5: " + solution.isValidSudoku(board5)); // true
// 测试用例6:单宫格重复
char[][] board6 = {
{'.','.','.','.','.','.','.','.','.'},
{'.','.','.','.','.','.','.','.','.'},
{'.','.','.','.','.','.','.','.','.'},
{'.','.','.','1','.','.','.','.','.'},
{'.','.','.','.','1','.','.','.','.'}, // 两个1在同一宫格
{'.','.','.','.','.','.','.','.','.'},
{'.','.','.','.','.','.','.','.','.'},
{'.','.','.','.','.','.','.','.','.'},
{'.','.','.','.','.','.','.','.','.'}
};
System.out.println("Test 6: " + solution.isValidSudoku(board6)); // false
}
关键点
-
宫格索引计算:
boxIndex = (i/3)*3 + j/3
- 将
9x9
网格划分为 9 个3x3
宫格
-
数字索引转换:
num = char - '1'
('1'→0
,'2'→1
, …,'9'→8
)
-
高效检查:
- 使用布尔数组记录出现情况
- 一次遍历完成所有检查
-
空白格处理:
- 遇到
'.'
直接跳过
- 遇到
常见问题
-
为什么宫格索引用
(i/3)*3 + j/3
?
该公式将网格划分为9个宫格:i/3
确定行方向宫格索引(0-2)j/3
确定列方向宫格索引(0-2)(i/3)*3 + j/3
将二维宫格索引映射为一维(0-8)
-
为什么用
char - '1'
而不用char - '0'
?char - '1'
将'1'
映射为 0,'9'
映射为 8- 符合数组索引从 0 开始的特点
- 若用
char - '0'
需要额外减1操作
-
空白格会影响判断吗?
不会,代码中遇到'.'
直接跳过,只检查数字。 -
能否用 HashSet 代替数组?
可以,但数组更高效:Set<String> seen = new HashSet<>(); for (...) { if (c != '.') { String rowKey = i + "r" + num; String colKey = j + "c" + num; String boxKey = boxIndex + "b" + num; if (!seen.add(rowKey) || !seen.add(colKey) || !seen.add(boxKey)) return false; } }
-
最坏情况时间复杂度是多少?
始终为 O(1),因为网格大小固定(9x9=81 个单元格)。