LeetCode JavaScript 回溯算法 10 大经典案例
目录
1. 全排列
题目描述
给定一个不含重复数字的数组 nums
,返回其所有可能的全排列。
解题思路
使用回溯算法,通过交换元素位置来生成所有排列。
JavaScript 实现
js
function permute(nums) {
const result = [];
function backtrack(start) {
// 递归终止条件
if (start === nums.length) {
result.push([...nums]);
return;
}
// 尝试每个位置的元素
for (let i = start; i < nums.length; i++) {
// 交换元素
[nums[start], nums[i]] = [nums[i], nums[start]];
// 递归处理下一个位置
backtrack(start + 1);
// 回溯:恢复交换前的状态
[nums[start], nums[i]] = [nums[i], nums[start]];
}
}
backtrack(0);
return result;
}
// 示例
console.log(permute([1, 2, 3]));
// 输出: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
2. 组合总和
题目描述
给定一个无重复元素的数组 candidates
和一个目标数 target
,找出 candidates
中所有可以使数字和为 target
的组合。
解题思路
使用回溯算法,通过递归尝试每个数字,注意避免重复组合。
JavaScript 实现
js
function combinationSum(candidates, target) {
const result = [];
function backtrack(start, currentCombination, remainingTarget) {
// 递归终止条件
if (remainingTarget === 0) {
result.push([...currentCombination]);
return;
}
if (remainingTarget < 0) {
return;
}
// 从start开始遍历,避免重复
for (let i = start; i < candidates.length; i++) {
currentCombination.push(candidates[i]);
// 可以重复使用,所以传入i而不是i+1
backtrack(i, currentCombination, remainingTarget - candidates[i]);
currentCombination.pop(); // 回溯
}
}
backtrack(0, [], target);
return result;
}
// 示例
console.log(combinationSum([2, 3, 6, 7], 7));
// 输出: [[2,2,3],[7]]
3. 子集
题目描述
给定一个整数数组 nums
,返回该数组所有可能的子集(幂集)。
解题思路
对每个元素都有选择或不选择两种状态,使用回溯算法生成所有组合。
JavaScript 实现
js
function subsets(nums) {
const result = [];
function backtrack(start, currentSubset) {
// 每个节点都是一个解
result.push([...currentSubset]);
// 从start开始遍历
for (let i = start; i < nums.length; i++) {
currentSubset.push(nums[i]);
backtrack(i + 1, currentSubset);
currentSubset.pop(); // 回溯
}
}
backtrack(0, []);
return result;
}
// 示例
console.log(subsets([1, 2, 3]));
// 输出: [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
4. 括号生成
题目描述
数字 n
代表生成括号的对数,请你设计一个函数,用于生成所有可能的并且有效的括号组合。
解题思路
使用回溯算法,维护左括号和右括号的数量,确保括号有效性。
JavaScript 实现
js
function generateParenthesis(n) {
const result = [];
function backtrack(current, leftCount, rightCount) {
// 递归终止条件
if (current.length === 2 * n) {
result.push(current);
return;
}
// 添加左括号的条件
if (leftCount < n) {
backtrack(current + '(', leftCount + 1, rightCount);
}
// 添加右括号的条件
if (rightCount < leftCount) {
backtrack(current + ')', leftCount, rightCount + 1);
}
}
backtrack('', 0, 0);
return result;
}
// 示例
console.log(generateParenthesis(3));
// 输出: ["((()))","(()())","(())()","()(())","()()()"]
u5. N皇后
题目描述
n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
解题思路
逐行放置皇后,使用回溯算法检查每种放置方案的有效性。
JavaScript 实现
js
function solveNQueens(n) {
const result = [];
const board = new Array(n).fill().map(() => new Array(n).fill('.'));
function isValid(row, col) {
// 检查列
for (let i = 0; i < row; i++) {
if (board[i][col] === 'Q') return false;
}
// 检查左上对角线
for (let i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
if (board[i][j] === 'Q') return false;
}
// 检查右上对角线
for (let i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
if (board[i][j] === 'Q') return false;
}
return true;
}
function backtrack(row) {
if (row === n) {
// 转换为字符串数组
result.push(board.map(row => row.join('')));
return;
}
for (let col = 0; col < n; col++) {
if (isValid(row, col)) {
board[row][col] = 'Q';
backtrack(row + 1);
board[row][col] = '.'; // 回溯
}
}
}
backtrack(0);
return result;
}
// 示例
console.log(solveNQueens(4));
6. 单词搜索
题目描述
给定一个二维网格和一个单词,找出该单词是否存在于网格中。
解题思路
对每个位置进行深度优先搜索,使用回溯算法标记已访问的格子。
JavaScript 实现
js
function exist(board, word) {
if (!board || !board.length || !word) return false;
const rows = board.length;
const cols = board[0].length;
function dfs(row, col, index) {
// 边界检查
if (row < 0 || row >= rows || col < 0 || col >= cols) {
return false;
}
// 字符不匹配
if (board[row][col] !== word[index]) {
return false;
}
// 找到完整单词
if (index === word.length - 1) {
return true;
}
// 标记当前格子为已访问
const temp = board[row][col];
board[row][col] = '#';
// 四个方向搜索
const found = dfs(row + 1, col, index + 1) ||
dfs(row - 1, col, index + 1) ||
dfs(row, col + 1, index + 1) ||
dfs(row, col - 1, index + 1);
// 回溯:恢复原字符
board[row][col] = temp;
return found;
}
// 从每个位置开始搜索
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
if (dfs(i, j, 0)) {
return true;
}
}
}
return false;
}
// 示例
const board = [
['A','B','C','E'],
['S','F','C','S'],
['A','D','E','E']
];
console.log(exist(board, "ABCCED")); // true
7. 路径总和
题目描述
给定一个二叉树和一个目标和,判断该树中是否存在从根节点到叶子节点的路径,使得路径上所有节点值之和等于目标和。
解题思路
使用深度优先搜索,回溯算法记录路径和。
JavaScript 实现
js
// 定义二叉树节点
class TreeNode {
constructor(val, left = null, right = null) {
this.val = val;
this.left = left;
this.right = right;
}
}
function hasPathSum(root, targetSum) {
if (!root) return false;
function dfs(node, currentSum) {
if (!node) return false;
// 更新当前路径和
currentSum += node.val;
// 到达叶子节点
if (!node.left && !node.right) {
return currentSum === targetSum;
}
// 递归左右子树
return dfs(node.left, currentSum) || dfs(node.right, currentSum);
}
return dfs(root, 0);
}
// 示例
const root = new TreeNode(5);
root.left = new TreeNode(4);
root.right = new TreeNode(8);
root.left.left = new TreeNode(11);
root.left.left.left = new TreeNode(7);
root.left.left.right = new TreeNode(2);
console.log(hasPathSum(root, 22)); // true
8. 分割回文串
题目描述
给定一个字符串 s
,将 s
分割成一些子串,使每个子串都是回文串。
解题思路
使用回溯算法,先判断当前子串是否为回文串,然后继续分割剩余部分。
JavaScript 实现
js
function partition(s) {
const result = [];
function isPalindrome(str) {
let left = 0, right = str.length - 1;
while (left < right) {
if (str[left] !== str[right]) return false;
left++;
right--;
}
return true;
}
function backtrack(start, currentPartition) {
// 递归终止条件
if (start === s.length) {
result.push([...currentPartition]);
return;
}
// 尝试所有可能的分割点
for (let i = start; i < s.length; i++) {
const substring = s.substring(start, i + 1);
// 如果是回文串,则继续分割
if (isPalindrome(substring)) {
currentPartition.push(substring);
backtrack(i + 1, currentPartition);
currentPartition.pop(); // 回溯
}
}
}
backtrack(0, []);
return result;
}
// 示例
console.log(partition("aab"));
// 输出: [["a","a","b"],["aa","b"]]
9. 解数独
题目描述
编写一个程序,通过填充空格来解决数独问题。
解题思路
使用回溯算法,对每个空格尝试1-9的数字,检查是否满足数独规则。
JavaScript 实现
js
function solveSudoku(board) {
function isValid(board, row, col, num) {
// 检查行
for (let i = 0; i < 9; i++) {
if (board[row][i] === num) return false;
}
// 检查列
for (let i = 0; i < 9; i++) {
if (board[i][col] === num) return false;
}
// 检查3x3宫格
const boxRow = Math.floor(row / 3) * 3;
const boxCol = Math.floor(col / 3) * 3;
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (board[boxRow + i][boxCol + j] === num) return false;
}
}
return true;
}
function backtrack() {
for (let row = 0; row < 9; row++) {
for (let col = 0; col < 9; col++) {
if (board[row][col] === '.') {
for (let num = 1; num <= 9; num++) {
const charNum = num.toString();
if (isValid(board, row, col, charNum)) {
board[row][col] = charNum;
if (backtrack()) return true;
board[row][col] = '.'; // 回溯
}
}
return false;
}
}
}
return true;
}
backtrack();
return board;
}
10. 电话号码的字母组合
题目描述
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
解题思路
使用回溯算法,将每个数字对应的字母依次组合。
JavaScript 实现
js
function letterCombinations(digits) {
if (!digits) return [];
const phoneMap = {
'2': 'abc',
'3': 'def',
'4': 'ghi',
'5': 'jkl',
'6': 'mno',
'7': 'pqrs',
'8': 'tuv',
'9': 'wxyz'
};
const result = [];
function backtrack(index, currentCombination) {
// 递归终止条件
if (index === digits.length) {
result.push(currentCombination);
return;
}
const digit = digits[index];
const letters = phoneMap[digit];
for (let i = 0; i < letters.length; i++) {
backtrack(index + 1, currentCombination + letters[i]);
}
}
backtrack(0, '');
return result;
}
// 示例
console.log(letterCombinations("23"));
// 输出: ["ad","ae","af","bd","be","bf","cd","ce","cf"]
小结
回溯算法的核心思想是:
- 选择:在每一步做出选择
- 约束:检查选择是否符合约束条件
- 目标:达到目标状态时记录结果
- 回溯:撤销选择,尝试其他可能性