新的模块,回溯算法解题套路框架
1 题目
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
示例 1:
输入:n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
示例 2:
输入:n = 1, k = 1
输出:[[1]]
提示:
1 <= n <= 201 <= k <= n
2 代码实现
c++
cpp
class Solution {
private:
vector<vector<int>> res ;
vector<int> path ;
void backtracking(int n , int k , int start){
if (path.size() == k ){
res.push_back(path);
return ;
}
for (int i = start ; i<= n - (k - path.size()) + 1 ;++i ){
path.push_back(i);
backtracking(n, k , i + 1 );
path.pop_back();
}
}
public:
vector<vector<int>> combine(int n, int k) {
res.clear();
path.clear();
backtracking(n, k , 1);
return res ;
}
};
js
javascript
/**
* @param {number} n
* @param {number} k
* @return {number[][]}
*/
var combine = function(n, k) {
const res = [];
const path = [];
const backtracking = (n,k,start) => {
if (path.length == k ){
res.push([...path]);
return ;
}
for (let i = start ; i <= n - (k - path.length) + 1 ; i ++){
path.push(i);
backtracking(n,k,i+1);
path.pop();
}
};
backtracking(n,k,1);
return res ;
};
思考
实际上就是dfs?啥玩意啊一点不知道怎么写。
题解
回溯算法解决组合问题(77. 组合)
首先,我会先帮你理解回溯算法的核心思路,再一步步实现代码,确保你能看懂每一个细节。
一、核心思路:回溯算法的本质
组合问题的核心是穷举所有符合条件的子集,回溯算法(本质是深度优先搜索 DFS)是解决这类问题的标准方法。回溯算法的核心框架可以总结为:
bash
路径:已经选好的数字(比如选了1,路径就是[1])
选择列表:当前可以选择的数字(比如选了1后,选择列表是[2,3,4])
结束条件:路径长度等于k(选够k个数)
对于本题:
- 从数字 1 开始,逐个选择数字加入路径;
- 当路径长度等于 k 时,把路径加入结果集;
- 选完一个数字后,回溯(撤销选择),继续尝试下一个数字;
- 为了避免重复组合(比如 [1,2] 和 [2,1]),选择数字时只往后选(比如选了 1 之后只选 2/3/4,选了 2 之后只选 3/4)。
二、完整 C++ 代码实现
cpp
#include <vector>
using namespace std;
class Solution {
private:
// 结果集:存储所有符合条件的组合
vector<vector<int>> result;
// 路径:存储当前正在构建的组合
vector<int> path;
// 回溯函数
// start:当前选择的起始数字(避免重复组合)
// n:数字范围上限
// k:组合的长度
void backtracking(int n, int k, int start) {
// 结束条件:路径长度等于k,找到一个有效组合
if (path.size() == k) {
result.push_back(path); // 把当前路径加入结果集
return; // 回溯,不再继续递归
}
// 遍历选择列表:从start到n的所有数字
// 剪枝优化:剩余可选数字数 >= 需要选的数字数(避免无效遍历)
// 需要选的数字数 = k - path.size()
// 所以i的上限是 n - (k - path.size()) + 1
for (int i = start; i <= n - (k - path.size()) + 1; ++i) {
path.push_back(i); // 做出选择:把i加入路径
backtracking(n, k, i + 1); // 递归:下一层从i+1开始选(避免重复)
path.pop_back(); // 撤销选择(回溯):把i从路径中移除
}
}
public:
vector<vector<int>> combine(int n, int k) {
// 初始化(清空之前的结果,避免多次调用时的残留)
result.clear();
path.clear();
// 调用回溯函数,从数字1开始选
backtracking(n, k, 1);
return result;
}
};
// 测试代码(可以直接运行)
#include <iostream>
void printResult(vector<vector<int>>& res) {
cout << "[" << endl;
for (auto& vec : res) {
cout << " [";
for (int i = 0; i < vec.size(); ++i) {
cout << vec[i];
if (i != vec.size() - 1) cout << ",";
}
cout << "]," << endl;
}
cout << "]" << endl;
}
int main() {
Solution s;
// 测试示例1:n=4, k=2
vector<vector<int>> res1 = s.combine(4, 2);
cout << "示例1输出:" << endl;
printResult(res1);
// 测试示例2:n=1, k=1
vector<vector<int>> res2 = s.combine(1, 1);
cout << "示例2输出:" << endl;
printResult(res2);
return 0;
}
三、代码详细解释
1. 核心变量
result:全局私有变量,存储最终所有符合条件的组合;path:全局私有变量,存储当前正在构建的组合(回溯的 "路径")。
2. 回溯函数 backtracking
- 参数 :
n:数字范围上限(比如 4);k:组合的长度(比如 2);start:当前选择的起始数字(核心!避免重复组合,比如选了 1 之后只能选 2/3/4)。
- 结束条件 :
path.size() == k,说明已经选够 k 个数,把当前路径加入结果集,然后返回。 - 遍历逻辑 :
- 循环从
start开始,而不是从 1 开始,避免重复组合(比如 [1,2] 和 [2,1]); - 剪枝优化 :
i <= n - (k - path.size()) + 1,比如 n=4、k=2,当 path.size ()=0 时,i 最多到 3(因为选了 3 之后还能选 4,选 4 之后没有下一个数了),可以减少无效遍历。
- 循环从
- 回溯操作 :
path.push_back(i):做出选择,把当前数字加入路径;backtracking(n, k, i+1):递归下一层,下一层从 i+1 开始选(避免重复选同一个数);path.pop_back():撤销选择(回溯的核心),回到上一层继续尝试其他数字。
3. 主函数 combine
- 先清空
result和path(避免多次调用时的残留); - 调用回溯函数,从数字 1 开始选;
- 返回最终结果。
4. 测试代码
printResult:格式化输出结果,方便验证;main函数:测试示例 1 和示例 2,输出和题目要求一致。
四、执行过程(以 n=4, k=2 为例)
- 初始调用
backtracking(4,2,1),path 为空; - 第一次循环 i=1:
- path 变成 [1],调用
backtracking(4,2,2); - 内层循环 i=2:path 变成 [1,2],长度等于 2,加入结果集,返回;
- 撤销选择,path 变回 [1];
- 内层循环 i=3:path 变成 [1,3],加入结果集,返回;
- 撤销选择,path 变回 [1];
- 内层循环 i=4:path 变成 [1,4],加入结果集,返回;
- 撤销选择,path 变回空;
- path 变成 [1],调用
- 第二次循环 i=2:
- path 变成 [2],调用
backtracking(4,2,3); - 内层循环 i=3:path 变成 [2,3],加入结果集,返回;
- 撤销选择,path 变回 [2];
- 内层循环 i=4:path 变成 [2,4],加入结果集,返回;
- 撤销选择,path 变回空;
- path 变成 [2],调用
- 第三次循环 i=3:
- path 变成 [3],调用
backtracking(4,2,4); - 内层循环 i=4:path 变成 [3,4],加入结果集,返回;
- 撤销选择,path 变回空;
- path 变成 [3],调用
- 最终结果就是题目示例 1 的输出。
总结
- 回溯算法解决组合问题的核心是:路径(已选数字)+ 选择列表(可选数字)+ 结束条件(选够 k 个数);
- 关键优化点:通过
start参数避免重复组合,通过剪枝减少无效遍历; - 回溯的核心操作:做出选择 → 递归 → 撤销选择,这是所有回溯问题的通用框架。
这个框架可以套用到所有组合、排列、子集类的回溯问题中,掌握后就能解决大部分同类题目。
js版本写不下去了
我怎么觉得栈更好写,而且有关对象的api我不是很熟悉,我不知道怎么清除,箭头函数也不会写。。
javascript
/**
* @param {number} n
* @param {number} k
* @return {number[][]}
*/
var combine = function(n, k) {
const backtracking (n,k,start) => {
const res = {};
const path ={};
if (path.length === k ){
res.push(path);
}
for (let i = start ; i < n - (k - path.length) + 1 ;i++){
path.push(i);
backtracking(n,k,start + 1 );
path.
}
}
};
我先帮你修正 JS 版本的代码,同时解答你 "用栈更好" 的疑问 ------ 其实回溯算法本质上就是利用函数调用栈实现的,你感觉用栈更顺是完全没问题的,核心逻辑是相通的。
一、先修正你的代码(完整可运行版)
你的代码存在几个关键问题:变量作用域错误、箭头函数语法错误、回溯递归参数错误、撤销选择(pop)没写完、res/path 应该用数组而非对象,我会逐行修正并详细注释:
javascript
运行
/**
* @param {number} n
* @param {number} k
* @return {number[][]}
*/
var combine = function(n, k) {
// 1. 结果集(存储所有符合条件的组合):必须定义在backtracking外部,否则每次递归都会重置
const res = [];
// 2. 路径(当前正在构建的组合):同理,定义在外部
const path = [];
// 3. 修正箭头函数语法 + 补全回溯逻辑
const backtracking = (n, k, start) => {
// 结束条件:路径长度等于k,找到有效组合
if (path.length === k) {
// 注意:不能直接push(path),因为path是引用类型,后续修改会影响已存入的结果
// 必须创建副本([...path])
res.push([...path]);
return; // 终止当前递归,开始回溯
}
// 4. 修正循环条件(你少写了=,导致边界错误)+ 剪枝优化
// i <= n - (k - path.length) + 1 是剪枝,避免无效遍历
for (let i = start; i <= n - (k - path.length) + 1; i++) {
path.push(i); // 做出选择:把当前数字加入路径(入栈)
// 5. 修正递归参数:应该是i+1,而非start+1(否则会重复选前面的数)
backtracking(n, k, i + 1);
path.pop(); // 撤销选择:从路径移除最后一个数(出栈)
}
};
// 调用回溯函数,起始数字为1
backtracking(n, k, 1);
return res;
};
// 测试用例
console.log(combine(4, 2)); // 输出:[[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]]
console.log(combine(1, 1)); // 输出:[[1]]
二、关于 "用栈更好" 的解答
你觉得用栈更顺,其实是完全正确的 ------回溯算法的底层就是栈(函数调用栈) ,我们写的path.push()和path.pop()本质就是栈的 "入栈" 和 "出栈" 操作:
1. 用栈的视角理解回溯过程(以 n=4, k=2 为例)
表格
| 操作 | path(栈) | 说明 |
|---|---|---|
| push(1) | [1] | 选第一个数 1 |
| push(2) | [1,2] | 选第二个数 2,长度够 2,存入结果 |
| pop() | [1] | 撤销选 2,回到 [1] |
| push(3) | [1,3] | 选第二个数 3,存入结果 |
| pop() | [1] | 撤销选 3,回到 [1] |
| push(4) | [1,4] | 选第二个数 4,存入结果 |
| pop() | [] | 撤销选 1,回到空栈 |
| push(2) | [2] | 选第一个数 2 |
| ... | ... | 后续逻辑同理 |
2. 如果你想显式用栈实现(非递归版)
如果你不想用递归(隐式栈),也可以手动用栈实现回溯,逻辑和递归版完全一致,供你参考:
javascript
运行
var combine = function(n, k) {
const res = [];
// 栈元素格式:[当前起始数字, 当前路径]
const stack = [[1, []]];
while (stack.length > 0) {
const [start, path] = stack.pop(); // 出栈(回溯)
// 终止条件:路径长度等于k
if (path.length === k) {
res.push(path);
continue;
}
// 剪枝优化:i的上限
const max = n - (k - path.length) + 1;
// 注意:这里要倒序入栈,保证结果顺序和递归版一致(可选)
for (let i = max; i >= start; i--) {
// 入栈:新路径是当前路径+当前数字,下一轮起始数字是i+1
stack.push([i + 1, [...path, i]]);
}
}
return res;
};
// 测试
console.log(combine(4, 2)); // 输出和递归版一致
三、你原代码的核心错误总结
- 变量作用域 :
res和path定义在backtracking内部,导致每次递归都会重置,无法累积结果; - 语法错误 :箭头函数
const backtracking (n,k,start) => {}少写了=,正确是const backtracking = (n,k,start) => {}; - 递归参数错误 :
backtracking(n,k,start + 1)应该是i+1,否则会重复选 1、2、3...(比如选了 1 之后,下一轮还是从 2 开始,而非 1+1=2,但如果 start 不变会一直循环); - 引用类型陷阱 :
res.push(path)直接 push 引用,后续path.pop()会修改已存入的结果,必须用[...path]创建副本; - 循环条件错误 :
i < n - (k - path.length) + 1少写了=,导致边界数字(比如 4)无法被选中; - 撤销选择未完成 :
path.后面没写pop(),导致路径无法回溯。
总结
- 回溯算法的核心就是栈操作(push 入栈选数,pop 出栈撤销选数),你觉得用栈更顺是完全正确的;
- JS 版回溯的关键:
res和path要定义在回溯函数外部,递归参数用i+1而非start+1,存入结果时要创建路径副本; - 两种实现方式:递归版(隐式利用函数调用栈)更简洁,非递归版(显式栈)更直观体现栈的逻辑,可根据习惯选择。
3 题目
中等
相关标签
相关企业
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

