题目描述
本文是LC第17题,电话号码的字母组合,题目描述如下:
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
限制
0 <= digits.length <= 4
digits[i] 是范围 [‘2’, ‘9’] 的一个数字。
示例 1:
输入:digits = “23”
输出:[“ad”,“ae”,“af”,“bd”,“be”,“bf”,“cd”,“ce”,“cf”]
示例 2:
输入:digits = “”
输出:[]
示例 3:
输入:digits = “2”
输出:[“a”,“b”,“c”]
解题思路
- 回溯法(递归)(dfs深度优先搜索)
数据结构:哈希表,存储每个数字对应的所有可能的字母。
算法:回溯过程中维护一个字符串,表示已有的字母序列。该字符串初始为空。
- 每次取电话号码的一位数字,从哈希表中获得该数字对应的所有可能的字母
- 将第一个字母插入到已有的字母排列后面,
- 继续处理电话号码的后一位数字,
- 直到处理完电话号码中的所有数字,即得到一个完整的字母排列。
- 进行回退操作,每次回退一个数字,假如第一次添加的是该数字的第一个字母,回退后,就是将该字母从序列末尾取出,然后将第二个字母放入末尾,形成一个新的序列。
回溯算法用于寻找所有的可行解,如果发现一个解不可行,则会舍弃不可行的解。
在这道题中,由于每个数字对应的每个字母都可能进入字母组合,因此不存在不可行的解,直接穷举所有的解即可。
class Solution {
private:
map<char, string> map_phone = {
{'2', "abc"},
{'3', "def"},
{'4', "ghi"},
{'5', "jkl"},
{'6', "mno"},
{'7', "pqrs"},
{'8', "tuv"},
{'9', "wxyz"}
};
public:
vector<string> letterCombinations(string digits)
{
// 判空
if (digits == "") return {};
vector<string> result;
string path;
backtrack(result, digits, 0, path);
return result;
}
private:
// NOTION: 所有 值传递的 参数在递归/回溯 中都会刷新, 不会永久存储;;因此 result 参数作为保存结果的参数, 必须用引用或指针传递。
void backtrack(vector<string>& result, string digits, int index, string path)
{
if (index == digits.length()) {
result.push_back(path);
} else {
string str = map_phone[digits[index]];
for (int j = 0; j < str.length(); ++j) {
backtrack(result, digits, index+1, path+str[j]);
}
}
}
};
其实,backtrack
的实现是核心,因此一定要写好backtrack
中的循环,笔者第一次解题的时候将backtrace
中的循环写成了如下代码
for (int i = index; i < digits.length(); ++i) {
string str = map_phone[digits[i]];
for (int j = 0; j < str.length(); ++j) {
backtrack(result, digits, index+1, path+str[j]);
}
}
而上述错误代码与题解中正确代码区别在于
上述代码产出的是类似如下的一颗树状结构:
index: 0 2 3 (index=0, 遍历 2 和 3)
/|\ /|\
a b c d e f
index: 1 /|\ .../|\ (index=1, 只遍历3)
d e f d e f
/|\
ad ...
index: 2 (没有要遍历的元素)
res: ad ae af bd be bf cd ce cf dd de df ed ee ef fd fe ff
题解代码产生是如下的树状结构,注意辨析二者的区别
index: 0 2
/|\
a b c
/|\...
index: 1 d e f
/|\
ad...
res: ad ae af bd be bf cd ce cf
- 双端队列(bfs广度优先搜索)
核心思路:借助C++中的双端队列,deque,该结构是一个既可以在队列头部插入,也可以在队列尾部插入的结构。
遍历数字,每遍历一个数字都将该数字代表的所有字符,都push到队列中已经有的每个序列的末尾,形成一个新的序列,同时将原来的旧的序列从队列头部pop掉。
// 方法二 : 双端队列 (bfs)
vector<string> letterCombinations(string digits)
{
if (digits == "") return {};
vector<string> result;
deque<string> path_queue;
path_queue.push_back("");
for (int i = 0; i < digits.length(); ++i) {
string str = m_map_phone[digits[i]];
int cycle = path_queue.size();
for (int k = 0; k < cycle; ++k) {
string path = path_queue.front();
path_queue.pop_front();
for (int j = 0; j < str.length(); ++j) {
path_queue.push_back(path+str[j]);
}
}
}
result.assign(path_queue.begin(), path_queue.end());
return result;
}