题目描述
编写一个程序,通过填充空格来解决数独问题。 数独的解法需 遵循如下规则:
-
数字
1-9在每一行只能出现一次。 -
数字
1-9在每一列只能出现一次。 -
数字
1-9在每一个以粗实线分隔的3x3宫内只能出现一次。 数独部分空格内已填入了数字,空白格用'.'表示。
提示:
给定的数独序列只包含数字
1-9和字符'.'。你可以假设给定的数独只有唯一解。
给定数独永远是
9x9形式的。
思路解析
解数独 这道题,棋盘的每一个位置都要放数字,我们需要去试探每一个空位放 1-9 究竟合不合适。因此,解数独是一个二维递归 问题! 我们需要两个 for 循环遍历棋盘的行和列,然后再用一个递归函数去控制每个位置放入的数字。
1. 回溯函数返回值设计 (关键点)
注意,这是解数独问题最容易踩坑的地方:回溯函数的返回值必须是 bool 类型!
之前我们做组合问题、排列问题,找到所有满足条件的集合即可,所以返回值为 void。但数独问题只需要找到一个符合条件的解即可 (题目说了只有唯一解)。 因为只需要找到一个解,当我们到达叶子节点(也就是把所有空位填满且合法)时,就立刻返回 true,层层向上传递,停止后面的所有搜索。
2. 回溯三部曲
-
递归函数及参数 需要传入完整的棋盘
board。返回值刚才说了,是bool类型。cppbool backtracking(vector<vector<char>>& board) -
递归终止条件 其实不需要显式地写终止条件。因为我们的两个
for循环会遍历整个棋盘。如果遍历完了所有的空位,都没有返回false,说明我们成功把棋盘填满了,此时直接在函数末尾返回true即可。 -
单层搜索逻辑 外层两个
for循环遍历行和列,找到空位.。 内层再用一个for循环枚举1-9。 判断如果把这个数字放进去是合法的,那就放入该数字,并立刻进入下一层递归if (backtracking(board)) return true;。 如果递归发现不对,就回溯,把数字改回.。 如果1-9全都试过了都不行,说明当前的分支是死路,直接return false;。
3. 判断棋盘是否合法
我们需要写一个 isValid 函数,专门用来判断在 board[row][col] 放入数字 val 是否合法。 检查三个维度:
-
同行是否重复
-
同列是否重复
-
同一个 3x3 九宫格 是否重复(计算九宫格左上角起点的公式:
startRow = (row / 3) * 3)
偷一下卡哥的图