示例 1:
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
示例 2:
输入:digits = "2"
输出:["a","b","c"]
提示:
1 <= digits.length <= 4digits[i]是范围['2', '9']的一个数字。
4 代码实现
c++
cpp
class Solution {
private:
const string letterMap[10] = {
"",
"",
"abc",
"def",
"ghi",
"jkl",
"mno",
"pqrs",
"tuv",
"wxyz"
};
vector<string> res ;
string path ;
void backtracking (const string &digits,int index){
if (index == digits.size()){
res.push_back(path);
return ;
}
int digit = digits[index] - '0';
string letters = letterMap[digit];
for (int i = 0 ; i < letters.size();++i){
path.push_back(letters[i]);
backtracking(digits,index + 1);
path.pop_back();
}
}
public:
vector<string> letterCombinations(string digits) {
if (digits.empty()){
return {};
}
res.clear();
path.clear();
backtracking(digits,0);
return res ;
}
};
js
javascript
/**
* @param {string} digits
* @return {string[]}
*/
var letterCombinations = function(digits) {
if (digits.length === 0) return [];
const letterMap = [
"",
"",
"abc",
"def",
"ghi",
"jkl",
"mno",
"pqrs",
"tuv",
"wxyz"
];
const result = [];
let path = "";
const backtracking = (digits, index) => {
if (index === digits.length) {
result.push(path);
return;
}
const digit = digits[index] - '0';
const letters = letterMap[digit];
for (let i = 0; i < letters.length; i++) {
path += letters[i];
backtracking(digits, index + 1);
path = path.slice(0, -1);
}
};
backtracking(digits, 0);
return result;
};
思考
用数组存表,按照下标遍历回溯?啊,不知道怎么写....
题解
回溯算法解决电话号码的字母组合(17. 电话号码的字母组合)
这道题是回溯算法的经典应用,核心思路和组合问题一致,但需要先建立数字→字母的映射关系,再通过回溯遍历所有可能的字母组合。我会从思路分析到代码实现,一步步讲清楚,确保你能理解每一个细节。
一、核心思路分析
1. 先明确数字与字母的映射关系
电话按键的映射可以用数组 / 哈希表存储,数组更高效(下标对应数字):
表格
| 数字 | 字母 | 数组下标 |
|---|---|---|
| 2 | abc | 2 |
| 3 | def | 3 |
| 4 | ghi | 4 |
| 5 | jkl | 5 |
| 6 | mno | 6 |
| 7 | pqrs | 7 |
| 8 | tuv | 8 |
| 9 | wxyz | 9 |
2. 回溯算法的核心框架
- 路径:当前已经拼接好的字母串(比如选了 2 的 a,3 的 d,路径就是 "ad");
- 选择列表:当前数字对应的所有字母(比如处理数字 3 时,选择列表是 d/e/f);
- 结束条件:路径长度等于输入数字串的长度(比如输入 "23",路径长度为 2 时,就是一个完整组合);
- 遍历逻辑:按顺序处理每个数字,对每个数字的字母做选择→递归→撤销选择。
二、完整 C++ 代码实现
cpp
#include <vector>
#include <string>
using namespace std;
class Solution {
private:
// 1. 数字到字母的映射表(下标对应数字,比如map[2] = "abc")
const string letterMap[10] = {
"", // 0(无对应)
"", // 1(无对应)
"abc", // 2
"def", // 3
"ghi", // 4
"jkl", // 5
"mno", // 6
"pqrs", // 7
"tuv", // 8
"wxyz" // 9
};
// 2. 结果集:存储所有符合条件的字母组合
vector<string> result;
// 3. 路径:存储当前拼接的字母串
string path;
// 回溯函数
// digits:输入的数字串
// index:当前处理到digits的第几个数字(核心:按顺序处理每个数字)
void backtracking(const string& digits, int index) {
// 结束条件:处理完所有数字(路径长度=数字串长度)
if (index == digits.size()) {
result.push_back(path); // 加入结果集
return; // 回溯,终止当前递归
}
// 4. 获取当前处理的数字(char转int)
int digit = digits[index] - '0';
// 5. 获取当前数字对应的字母串
string letters = letterMap[digit];
// 6. 遍历当前数字的所有字母
for (int i = 0; i < letters.size(); ++i) {
path.push_back(letters[i]); // 做出选择:拼接当前字母
backtracking(digits, index + 1); // 递归:处理下一个数字
path.pop_back(); // 撤销选择:删除最后一个字母(回溯)
}
}
public:
vector<string> letterCombinations(string digits) {
// 边界条件:空字符串直接返回空结果
if (digits.empty()) {
return {};
}
// 初始化(避免多次调用残留)
result.clear();
path.clear();
// 调用回溯函数,从第0个数字开始处理
backtracking(digits, 0);
return result;
}
};
// 测试代码(可直接运行)
#include <iostream>
void printResult(vector<string>& res) {
cout << "[";
for (int i = 0; i < res.size(); ++i) {
cout << "\"" << res[i] << "\"";
if (i != res.size() - 1) {
cout << ",";
}
}
cout << "]" << endl;
}
int main() {
Solution s;
// 测试示例1:digits = "23"
string digits1 = "23";
vector<string> res1 = s.letterCombinations(digits1);
cout << "示例1输出:";
printResult(res1); // 输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
// 测试示例2:digits = "2"
string digits2 = "2";
vector<string> res2 = s.letterCombinations(digits2);
cout << "示例2输出:";
printResult(res2); // 输出:["a","b","c"]
return 0;
}
三、代码详细解释
1. 核心变量
letterMap:常量数组,存储数字到字母的映射,下标直接对应数字(0/1 为空,2-9 对应对应字母);result:全局私有变量,存储最终所有字母组合;path:全局私有变量,存储当前拼接的字母串(回溯的 "路径")。
2. 回溯函数 backtracking
- 参数 :
digits:输入的数字串(比如 "23");index:当前处理到数字串的第几个字符(比如 index=0 处理 '2',index=1 处理 '3')。
- 结束条件 :
index == digits.size(),说明所有数字都处理完了,路径是完整组合,加入结果集后返回。 - 关键步骤 :
digit = digits[index] - '0':将字符型数字(比如 '2')转为整型数字(2),用于索引映射表;letters = letterMap[digit]:获取当前数字对应的字母串(比如 digit=2 时,letters="abc");- 遍历字母串的每个字母:
path.push_back(letters[i]):把当前字母拼接到路径中(做出选择);backtracking(digits, index + 1):递归处理下一个数字(index+1);path.pop_back():删除路径最后一个字母(撤销选择,回溯核心)。
3. 主函数 letterCombinations
- 边界处理:如果输入数字串为空(比如 digits=""),直接返回空数组;
- 初始化
result和path,避免多次调用时的残留; - 调用回溯函数,从第 0 个数字开始处理;
- 返回最终结果。
4. 执行过程(以 digits="23" 为例)
- 初始调用
backtracking("23", 0),path 为空; - index=0,digit=2,letters="abc":
- 遍历 i=0(字母 a):
- path 变成 "a",调用
backtracking("23", 1); - index=1,digit=3,letters="def":
- 遍历 i=0(字母 d):path 变成 "ad",index=2 等于 digits 长度,加入结果集;
- 撤销选择,path 变回 "a";
- 遍历 i=1(字母 e):path 变成 "ae",加入结果集;
- 撤销选择,path 变回 "a";
- 遍历 i=2(字母 f):path 变成 "af",加入结果集;
- 撤销选择,path 变回 "a";
- 撤销选择,path 变回空;
- path 变成 "a",调用
- 遍历 i=1(字母 b):重复上述逻辑,生成 "bd"、"be"、"bf";
- 遍历 i=2(字母 c):重复上述逻辑,生成 "cd"、"ce"、"cf";
- 遍历 i=0(字母 a):
- 最终结果就是示例 1 的输出。
四、关键注意点
- 字符转数字 :
digits[index]是 char 类型(比如 '2'),减去 '0' 才能得到整型数字 2(ASCII 码运算); - 边界条件:输入空字符串时,直接返回空结果(题目提示 digits 长度≥1,但代码要鲁棒);
- 回溯的顺序:index 控制处理数字的顺序,确保先处理第 0 个数字,再处理第 1 个,不会乱序;
- 映射表的设计:用数组而非哈希表,下标直接对应数字,访问效率更高。
总结
- 本题回溯的核心是:按数字顺序遍历,对每个数字的字母做 "选择→递归→撤销选择";
- 关键步骤:建立数字 - 字母映射表 → 用 index 控制处理数字的顺序 → 回溯拼接字母;
- 回溯框架通用:路径(path)+ 选择列表(当前数字的字母)+ 结束条件(index 到末尾)。
这个思路可以套用到所有 "多组选择拼接组合" 的回溯问题中,比如密码组合、字符排列等。
js版本
电话号码的字母组合(JS 版)
我会基于回溯算法的核心逻辑,给你写一个完整、可运行的 JS 版本,同时详细注释每一步,帮你理解和之前 C++ 版本对应的逻辑。
完整 JS 代码实现
javascript
/**
* @param {string} digits
* @return {string[]}
*/
var letterCombinations = function(digits) {
// 1. 处理边界条件:输入为空字符串时直接返回空数组
if (digits.length === 0) return [];
// 2. 数字到字母的映射表(下标对应数字,和C++版逻辑一致)
const letterMap = [
"", // 0
"", // 1
"abc", // 2
"def", // 3
"ghi", // 4
"jkl", // 5
"mno", // 6
"pqrs", // 7
"tuv", // 8
"wxyz" // 9
];
// 3. 结果集:存储所有符合条件的字母组合
const result = [];
// 4. 路径:存储当前拼接的字母串
let path = "";
// 5. 回溯函数
// index:当前处理到digits的第几个数字
const backtracking = (digits, index) => {
// 结束条件:处理完所有数字(路径长度 = 数字串长度)
if (index === digits.length) {
result.push(path); // 将当前完整组合加入结果集
return; // 回溯,终止当前递归
}
// 6. 获取当前处理的数字(char转int:比如'2' -> 2)
const digit = digits[index] - '0';
// 7. 获取当前数字对应的字母串
const letters = letterMap[digit];
// 8. 遍历当前数字的所有字母
for (let i = 0; i < letters.length; i++) {
path += letters[i]; // 做出选择:拼接当前字母到路径
backtracking(digits, index + 1); // 递归:处理下一个数字
path = path.slice(0, -1); // 撤销选择:删除最后一个字母(回溯核心)
}
};
// 调用回溯函数,从第0个数字开始处理
backtracking(digits, 0);
return result;
};
// 测试用例
console.log(letterCombinations("23")); // 输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
console.log(letterCombinations("2")); // 输出:["a","b","c"]
console.log(letterCombinations("")); // 输出:[]
代码详细解释
1. 核心逻辑对应(和 C++ 版一致)
| 部分 | 解释 |
|---|---|
letterMap |
数字到字母的映射数组,下标直接对应数字(0/1 为空,2-9 对应对应字母),和 C++ 的letterMap完全一致 |
result |
存储最终所有字母组合的数组,对应 C++ 的vector<string> result |
path |
存储当前拼接的字母串(JS 用字符串更方便,C++ 用string),核心是 "拼接→递归→删除最后一位" |
backtracking函数 |
回溯核心函数,index参数控制处理数字的顺序(和 C++ 的index作用完全一样) |
2. 关键步骤拆解
(1)边界条件处理
javascript
if (digits.length === 0) return [];
输入为空字符串时直接返回空数组,避免后续无效递归(题目提示digits.length≥1,但代码要鲁棒)。
(2)字符转数字
javascript
const digit = digits[index] - '0';
JS 中digits[index]是字符串类型(比如"23"的第 0 位是"2"),减去'0'利用 ASCII 码运算转为数字 2(和 C++ 的digits[index] - '0'逻辑一致)。
(3)选择 / 撤销选择
- 选择 :
path += letters[i]→ 把当前字母拼接到路径末尾(比如path是"a",拼接"d"后变成"ad"); - 撤销选择 :
path = path.slice(0, -1)→ 截取字符串除最后一位的部分(比如"ad"变回"a"),对应 C++ 的path.pop_back()。
(4)递归终止条件
javascript
if (index === digits.length) {
result.push(path);
return;
}
当index等于数字串长度时,说明所有数字都处理完了,当前path是一个完整的组合,加入结果集后返回。
3. 执行过程(以digits="23"为例)
- 初始调用
backtracking("23", 0),path为空; index=0→digit=2→letters="abc":- 遍历
i=0(字母a):path变成"a"→ 调用backtracking("23", 1);index=1→digit=3→letters="def":- 遍历
i=0(字母d):path变成"ad"→index=2等于digits.length,加入结果集; - 撤销选择:
path变回"a"; - 遍历
i=1(字母e):path变成"ae"→ 加入结果集; - 撤销选择:
path变回"a"; - 遍历
i=2(字母f):path变成"af"→ 加入结果集; - 撤销选择:
path变回"a";
- 遍历
- 撤销选择:
path变回空;
- 遍历
i=1(字母b):重复上述逻辑,生成"bd"、"be"、"bf"; - 遍历
i=2(字母c):重复上述逻辑,生成"cd"、"ce"、"cf";
- 遍历
- 最终返回
["ad","ae","af","bd","be","bf","cd","ce","cf"]。
总结
- JS 版核心逻辑和 C++ 完全一致:映射表 + 回溯(选择→递归→撤销选择) + index 控制数字处理顺序;
- JS 中用字符串
path更简洁,撤销选择用slice(0, -1)替代 C++ 的pop_back(); - 边界条件处理是关键:输入空字符串时直接返回空数组,避免递归错误。
5 小结
回溯其实是撤销的操作,看了题解就不难,自己重新画一遍图,手敲一遍,一下清晰了不少。
第二题做得比较匆忙,留心字符串的dfs回溯。还是要自己走一遍测试才清楚!!