目录
[一、N 皇后问题(LeetCode 51・困难)](#一、N 皇后问题(LeetCode 51・困难))
[Java 代码实现(标准回溯版)](#Java 代码实现(标准回溯版))
[优化版:用 Set 记录冲突,O (1) 校验](#优化版:用 Set 记录冲突,O (1) 校验)
[二、搜索插入位置(LeetCode 35・简单)](#二、搜索插入位置(LeetCode 35・简单))
[Java 代码实现(标准二分版)](#Java 代码实现(标准二分版))
大家好,今天我们来拆解两道经典算法题:一道是回溯法的天花板级难题 ------N 皇后问题 ,另一道是二分查找的入门题搜索插入位置。前者帮你彻底吃透回溯剪枝的核心思想,后者带你入门二分查找的标准模板,非常适合作为算法学习的进阶内容~
一、N 皇后问题(LeetCode 51・困难)
题目描述
按照国际象棋的规则,皇后可以攻击与之处在同一行、同一列或同一斜线上的棋子。
n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。给你一个整数 n,返回所有不同的 n 皇后问题的解决方案。
每一种解法包含一个不同的 n 皇后问题的棋子放置方案,该方案中 'Q' 和 '.' 分别代表皇后和空位。你可以按任意顺序返回答案。
示例(n=4):
plaintext
输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:4 皇后问题存在两个不同的解法
解题思路
N 皇后问题是回溯法 + 剪枝的经典应用,核心逻辑可以拆解为 3 步:
- 逐行放置皇后:因为同一行只能放一个皇后,所以我们按行遍历,每行只放一个皇后,避免行冲突。
- 合法性校验 :对于当前要放置皇后的位置
(row, col),需要校验 3 个条件:- 同一列是否有皇后
- 左上到右下的对角线是否有皇后(
row - col相等) - 右上到左下的对角线是否有皇后(
row + col相等)
- 回溯枚举:遍历当前行的每一列,若位置合法则放置皇后,递归下一行;递归结束后回溯,撤销皇后,尝试其他列的位置。
- 终止条件 :当
row == n时,说明已经放完所有皇后,将当前棋盘加入结果集。
Java 代码实现(标准回溯版)
java
运行
import java.util.ArrayList;
import java.util.List;
public class NQueens {
List<List<String>> result = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
// 初始化棋盘:全为 '.'
char[][] board = new char[n][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
board[i][j] = '.';
}
}
// 从第0行开始回溯
backtrack(board, 0, n);
return result;
}
/**
* 回溯函数
* @param board 当前棋盘状态
* @param row 当前放置的行号
* @param n 棋盘大小
*/
private void backtrack(char[][] board, int row, int n) {
// 终止条件:所有行都放完了,将棋盘转为List<String>加入结果
if (row == n) {
result.add(boardToList(board));
return;
}
// 遍历当前行的每一列,尝试放置皇后
for (int col = 0; col < n; col++) {
// 校验当前位置是否合法
if (isValid(board, row, col, n)) {
// 做选择:放置皇后
board[row][col] = 'Q';
// 递归:放置下一行
backtrack(board, row + 1, n);
// 撤销选择:回溯,移除皇后
board[row][col] = '.';
}
}
}
/**
* 校验位置(row, col)是否可以放置皇后
*/
private boolean isValid(char[][] board, int row, int col, int n) {
// 1. 校验同一列:从第0行到当前行,是否有皇后
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> boardToList(char[][] board) {
List<String> list = new ArrayList<>();
for (char[] row : board) {
list.add(new String(row));
}
return list;
}
}
复杂度分析
- 时间复杂度:O(n!),第一行有 n 种选择,第二行最多 n-1 种,第三行最多 n-2 种,以此类推,总共有n!种可能,剪枝后实际复杂度远低于理论值。
- 空间复杂度:O(n2),棋盘占用n2空间,递归栈深度为n。
优化版:用 Set 记录冲突,O (1) 校验
上面的代码每次校验都要遍历,我们可以用 3 个 Set 分别记录已占用的列、左上 - 右下对角线、右上 - 左下对角线,实现 O (1) 时间复杂度的合法性校验,大幅提升效率:
java
运行
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class NQueensOpt {
List<List<String>> result = new ArrayList<>();
Set<Integer> colSet = new HashSet<>(); // 已占用的列
Set<Integer> diag1Set = new HashSet<>(); // 左上-右下对角线 (row - col)
Set<Integer> diag2Set = new HashSet<>(); // 右上-左下对角线 (row + col)
public List<List<String>> solveNQueens(int n) {
char[][] board = new char[n][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
board[i][j] = '.';
}
}
backtrack(board, 0, n);
return result;
}
private void backtrack(char[][] board, int row, int n) {
if (row == n) {
result.add(boardToList(board));
return;
}
for (int col = 0; col < n; col++) {
int diag1 = row - col;
int diag2 = row + col;
// O(1)校验:列、两条对角线是否被占用
if (!colSet.contains(col) && !diag1Set.contains(diag1) && !diag2Set.contains(diag2)) {
// 做选择
board[row][col] = 'Q';
colSet.add(col);
diag1Set.add(diag1);
diag2Set.add(diag2);
// 递归
backtrack(board, row + 1, n);
// 撤销选择
board[row][col] = '.';
colSet.remove(col);
diag1Set.remove(diag1);
diag2Set.remove(diag2);
}
}
}
private List<String> boardToList(char[][] board) {
List<String> list = new ArrayList<>();
for (char[] row : board) {
list.add(new String(row));
}
return list;
}
}
核心知识点总结
- 回溯法核心:逐行枚举、合法校验、回溯撤销,是解决排列组合、棋盘类问题的通用思路。
- 对角线规律 :
- 左上→右下:
row - col为定值 - 右上→左下:
row + col为定值
- 左上→右下:
- 剪枝优化:提前排除不合法的位置,避免无效递归,是回溯法的核心优化手段。
二、搜索插入位置(LeetCode 35・简单)
题目描述
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。
示例:
plaintext
输入: nums = [1,3,5,6], target = 5
输出: 2
输入: nums = [1,3,5,6], target = 2
输出: 1
解题思路
这道题是二分查找的标准入门题,核心是在有序数组中查找目标值,若不存在则返回插入位置,完全符合二分查找的适用场景:
- 初始化左右指针 :
left = 0,right = nums.length - 1 - 循环二分 :当
left <= right时,计算中间位置mid = left + (right - left) / 2(避免溢出) - 比较判断 :
- 若
nums[mid] == target:直接返回mid - 若
nums[mid] < target:目标在右半区,left = mid + 1 - 若
nums[mid] > target:目标在左半区,right = mid - 1
- 若
- 循环结束 :此时
left > right,left就是目标值的插入位置(因为循环结束时,left 指向第一个大于 target 的元素)
Java 代码实现(标准二分版)
java
运行
public class SearchInsert {
public int searchInsert(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left <= right) {
// 计算中间位置,避免(left + right)溢出
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
// 找到目标值,直接返回索引
return mid;
} else if (nums[mid] < target) {
// 目标在右半区,左指针右移
left = mid + 1;
} else {
// 目标在左半区,右指针左移
right = mid - 1;
}
}
// 循环结束,left就是插入位置
return left;
}
}
复杂度分析
- 时间复杂度:O(logn),二分查找每次将搜索范围缩小一半,时间复杂度为对数级。
- 空间复杂度:O(1),仅使用常数级额外空间。