电话号码的字母组合、
以下是解决电话号码的字母组合问题(LeetCode 17)的C++回溯算法代码,结合了多个权威解法的核心逻辑,并添加详细注释说明:
#include <vector>
#include <string>
using namespace std;
class Solution {
private:
// 数字到字母的映射表(索引0~9对应数字0~9)
const string letterMap[10] = {
"", // 0
"", // 1
"abc", // 2
"def", // 3
"ghi", // 4
"jkl", // 5
"mno", // 6
"pqrs", // 7(4个字母)
"tuv", // 8
"wxyz" // 9(4个字母)
};
vector<string> result; // 存储所有组合结果
string current; // 当前构建的组合字符串
// 回溯函数
void backtrack(const string& digits, int index) {
// 终止条件:已处理完所有数字
if (index == digits.size()) {
result.push_back(current); // 保存当前组合
return;
}
char digit = digits[index]; // 当前处理的数字字符
int num = digit - '0'; // 转为数字索引
string letters = letterMap[num]; // 获取对应的字母串
// 遍历当前数字的所有可能字母
for (int i = 0; i < letters.size(); ++i) {
current.push_back(letters[i]); // 选择当前字母
backtrack(digits, index + 1); // 递归处理下一个数字
current.pop_back(); // 回溯:撤销选择
}
}
public:
vector<string> letterCombinations(string digits) {
result.clear();
current.clear();
if (digits.empty()) return result; // 处理空输入
backtrack(digits, 0); // 从第0位数字开始回溯
return result;
}
};
🔍 关键代码解析
-
字母映射表 (
letterMap
)-
索引对应数字(如
letterMap[2] = "abc"
),跳过0
和1
。 -
数字
7
和9
对应 4个字母,其余对应3个字母。
-
-
回溯函数 (
backtrack
)-
终止条件 :
index == digits.size()
时,说明已生成完整组合,存入result
。 -
遍历逻辑:
-
通过
digits[index] - '0'
将数字字符转为索引(如'2'→2
)。 -
循环遍历当前数字对应的每个字母:
for (int i = 0; i < letters.size(); ++i)
-
选择 :
current.push_back(letters[i])
添加字母。 -
递归 :
backtrack(digits, index + 1)
处理下一个数字。 -
回溯 :
current.pop_back()
撤销最后添加的字母。
-
-
-
初始化与边界处理
-
主函数中清空
result
和current
,避免多次调用残留数据。 -
直接返回空结果当输入
digits
为空(如digits = ""
)。
-
⚙️ 算法特性
指标 | 值 | 说明 |
---|---|---|
时间复杂度 | O(3ᵐ × 4ⁿ) | m :输入中对应3个字母的数字个数(如2,3,4,5,6,8);n :对应4个字母的数字个数(如7,9)。 |
空间复杂度 | O(m + n) | 递归栈深度(最大为数字串长度),结果集空间不计入。 |
适用场景 | digits.length ≤ 4 |
因组合数指数级增长,输入较长时需注意性能。 |
🔄 替代解法:队列法(BFS)
vector<string> letterCombinations(string digits) {
if (digits.empty()) return {};
vector<string> result = {""}; // 初始化含空字符串的队列
const string letterMap[10] = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
for (char digit : digits) {
vector<string> tmp;
string letters = letterMap[digit - '0'];
for (string s : result) {
for (char c : letters) {
tmp.push_back(s + c); // 组合已有结果与新字母
}
}
result = tmp; // 更新当前层的结果
}
return result;
}
-
思路:逐层扩展组合,类似BFS。
-
优势:避免递归栈开销,更适合迭代场景。
两种解法均能高效解决问题,回溯法 更符合算法竞赛标准,队列法更易理解。根据场景选择即可。
队列法代码详细解释,并且把附带详细注释的代码重新编辑整理好发给我
以下是对您提供的电话号码字母组合迭代实现代码的详细解释和注释版。该算法采用BFS(广度优先搜索)思想逐层构建组合,效率高且易于理解:
#include <vector>
#include <string>
using namespace std;
vector<string> letterCombinations(string digits) {
// 处理边界情况:输入为空字符串时直接返回空向量
if (digits.empty()) return {};
// 初始化结果队列:包含一个空字符串作为组合起点
vector<string> result = {""};
// 建立数字到字母的映射表(索引对应数字字符的ASCII码值)
const string letterMap[10] = {
"", "", // 0, 1(无字母映射)
"abc", // 2
"def", // 3
"ghi", // 4
"jkl", // 5
"mno", // 6
"pqrs", // 7(注意有4个字母)
"tuv", // 8
"wxyz" // 9(注意有4个字母)
};
// 遍历输入数字字符串的每个数字
for (char digit : digits) {
vector<string> tmp; // 临时存储当前层的新组合
// 获取当前数字对应的字母串(ASCII转换技巧)
string letters = letterMap[digit - '0']; // 字符→整数索引转换
// 组合生成核心逻辑(三重循环):
for (string s : result) { // 遍历上层所有组合
for (char c : letters) { // 遍历当前数字的所有字母
tmp.push_back(s + c); // 生成新组合:旧组合+新字母
}
}
result = tmp; // 更新结果队列为当前层的新组合
}
return result; // 返回最终所有组合
}
🔑 核心逻辑详解
-
初始化与边界处理
-
空输入直接返回空结果(
digits.empty()
判断) -
结果队列初始化包含空字符串,作为组合起点
-
-
数字-字母映射
-
使用索引数组
letterMap
实现高效映射(索引=数字ASCII值-'0') -
特别注意数字7和9对应4个字母(其他为3个)
-
-
三层循环结构
第一层:遍历输入数字 (for char digit : digits) │ ├─ 第二层:遍历上层结果 (for string s : result) │ │ │ └─ 第三层:遍历当前数字的字母 (for char c : letters) │ │ │ └─ 生成新组合:s + c → 存入tmp │ └─ 更新结果:result = tmp
-
每处理一个数字,组合数量扩大
letters.size()
倍 -
示例流程(输入"23"):
初始: [""] 处理'2'后: ["a", "b", "c"] 处理'3'后: ["ad","ae","af","bd",...]
-
-
时间复杂度分析
-
最优:O(3ᴺ) - 当输入全为3字母数字(2-6,8)
-
最差:O(4ᴹ) - 当输入全为4字母数字(7,9)
-
实际:O(3ᴺ·4ᴹ),其中N为3字母数字个数,M为4字母数字个数
-
⚡ 性能优化说明
-
避免递归开销
相比回溯法,迭代实现无函数调用栈开销,内存更稳定
-
预分配内存建议
可在循环前预计算总组合数,使用
reserve()
优化:int total = 1; for (char d : digits) total *= letterMap[d-'0'].size(); result.reserve(total);
🌰 执行示例(输入"23")
处理阶段 | result 内容 |
tmp 内容生成过程 |
---|---|---|
初始 | [""] |
- |
处理'2' | ["a", "b", "c"] |
""+"a" , ""+"b" , ""+"c" |
处理'3' | ["ad","ae",...,"cf"] |
"a"+"d" , "a"+"e" , ... |
最终输出:
["ad","ae","af","bd","be","bf","cd","ce","cf"]
#include <vector>
#include <string>
using namespace std;
class Solution {
private:
// 数字到字母的映射表(索引0~9对应数字0~9)
const string letterMap[10] = {
"", // 0
"", // 1
"abc", // 2
"def", // 3
"ghi", // 4
"jkl", // 5
"mno", // 6
"pqrs", // 7(4个字母)
"tuv", // 8
"wxyz" // 9(4个字母)
};
vector<string> result; // 存储所有组合结果
string current; // 当前构建的组合字符串
// 回溯函数
void backtrack(const string& digits, int index) {
// 终止条件:已处理完所有数字
if (index == digits.size()) {
result.push_back(current); // 保存当前组合
return;
}
char digit = digits[index]; // 当前处理的数字字符
int num = digit - '0'; // 转为数字索引
string letters = letterMap[num]; // 获取对应的字母串
// 遍历当前数字的所有可能字母
for (int i = 0; i < letters.size(); ++i) {
current.push_back(letters[i]); // 选择当前字母
backtrack(digits, index + 1); // 递归处理下一个数字
current.pop_back(); // 回溯:撤销选择
}
}
public:
vector<string> letterCombinations(string digits) {
result.clear();
current.clear();
if (digits.empty()) return result; // 处理空输入
backtrack(digits, 0); // 从第0位数字开始回溯
return result;
}
}; 详细解释
以下是对你提供的电话号码字母组合回溯算法的详细解析,结合代码结构、回溯原理和性能优化进行说明:
1. 核心数据结构:数字-字母映射表
const string letterMap[10] = {
"", // 0(未使用)
"", // 1(未使用)
"abc", // 2
"def", // 3
"ghi", // 4
"jkl", // 5
"mno", // 6
"pqrs", // 7(4字母)
"tuv", // 8
"wxyz" // 9(4字母)
};
-
设计意图 :直接通过数字索引(
digits[i] - '0'
)快速获取对应字母集合,时间复杂度为O(1)。 -
特殊处理:数字0和1映射为空字符串(电话键盘无对应字母),数字7和9对应4个字母(其他为3个)。
2. 回溯函数 backtrack()
核心逻辑
(1)终止条件
if (index == digits.size()) {
result.push_back(current); // 保存完整组合
return;
}
-
作用 :当处理完所有数字(
index
达到digits
长度)时,将当前组合current
存入结果集result
。 -
递归深度:与输入字符串长度一致,每个数字对应一层递归。
(2)字母遍历与回溯
char digit = digits[index]; // 当前数字字符
int num = digit - '0'; // 转为索引(0-9)
string letters = letterMap[num]; // 获取字母集
for (int i = 0; i < letters.size(); ++i) {
current.push_back(letters[i]); // 选择当前字母
backtrack(digits, index + 1); // 递归处理下一数字
current.pop_back(); // 回溯:撤销选择
}
-
树形结构:每个数字对应树的一层,每个字母对应一个分支。
- 示例 :输入
"23"
时,第一层为2
(分支a/b/c
),第二层为3
(分支d/e/f
),组合结果为["ad","ae",...,"cf"]
。
- 示例 :输入
-
回溯操作 :
pop_back()
撤销当前选择,确保返回父节点时路径状态正确。
**3. 主函数 letterCombinations()
**
vector<string> letterCombinations(string digits) {
result.clear();
current.clear();
if (digits.empty()) return result; // 处理空输入
backtrack(digits, 0); // 从索引0启动回溯
return result;
}
-
边界处理 :直接返回空结果集应对空输入(如
digits = ""
),避免无效递归。 -
初始化 :清空全局变量
result
和current
,确保多次调用无状态残留。
4. 算法性能分析
指标 | 值 | 解释 |
---|---|---|
时间复杂度 | O(3ᴹ × 4ᴺ) | M :对应3字母的数字(2-6, 8)数量 N :对应4字母的数字(7, 9)数量 |
空间复杂度 | O(L) | L :递归栈深度(=输入字符串长度) |
最优情况 | O(1) | 输入为空时直接返回result (无递归) |
5. 关键优化与注意事项
-
剪枝无效路径 :当前代码无显式剪枝,但通过
digits.empty()
避免空输入进入回溯,属于隐式优化。 -
状态管理:
-
current
作为路径跟踪器,通过push_back
/pop_back
动态维护组合状态。 -
全局变量
result
减少参数传递开销,但需注意多线程安全性。
-
-
映射表扩展性 :若需支持数字
0
/1
的映射(如某些键盘映射+
或*
),只需扩展letterMap
内容。
总结
此回溯算法通过深度优先搜索(DFS) 遍历所有可能的字母组合:
-
映射表加速:常量时间获取字母集合。
-
回溯框架:选择→递归→撤销,确保路径状态正确回溯。
-
高效终止:递归深度与输入长度严格一致,避免冗余计算。
-
适用场景:数字字符串较短(≤4)时高效,若输入较长需考虑迭代法优化空间。
backtrack(digits, 0); // 从索引0启动回溯
在回溯算法中,backtrack(digits, 0)
的 索引 0 是回溯的起始点,它表示从输入字符串 digits
的第一个字符(下标 0)开始处理。这个参数在递归过程中动态推进,控制着算法的执行流程。以下是详细解释:
1. 索引的作用:递归深度与位置标记
-
核心意义
index
表示当前正在处理的数字在digits
中的位置。例如:-
index = 0
:处理第一个数字(如digits[0] = '2'
)。 -
index = 1
:处理第二个数字(如digits[1] = '3'
)。
-
-
递归终止条件
当
index == digits.length()
时,说明已处理完所有数字,将当前组合current
存入结果集result
。
2. 索引在回溯中的推进流程
以输入 digits = "23"
为例,执行流程如下:
-
第一层递归(
index = 0
)-
当前数字
digits[0] = '2'
→ 对应字母"abc"
。 -
遍历字母
'a'
、'b'
、'c'
:- 选择
'a'
→current = "a"
→ 递归进入下一层(index = 1
)。
- 选择
-
-
第二层递归(
index = 1
)-
当前数字
digits[1] = '3'
→ 对应字母"def"
。 -
遍历字母
'd'
、'e'
、'f'
:-
选择
'd'
→current = "ad"
→ 满足终止条件(index = 2
),保存结果。 -
回溯:撤销
'd'
→ 继续选'e'
→ 生成"ae"
,依此类推。
-
-
-
回溯到第一层
-
撤销
'a'
→ 选择'b'
→ 进入第二层生成"bd"
、"be"
、"bf"
。 -
同理处理
'c'
。
graph TD
A[开始:index=0] --> B[数字 '2':选 a]
B --> C[index=1:数字 '3' 选 d]
C --> D[生成 ad]
B --> E[index=1:数字 '3' 选 e]
E --> F[生成 ae]
B --> G[index=1:数字 '3' 选 f]
G --> H[生成 af]
A --> I[数字 '2':选 b]
I --> J[index=1:数字 '3' 选 d]
J --> K[生成 bd]
I --> L[...]
A --> M[数字 '2':选 c]
M --> N[...] -
3. 为什么索引从 0 开始?
-
编程规范一致性
编程语言中数组/字符串的索引默认从 0 开始(如
digits[0]
表示首个字符)。 -
避免偏移计算
若从 1 开始,需额外处理
index-1
,增加复杂度和错误风险。 -
递归边界清晰
index == digits.length()
直接对应字符串末尾,无需调整。
4. 索引与回溯的关联
-
推进机制
每次递归调用
backtrack(digits, index + 1)
,索引 +1 表示处理下一个数字。 -
隐式回溯
当递归返回上层时,
index
自动回退到上一层的值,无需显式重置。示例:第二层递归结束后,返回第一层时
index
仍为 0,继续遍历剩余字母。
5. 对比其他场景的索引设计
问题类型 | 索引起始值 | 原因 |
---|---|---|
电话号码字母组合 | 0 | 顺序处理,不重复使用数字 |
全排列(无重复元素) | 0 | 需遍历所有位置的可能元素 |
子集问题 | 动态 start |
避免重复组合(如 [1,2] 和 [2,1] ) |
总结
-
**
backtrack(digits, 0)
中的 0** 是回溯算法的入口标记,确保从输入字符串的首字符开始生成组合。 -
索引递增(
index + 1
) 实现递归层级的推进,逐步覆盖所有数字。 -
索引归零 通过递归栈自动完成,回溯时自然回到上一层状态。
此设计是回溯算法的通用范式,在组合、排列等问题中广泛适用。如需进一步优化(如剪枝),可结合索引范围限制搜索空间 。
【问题】char digit = digits[index]; // 当前处理的数字字符
int num = digit - '0'; // 转为数字索引
string letters = letterMap[num]; // 获取对应的字母串 没理解
理解这三行代码是回溯算法处理数字字符的核心步骤。以下是分步解析:
1. **char digit = digits[index];
**
-
作用 :从输入字符串
digits
中取出第index
位的字符(如digits = "23"
时,若index=0
,则digit = '2'
)。 -
数据类型 :
digit
是char
类型(字符),例如'2'
的 ASCII 码值为50
,而非数值2
。
2. **int num = digit - '0';
**
-
转换原理:
-
字符
'0'
的 ASCII 码为 48 ,'1'
为 49,依此类推至'9'
为 57。 -
计算示例:
- 若
digit = '2'
(ASCII 值50
),则num = 50 - 48 = 2
(整数)。
- 若
-
-
目的 :将字符数字(如
'2'
)转为整型数字(如2
),用于后续数组索引。
3. **string letters = letterMap[num];
**
-
映射逻辑:
-
letterMap
是预定义的字符串数组,索引对应数字0-9
:const string letterMap[10] = { "", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz" }; // 索引2对应"abc",3对应"def",以此类推
-
若
num = 2
(即数字2
),则letters = "abc"
。
-
-
输出结果 :获取当前数字对应的所有可能字母(如
'2' → "abc"
)。
🌰 实例演示(输入 digits = "23"
)
-
index=0
:digit = '2'
→num = '2' - '0' = 2
→letters = "abc"
。
-
index=1
:digit = '3'
→num = '3' - '0' = 3
→letters = "def"
。
-
组合结果 :
"a" + "d" = "ad"
、"a" + "e" = "ae"
......"c" + "f" = "cf"
。
⚠️ 关键注意事项
-
输入合法性 :题目保证输入仅为
'2'-'9'
,因此num
范围是2-9
,不会越界。 -
特殊字符处理 :若输入含
'0'
或'1'
(映射为空字符串),组合过程自动跳过。 -
效率优化 :字符转整数的操作时间复杂度为 O(1),是高效的内存操作。
💡 为什么不用字典而用数组?
数组索引直接通过数值计算定位,比字典的键值查询(哈希计算)更高效,尤其适合固定范围的映射关系。