代码实现
下面提供 Python, C, C++ 三个版本的代码,核心逻辑完全一致。
C++ 版本
cpp
class Solution {
private:
bool row[9][10];
bool col[9][10];
bool block[3][3][10];
vector<pair<int, int>> spaces;
bool dfs(int pos, vector<vector<char>>& board) {
if (pos == spaces.size()) {
return true; // 填满所有空位
}
auto [i, j] = spaces[pos];
for (int digit = 1; digit <= 9; ++digit) {
if (!row[i][digit] && !col[j][digit] && !block[i / 3][j / 3][digit]) {
// 做选择
row[i][digit] = col[j][digit] = block[i / 3][j / 3][digit] = true;
board[i][j] = digit + '0';
if (dfs(pos + 1, board)) return true;
// 撤销选择
row[i][digit] = col[j][digit] = block[i / 3][j / 3][digit] = false;
}
}
return false;
}
public:
void solveSudoku(vector<vector<char>>& board) {
memset(row, false, sizeof(row));
memset(col, false, sizeof(col));
memset(block, false, sizeof(block));
spaces.clear();
for (int i = 0; i < 9; ++i) {
for (int j = 0; j < 9; ++j) {
if (board[i][j] == '.') {
spaces.emplace_back(i, j);
} else {
int digit = board[i][j] - '0';
row[i][digit] = true;
col[j][digit] = true;
block[i / 3][j / 3][digit] = true;
}
}
}
dfs(0, board);
}
};
C 语言版本
C语言在传递二维数组时使用了双重指针,逻辑与C++一致,纯底层控制:
objectivec
#include <stdbool.h>
#include <string.h>
bool row[9][10];
bool col[9][10];
bool block[3][3][10];
// 用来保存空位坐标的结构体数组
struct {
int i, j;
} spaces[81];
int space_count;
bool dfs(int pos, char** board) {
if (pos == space_count) {
return true;
}
int i = spaces[pos].i;
int j = spaces[pos].j;
for (int digit = 1; digit <= 9; ++digit) {
if (!row[i][digit] && !col[j][digit] && !block[i / 3][j / 3][digit]) {
// 选择
row[i][digit] = true;
col[j][digit] = true;
block[i / 3][j / 3][digit] = true;
board[i][j] = digit + '0';
if (dfs(pos + 1, board)) return true;
// 回溯
row[i][digit] = false;
col[j][digit] = false;
block[i / 3][j / 3][digit] = false;
}
}
return false;
}
void solveSudoku(char** board, int boardSize, int* boardColSize) {
memset(row, false, sizeof(row));
memset(col, false, sizeof(col));
memset(block, false, sizeof(block));
space_count = 0;
for (int i = 0; i < 9; ++i) {
for (int j = 0; j < 9; ++j) {
if (board[i][j] == '.') {
spaces[space_count].i = i;
spaces[space_count].j = j;
space_count++;
} else {
int digit = board[i][j] - '0';
row[i][digit] = true;
col[j][digit] = true;
block[i / 3][j / 3][digit] = true;
}
}
}
dfs(0, board);
}
Python 版本
python
class Solution:
def solveSudoku(self, board: List[List[str]]) -> None:
# 记录某行、某列、某九宫格中,数字 1-9 是否已存在
row = [[False] * 10 for _ in range(9)]
col = [[False] * 10 for _ in range(9)]
block = [[[False] * 10 for _ in range(3)] for _ in range(3)]
spaces = [] # 专门记录所有空白格的坐标
# 1. 预处理:初始化状态数组,并收集所有空位
for i in range(9):
for j in range(9):
if board[i][j] == '.':
spaces.append((i, j))
else:
digit = int(board[i][j])
row[i][digit] = True
col[j][digit] = True
block[i // 3][j // 3][digit] = True
# 2. DFS 回溯
def dfs(pos: int) -> bool:
# 如果所有的空位都填满了,说明找到了解
if pos == len(spaces):
return True
i, j = spaces[pos]
# 尝试填入 1-9
for digit in range(1, 10):
# O(1) 判断是否合法
if not row[i][digit] and not col[j][digit] and not block[i // 3][j // 3][digit]:
# 做出选择
row[i][digit] = col[j][digit] = block[i // 3][j // 3][digit] = True
board[i][j] = str(digit)
# 进入下一层递归
if dfs(pos + 1):
return True
# 撤销选择(回溯)
row[i][digit] = col[j][digit] = block[i // 3][j // 3][digit] = False
return False
dfs(0)
问题:
python
block_used = [[[False] * 10 for _ in range(3)] for _ in range(3)]啥意思呢
这行代码确实是 Python 里比较容易让人头晕的嵌套列表推导式 (List Comprehension)。别急,我们把它像剥洋葱一样,从里到外一层一层拆开来看。
它的最终目的是:创建一个 3 维(三维)数组,用来记录数独中 9 个「3x3 的九宫格」里,数字 1 到 9 是否已经出现过。
我们可以把它拆成三个步骤来理解:
第一层(最里面):[False] * 10
-
代码意思 :创建一个包含 10 个
False的一维列表。 -
长这样 :
[False, False, False, False, False, False, False, False, False, False] -
为什么是 10 个而不是 9 个?
数独虽然只有数字 1-9,但编程中数组的索引是从
0开始的。为了方便,我们直接把数组大小开到 10。这样我们就可以直接用索引1到9来代表数独里的数字 1-9,而索引0直接废弃不用,省去了每次都要减 1 的计算麻烦。 -
它的作用 :这代表某一个特定的九宫格 里,数字 1-9 的使用状态。全是
False表示现在全是空的。
第二层(中间):[ [...] for _ in range(3) ]
-
代码意思:把刚才那个长度为 10 的一维列表,循环创建 3 次,装进一个大列表里。
-
长这样(一个二维数组):
python[ [False, False, ..., False], # 代表第 0 列的九宫格 [False, False, ..., False], # 代表第 1 列的九宫格 [False, False, ..., False] # 代表第 2 列的九宫格 ] -
它的作用:数独棋盘的横向有 3 个大九宫格。这一层生成了一整行的 3 个九宫格的状态表。
第三层(最外面):[ [...] for _ in range(3) ]
-
代码意思:把刚才那一整行的状态表(二维数组),再循环创建 3 次,装进一个更大的列表里。
-
长这样(一个三维数组):
python[ # 第 0 行的 3 个九宫格 [ [10个False], [10个False], [10个False] ], # 第 1 行的 3 个九宫格 [ [10个False], [10个False], [10个False] ], # 第 2 行的 3 个九宫格 [ [10个False], [10个False], [10个False] ] ] -
它的作用:完美对应了数独棋盘的 3 * 3 个大九宫格(总共 9 个)。
💡 结合坐标怎么使用这个三维数组?
理解了它的结构,我们再看看代码里是怎么用的:block_used[i // 3][j // 3][num]
假设我们现在在棋盘的坐标 (i, j) 上,也就是第 i 行第 j 列(i 和 j 的范围都是 0 到 8),我们想填入数字 num(比如数字 7)。
-
i // 3:计算出我们当前所在的格子,属于大九宫格的哪一行 。例如i=5(第5行),5 // 3 = 1,说明属于第1行的大九宫格(中间那行)。 -
j // 3:计算出我们所在的格子,属于大九宫格的哪一列 。例如j=7(第7列),7 // 3 = 2,说明属于第2列的大九宫格(最右边那列)。 -
[num]:直接去查状态表里索引为num的位置是True还是False。
连起来 block_used[1][2][7] 的意思就是:去查数独盘面上,第 1 行、第 2 列的那个大九宫格里面,数字 7 是否已经被填过了。(C与C++同理)
复杂度分析
-
时间复杂度 : O(9^m),其中 m 是数独中空白格的数量。由于每个空白格最多有 9 种可能的数字填入,最坏情况下的分支因子为 9。但实际上由于加入了严格的合法性剪枝 (
isValid函数),实际运行时间远远小于理论上限。 -
空间复杂度: O(1),如果不考虑系统递归调用栈所消耗的空间的话。由于数独固定大小为 9x9,递归深度最大为 81,所以空间复杂度也是常数级别的。
总结
解数独是回溯算法体系里非常硬核的一道题目,它突破了我们在一维数组上回溯的惯性思维,引入了二维数组的遍历与嵌套递归。
只要牢牢把握住:用 bool 返回值来截断后续多余的搜索分支 ,并且把 检查行、列、3x3 宫格的合法性函数 写对,这道 Hard 题目其实就像套模板一样清晰!
照例贴上卡哥的代码随想录