Leetcode 137 组合 | 电话号码的字母组合

新的模块,回溯算法解题套路框架

1 题目

77. 组合

给定两个整数 nk,返回范围 [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 <= 20
  • 1 <= 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. 从数字 1 开始,逐个选择数字加入路径;
  2. 当路径长度等于 k 时,把路径加入结果集;
  3. 选完一个数字后,回溯(撤销选择),继续尝试下一个数字;
  4. 为了避免重复组合(比如 [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
  • 先清空resultpath(避免多次调用时的残留);
  • 调用回溯函数,从数字 1 开始选;
  • 返回最终结果。
4. 测试代码
  • printResult:格式化输出结果,方便验证;
  • main函数:测试示例 1 和示例 2,输出和题目要求一致。
四、执行过程(以 n=4, k=2 为例)
  1. 初始调用backtracking(4,2,1),path 为空;
  2. 第一次循环 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 变回空;
  3. 第二次循环 i=2:
    • path 变成 [2],调用backtracking(4,2,3)
    • 内层循环 i=3:path 变成 [2,3],加入结果集,返回;
    • 撤销选择,path 变回 [2];
    • 内层循环 i=4:path 变成 [2,4],加入结果集,返回;
    • 撤销选择,path 变回空;
  4. 第三次循环 i=3:
    • path 变成 [3],调用backtracking(4,2,4)
    • 内层循环 i=4:path 变成 [3,4],加入结果集,返回;
    • 撤销选择,path 变回空;
  5. 最终结果就是题目示例 1 的输出。

总结

  1. 回溯算法解决组合问题的核心是:路径(已选数字)+ 选择列表(可选数字)+ 结束条件(选够 k 个数)
  2. 关键优化点:通过start参数避免重复组合,通过剪枝减少无效遍历;
  3. 回溯的核心操作:做出选择 → 递归 → 撤销选择,这是所有回溯问题的通用框架。

这个框架可以套用到所有组合、排列、子集类的回溯问题中,掌握后就能解决大部分同类题目。

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)); // 输出和递归版一致

三、你原代码的核心错误总结

  1. 变量作用域respath定义在backtracking内部,导致每次递归都会重置,无法累积结果;
  2. 语法错误 :箭头函数const backtracking (n,k,start) => {}少写了=,正确是const backtracking = (n,k,start) => {}
  3. 递归参数错误backtracking(n,k,start + 1)应该是i+1,否则会重复选 1、2、3...(比如选了 1 之后,下一轮还是从 2 开始,而非 1+1=2,但如果 start 不变会一直循环);
  4. 引用类型陷阱res.push(path)直接 push 引用,后续path.pop()会修改已存入的结果,必须用[...path]创建副本;
  5. 循环条件错误i < n - (k - path.length) + 1少写了=,导致边界数字(比如 4)无法被选中;
  6. 撤销选择未完成path.后面没写pop(),导致路径无法回溯。

总结

  1. 回溯算法的核心就是栈操作(push 入栈选数,pop 出栈撤销选数),你觉得用栈更顺是完全正确的;
  2. JS 版回溯的关键:respath要定义在回溯函数外部,递归参数用i+1而非start+1,存入结果时要创建路径副本;
  3. 两种实现方式:递归版(隐式利用函数调用栈)更简洁,非递归版(显式栈)更直观体现栈的逻辑,可根据习惯选择。

3 题目

17. 电话号码的字母组合

中等

相关标签

相关企业

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

示例 1:

复制代码
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]

示例 2:

复制代码
输入:digits = "2"
输出:["a","b","c"]

