一、引言
LeetCode 51 N 皇后问题是回溯算法 的绝对经典标杆题,更是面试中考察递归逻辑、剪枝思维与空间优化的核心考点。这道题的核心是破解 "如何在 N×N 棋盘上摆放 N 个皇后,使其互不攻击 ",而基础回溯遍历校验、布尔数组优化、位运算优化则是解决该问题的三大进阶思路 ------ 三者从暴力到优雅、从直观到极致,在时间 / 空间复杂度与代码抽象程度上层层递进,完美对应了算法思维从入门到高阶的成长路径。
本文会系统拆解这三种解法:先通过基础回溯法 理清最直观的 "逐行枚举 + 循环校验" 逻辑(易理解但效率偏低);再升级为布尔数组优化版 ,用 O (1) 时间完成冲突校验(可读性与性能的最佳平衡);最后揭秘位运算优化版,通过二进制状态压缩实现极致性能(面试高阶考点!),并全部给出可直接运行的 Java 代码,帮你从原理到实现彻底啃下 N 皇后。

二、N皇后规则
皇后问题里,皇后的攻击范围非常大:
- 同一行不能有两个皇后
- 同一列不能有两个皇后
- 两条对角线(左上↔右下、右上↔左下)上也不能有两个皇后
只要满足这三条,就是一个合法放置方案。
三、回溯思想
你可以把回溯理解成:
一条路走到黑,走不通就回头,换一条路再试。
放到 N 皇后里就是:
- 选择:在当前行选一列,试着放一个皇后
- 判断:检查这一列、两条对角线是否冲突
- 递归:如果不冲突,就去下一行继续放皇后
- 回溯 :
- 要么放完所有行 → 找到一组解
- 要么走到某行没位置可放 → 撤销当前皇后,回到上一行,换一列再试
3.1 遍历校验
java
// 判断在 (row, col) 放皇后是否合法
private boolean isValid(char[][] board, int row, int col) {
int n = board.length;
// 1. 检查当前列(上方所有行)
for (int i = 0; i < row; i++) {
if (board[i][col] == 'Q') {
return false;
}
}
// 2. 检查左上对角线 ↖
for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
if (board[i][j] == 'Q') {
return false;
}
}
// 3. 检查右上对角线 ↗
for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
if (board[i][j] == 'Q') {
return false;
}
}
return true;
}
行就不校验了,默认一行放一个。
3.2 布尔校验
我们可以用布尔数组替代集合 / 逐行遍历的优化版本,核心是用数组下标直接映射「列 / 对角线」的占用状态,实现 O (1) 校验。
java
// 列方向:下标 = 列号col,长度n(0~n-1共n列)
usedCol = new boolean[n];
// 45°对角线(左上→右下,row-col为定值):下标 = row - col + n - 1(偏移量避免负数),长度2n-1
usedDiag45 = new boolean[2 * n - 1];
// 135°对角线(右上→左下,row+col为定值):下标 = row + col,长度2n-1
usedDiag135 = new boolean[2 * n - 1];
|-------|------|-----|-----|
| 0 , 0 | 0 ,1 | 0,2 | 0,3 |
| 1 ,0 | 1,1 | 1,2 | 1,3 |
| 2 ,0 | 2,1 | 2,2 | 2,3 |
| 3 ,0 | 3,1 | 3,2 | 3,3 |
我们来具体看一下对角线
|-------|-------|-------|-------|
| \(3) | \(2) | \(1) | \(0) |
| \(4) | \(3) | \(2) | \(1) |
| \(5) | \(4) | \(3) | \(2) |
| \(6) | \(5) | \(4) | \(3) |
斜对角线个数(00-33)1 + (01-23,10-32) 2 + (02-13 , 20-31)2+(03,30)2 也就是2(n - 1)+1
我们再来找一下规律
由于是同一个对角线,那么一定row - col是相等的
并且对角线是沿着中间那条对称的,那我们可以干脆把中间那条的索引定位n - 1
以此类推,其他索引就是row - col + n - 1
左上-右下对角线说完了,我们再来看右上-左下对角线,其实很明显 ,同一条对角线是row + col 相等
我们再从数学的角度来理解
左上-右下对角线是y = x + b ,那么同一条对角线 y - x = b 相等(row -col or col - row)
右上-左下对角线是 y = -x + b,那么同一条对角线 y + x = b相等(row + col)
那么我们的判断条件就可以简化成下面这样了
java
!(usedCol[col] | usedDiag45[row + col] | usedDiag135[row - col + n - 1])
四、基础回溯
这里我们采用遍历校验,方便大家来理解
4.1 回溯思想
java
/**
* 回溯函数:逐行放置皇后
* @param board 当前棋盘状态
* @param row 当前处理的行号
*/
private void backtrack(char[][] board, int row) {
int n = board.length;
// 递归终止条件:所有行都放置完成
if (row == n) {
result.add(charToList(board));
return;
}
// 遍历当前行的每一列,尝试放置皇后
for (int col = 0; col < n; col++) {
// 校验当前位置是否合法
if (isValid(board, row, col)) {
// 放置皇后
board[row][col] = 'Q';
// 递归处理下一行
backtrack(board, row + 1);
// 回溯:撤销皇后
board[row][col] = '.';
}
}
}
这里我们来理解一下这里的回溯思想,看看是如何一步步成功求解的。
失败情况
放置 -> 递归进入下一行->判断能不能放-> 不能就继续判断下一列->全部都不行(递归结束,结果没有被添加到res中)-> 回溯到放置皇后的地方,撤销放置皇后 - > 重新开始放置下一个皇后
成功情况
放置 -> 递归进入下一行->判断能不能放-> 不能就继续判断下一列->寻找到可行的->继续递归直到达到最后一行放置完毕-> 将当前解放到res中存储-> 当前解应该被释放,回溯,撤销放置n皇后-> 开始重新放置下一个n皇后
4.2 完整代码
java
import java.util.*;
class Solution {
List<List<String>> result = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
// 初始化棋盘,全部为 '.'
char[][] board = new char[n][n];
for (char[] row : board) {
Arrays.fill(row, '.');
}
backtrack(board, 0);
return result;
}
/**
* 回溯函数:逐行放置皇后
* @param board 当前棋盘状态
* @param row 当前处理的行号
*/
private void backtrack(char[][] board, int row) {
int n = board.length;
// 递归终止条件:所有行都放置完成
if (row == n) {
result.add(charToList(board));
return;
}
// 遍历当前行的每一列,尝试放置皇后
for (int col = 0; col < n; col++) {
// 校验当前位置是否合法
if (isValid(board, row, col)) {
// 放置皇后
board[row][col] = 'Q';
// 递归处理下一行
backtrack(board, row + 1);
// 回溯:撤销皇后
board[row][col] = '.';
}
}
}
/**
* 校验 (row, col) 位置是否可以放置皇后
*/
private boolean isValid(char[][] board, int row, int col) {
int n = board.length;
// 1. 校验列方向(上方)
for (int i = 0; i < row; i++) {
if (board[i][col] == 'Q') {
return false;
}
}
// 2. 校验左上对角线
for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
if (board[i][j] == 'Q') {
return false;
}
}
// 3. 校验右上对角线
for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
if (board[i][j] == 'Q') {
return false;
}
}
return true;
}
/**
* 将 char[][] 棋盘转换为 List<String>
*/
private List<String> charToList(char[][] board) {
List<String> list = new ArrayList<>();
for (char[] row : board) {
list.add(new String(row));
}
return list;
}
}
4.3 复杂度分析
- 时间复杂度 :O(n!),第一行有
n种选择,第二行最多n-1种,第三行最多n-2种,以此类推,总共有 n×(n−1)×(n−2)×...×1=n! 种可能。 - 空间复杂度:O(n2),棋盘占用 n2 空间,递归栈深度为 n,总空间为 O(n2)。
五、布尔数组优化
这里呢,我们就采用布尔数组来O(1)校验
5.1 回溯思想
java
private void backtrack(char[][] board, int row) {
int n = board.length;
if (row == n) {
res.add(construct(board));
return;
}
for (int col = 0; col < n; col++) {
int d45 = row - col + n - 1;
int d135 = row + col;
// O(1) 冲突判断
if (usedCol[col] || usedDiag45[d45] || usedDiag135[d135]) continue;
// 标记占用
usedCol[col] = true;
usedDiag45[d45] = true;
usedDiag135[d135] = true;
board[row][col] = 'Q';
backtrack(board, row + 1);
// 回溯撤销
board[row][col] = '.';
usedCol[col] = false;
usedDiag45[d45] = false;
usedDiag135[d135] = false;
}
}
失败情况
放置(同时标记数组) -> 递归进入下一行->判断能不能放-> 不能就继续判断下一列->全部都不行(递归结束,结果没有被添加到res中)-> 回溯到放置皇后的地方,撤销放置皇后(将标记数组同步回溯) - > 重新开始放置下一个皇后
成功情况
放置(同时标记数组) -> 递归进入下一行->判断能不能放-> 不能就继续判断下一列->寻找到可行的->继续递归直到达到最后一行放置完毕-> 将当前解放到res中存储-> 当前解应该被释放,回溯,撤销放置n皇后(还有标记数组)-> 开始重新放置下一个n皇后
5.2 完整代码
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
// 解法2:回溯 + 布尔数组标记(O(1)校验)
public class NQueensSolution2 {
List<List<String>> res = new ArrayList<>();
boolean[] usedCol; // 标记列是否被占用
boolean[] usedDiag45; // 标记 左上->右下 对角线
boolean[] usedDiag135; // 标记 右上->左下 对角线
public List<List<String>> solveNQueens(int n) {
usedCol = new boolean[n];
usedDiag45 = new boolean[2 * n - 1];
usedDiag135 = new boolean[2 * n - 1];
char[][] board = new char[n][n];
for (int i = 0; i < n; i++) Arrays.fill(board[i], '.');
backtrack(board, 0);
return res;
}
private void backtrack(char[][] board, int row) {
int n = board.length;
if (row == n) {
res.add(construct(board));
return;
}
for (int col = 0; col < n; col++) {
int d45 = row - col + n - 1;
int d135 = row + col;
// O(1) 冲突判断
if (usedCol[col] || usedDiag45[d45] || usedDiag135[d135]) continue;
// 标记占用
usedCol[col] = true;
usedDiag45[d45] = true;
usedDiag135[d135] = true;
board[row][col] = 'Q';
backtrack(board, row + 1);
// 回溯撤销
board[row][col] = '.';
usedCol[col] = false;
usedDiag45[d45] = false;
usedDiag135[d135] = false;
}
}
private List<String> construct(char[][] board) {
List<String> path = new ArrayList<>();
for (char[] row : board) path.add(new String(row));
return path;
}
}
六、位运算
6.1 状态变量
我们不再用数组 / 集合,而是用一个 int 整型的二进制位来记录占用状态:
- 每一位二进制:
1 = 已占用,0 = 可放皇后 - 用 3 个 int 变量记录所有冲突:
col:列占用状态diag1:左上→右下对角线状态diag2:右上→左下对角线状态
- 列 :一共
n个 - 对角线 :一共
2n-1个
但位运算根本不存储 "第几条对角线" ,而是存储:这条对角线会影响下一行的哪一列。
col、diag1、diag2 三个变量 每一位的位置,都对应同一列
n=4,棋盘是 4 列:
- 二进制 第 0 位 → 第 0 列
- 二进制 第 1 位 → 第 1 列
- 二进制 第 2 位 → 第 2 列
- 二进制 第 3 位 → 第 3 列
不管是列、还是对角线,全部都用这 4 个二进制位表示。
我们设p为要放置的第n列 , p =0010(放置第一列)
那么col = col | p ,假设col = 0001(只放置了第0列),现在col | p就是0011(放置了第0列和第1列)
那么左上-右下对角线diag1 = (diag1 | p) << 1与p或计算再右移,我们来想想假设我们放在(1,1),那么你在第二行放的时候是不是(2,2)不能放了,也就是第2列不能放了,从第一列不能放到第二列不能放,是不是要右移一位。
那么左下-右上对角线也是同理,我们再(1,1)放了,在第二行(0,2)也不能放了,从第一列不能放到成为第0列不能放,是不是要左移一位,那么就是**(diag2 | p) >> 1**
6.2 位校验
那么我们现在搞清楚了这些状态变量,来看看怎么判断能不能放吧。
首先是col | diag1 | diag2 把列、两条对角线的列占用位置合并
在~(...) 按位取反,这样我们得到的就是可以用的列
但是我们知道int的位数肯定是大于等于n的,我们直接取反,高位就会全部变成1,多出了很多无效的n,所以我们应该只取最低的n位,前面的高位都应该是0
(1 << n) - 1 生成掩码
- 例 n=4 →
1<<4 = 10000→ 减 1 →01111 - 作用:只保留低 n 位,高位全部清 0
& 掩码
- 清理高位垃圾
- 最终得到:当前行所有合法可放位置
总结:
java
int available = (~(col | diag1 | diag2)) & ((1 << n) - 1);
6.3 遍历
这次我们得到的available不是一个boolean了,而是一个int,上面记录所有可以放置的n,那么按照从小到大遍历的顺序,我们应该找出col最小的那位
java
int p = available & -available;
这是位运算经典技巧:提取最右边的 1
在二进制里:一个数 取负数(-x) = 全部取反 + 1
从右边第一个 1 开始,左边全部翻转,右边全部不变
然后一 &,就只剩下最右边的 1。
6.4 回溯
接下来我们来思考一下位计算是怎么回溯的
我们已经知道available中存放的是用过的列
java
available &= available - 1;
例:available = 01010→ 减 1 → 01001→ & 后 → 01000
作用:
试过这一列了,把它从可选中去掉,继续试下一列。
6.5 回溯思想
java
/**
* 位运算回溯
* @param row 当前处理行
* @param col 列占用状态
* @param diag1 左上->右下对角线状态
* @param diag2 右上->左下对角线状态
*/
private void backtrack(char[][] board, int row, int col, int diag1, int diag2) {
int n = board.length;
// 递归终止:放完所有行
if (row == n) {
res.add(construct(board));
return;
}
// 1. 计算当前行所有可放皇后的位置(1=可放)
int available = (~(col | diag1 | diag2)) & ((1 << n) - 1);
// 2. 遍历所有可放位置(直到 available=0)
while (available != 0) {
// 3. 取出最低位 1(选一个位置放皇后)
int p = available & -available;
// 4. 计算当前列号
int c = Integer.bitCount(p - 1);
// 放皇后
board[row][c] = 'Q';
// 5. 递归下一行:更新三个状态
backtrack(board,
row + 1,
col | p, // 新列状态
(diag1 | p) << 1, // 新对角线1(左移)
(diag2 | p) >> 1 // 新对角线2(右移)
);
// 回溯撤销皇后
board[row][c] = '.';
// 6. 去掉已处理的最低位 1
available &= available - 1;
}
}
失败情况
放置(标记当前列,左上到右下对角线右移一位,左下到右上对角线左移一位) -> 递归进入下一行->判断能不能放(对列,左右对角线进行判断,并取有效低n位,获得所有可放列)-> 全部都不行(递归结束,结果没有被添加到res中)-> 回溯到放置皇后的地方,撤销放置皇后(将available中的最低1改成0,col这些不用改,因为传过去的时候都是临时变量) - > 重新开始放置下一个皇后
成功情况
放置(标记当前列,左上到右下对角线右移一位,左下到右上对角线左移一位) -> 递归进入下一行->判断能不能放(对列,左右对角线进行判断,并取有效低n位,获得所有可放列)-> 寻找到可行的->继续递归直到达到最后一行放置完毕-> 将当前解放到res中存储-> 当前解应该被释放,回溯,撤销放置n皇后(将available中的最低1改成0,col这些不用改,因为传过去的时候都是临时变量)-> 开始重新放置下一个n皇后
6.6 完整代码
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
// 解法3:位运算优化回溯(最高效、面试高阶版)
public class NQueensSolution3 {
List<List<String>> res = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
char[][] board = new char[n][n];
for (int i = 0; i < n; i++) Arrays.fill(board[i], '.');
// 初始状态:col=0, diag1=0, diag2=0 都没占用
backtrack(board, 0, 0, 0, 0);
return res;
}
/**
* 位运算回溯
* @param row 当前处理行
* @param col 列占用状态
* @param diag1 左上->右下对角线状态
* @param diag2 右上->左下对角线状态
*/
private void backtrack(char[][] board, int row, int col, int diag1, int diag2) {
int n = board.length;
// 递归终止:放完所有行
if (row == n) {
res.add(construct(board));
return;
}
// 1. 计算当前行所有可放皇后的位置(1=可放)
int available = (~(col | diag1 | diag2)) & ((1 << n) - 1);
// 2. 遍历所有可放位置(直到 available=0)
while (available != 0) {
// 3. 取出最低位 1(选一个位置放皇后)
int p = available & -available;
// 4. 计算当前列号
int c = Integer.bitCount(p - 1);
// 放皇后
board[row][c] = 'Q';
// 5. 递归下一行:更新三个状态
backtrack(board,
row + 1,
col | p, // 新列状态
(diag1 | p) << 1, // 新对角线1(左移)
(diag2 | p) >> 1 // 新对角线2(右移)
);
// 回溯撤销皇后
board[row][c] = '.';
// 6. 去掉已处理的最低位 1
available &= available - 1;
}
}
private List<String> construct(char[][] board) {
List<String> path = new ArrayList<>();
for (char[] row : board) path.add(new String(row));
return path;
}
}
七、总结
| 方案 | 校验速度 | 空间 | 难度 | 适用场景 |
|---|---|---|---|---|
| 基础遍历校验 | O(n) | 高 | 入门 | 理解回溯 |
| 布尔数组优化 | O(1) | 中 | 简单 | 工程 / 面试首选 |
| 位运算优化 | O (1) 指令级 | 极低 | 高阶 | 最优解、冲排名 |