提示:

  • 1 <= digits.length <= 4
  • digits[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(),说明所有数字都处理完了,路径是完整组合,加入结果集后返回。
  • 关键步骤
    1. digit = digits[index] - '0':将字符型数字(比如 '2')转为整型数字(2),用于索引映射表;
    2. letters = letterMap[digit]:获取当前数字对应的字母串(比如 digit=2 时,letters="abc");
    3. 遍历字母串的每个字母:
      • path.push_back(letters[i]):把当前字母拼接到路径中(做出选择);
      • backtracking(digits, index + 1):递归处理下一个数字(index+1);
      • path.pop_back():删除路径最后一个字母(撤销选择,回溯核心)。
3. 主函数 letterCombinations
  • 边界处理:如果输入数字串为空(比如 digits=""),直接返回空数组;
  • 初始化resultpath,避免多次调用时的残留;
  • 调用回溯函数,从第 0 个数字开始处理;
  • 返回最终结果。
4. 执行过程(以 digits="23" 为例)
  1. 初始调用backtracking("23", 0),path 为空;
  2. 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 变回空;
    • 遍历 i=1(字母 b):重复上述逻辑,生成 "bd"、"be"、"bf";
    • 遍历 i=2(字母 c):重复上述逻辑,生成 "cd"、"ce"、"cf";
  3. 最终结果就是示例 1 的输出。

四、关键注意点

  1. 字符转数字digits[index]是 char 类型(比如 '2'),减去 '0' 才能得到整型数字 2(ASCII 码运算);
  2. 边界条件:输入空字符串时,直接返回空结果(题目提示 digits 长度≥1,但代码要鲁棒);
  3. 回溯的顺序:index 控制处理数字的顺序,确保先处理第 0 个数字,再处理第 1 个,不会乱序;
  4. 映射表的设计:用数组而非哈希表,下标直接对应数字,访问效率更高。

总结

  1. 本题回溯的核心是:按数字顺序遍历,对每个数字的字母做 "选择→递归→撤销选择"
  2. 关键步骤:建立数字 - 字母映射表 → 用 index 控制处理数字的顺序 → 回溯拼接字母;
  3. 回溯框架通用:路径(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"为例)
  1. 初始调用backtracking("23", 0)path为空;
  2. index=0digit=2letters="abc"
    • 遍历i=0(字母a):path变成"a" → 调用backtracking("23", 1)
      • index=1digit=3letters="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"
  3. 最终返回["ad","ae","af","bd","be","bf","cd","ce","cf"]

总结

  1. JS 版核心逻辑和 C++ 完全一致:映射表 + 回溯(选择→递归→撤销选择) + index 控制数字处理顺序
  2. JS 中用字符串path更简洁,撤销选择用slice(0, -1)替代 C++ 的pop_back()
  3. 边界条件处理是关键:输入空字符串时直接返回空数组,避免递归错误。

5 小结

回溯其实是撤销的操作,看了题解就不难,自己重新画一遍图,手敲一遍,一下清晰了不少。

第二题做得比较匆忙,留心字符串的dfs回溯。还是要自己走一遍测试才清楚!!

相关推荐
Alex艾力的IT数字空间1 小时前
OCR 原理:从像素到文本的智能转换
数据结构·人工智能·python·神经网络·算法·cnn·ocr
Never_Satisfied1 小时前
在JavaScript / HTML中,判断指定的元素是否含有某个类
开发语言·javascript·html
牢七1 小时前
Slim-4.x php审计 前言篇
开发语言·php
沐知全栈开发2 小时前
HTML 列表
开发语言
froginwe112 小时前
HTML 实例详解
开发语言
知我Deja_Vu2 小时前
【避坑指南】ConcurrentHashMap 并发操作的致命陷阱
java·开发语言
仟濹2 小时前
【算法打卡day19(2026-03-11 周三)算法:打家劫舍-DP,双指针,二分查找,滑动窗口,方向控制,前缀和 】8个题
算法·leetcode·二分查找·动态规划
未来之窗软件服务2 小时前
自己写算法(十)js加密UUID保护解密——东方仙盟化神期
java·javascript·算法·代码加密·东方仙盟算法
样例过了就是过了2 小时前
LeetCode热题100 腐烂的橘子
数据结构·c++·算法·leetcode·bfs