当深度优先搜索(DFS)遇上数字字母组合问题
问题描述
给定一个仅包含数字 2-9 的字符串,你需要返回所有它能表示的字母组合。每个数字可以表示对应数字上字母的任意一种组合。注意,1 不对应任何字母。
以下是问题的示例:
示例 1:
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
示例 2:
输入:digits = ""
输出:[]
示例 3:
输入:digits = "2"
输出:["a","b","c"]
解决思想
为了解决这个问题,我们可以使用 深度优先搜索(DFS) 的方法,从头到尾逐个处理输入数字的每一位,生成所有可能的字母组合。我们会维护一个映射表,将数字与字母的对应关系保存起来,然后使用递归的方式探索所有可能的组合。
思路历程
-
建立映射表: 我们首先需要建立一个映射表,将数字与字母集合对应起来。这将帮助我们在处理每个数字时,知道可以选择哪些字母。可以使用
std::map
或数组来实现这个映射表。 -
递归生成组合: 我们将编写一个递归函数来处理生成字母组合的过程。这个函数会有几个参数:当前数字索引、当前构建的字符串和结果集。我们从索引0开始,递归地处理每个数字。
-
递归终止条件: 在递归函数内部,我们首先检查当前数字索引是否等于输入字符串的长度。如果是,说明我们已经处理完所有数字,将当前构建的字符串添加到结果集中,然后返回。
-
处理当前数字: 如果当前索引没有达到终止条件,我们需要获取当前数字对应的字母集合。然后,对于每个字母,我们都将它添加到当前构建的字符串末尾,然后递归调用函数,处理下一个数字(递增索引)。
-
获取字母集合: 我们可以通过之前建立的映射表来获取当前数字对应的字母集合。在遍历字母集合时,每次都将选定的字母添加到当前构建的字符串中,然后递归处理下一个数字。
-
结果集合并: 在递归的过程中,会不断地构建字符串,直到遍历完所有数字。这样,最终结果集中就会包含所有可能的字母组合。
代码实现:
class Solution {
public:
map<char , string> digitToLetters = {
{
'2', "abc"},
{
'3', "def"},
{
'4', "ghi"},
{
'5', "jkl"},
{
'6', "mno"},
{
'7', "pqrs"},
{
'8', "tuv"},
{
'9', "wxyz"}
};
void generateCombinations(string digits , int index , string current , vector<string> &ans){
if(index == digits.length()){
ans.push_back(current);
return ;
}
char now = digits[index];
string nowStr = digitToLetters[now];
for(char a : nowStr){
generateCombinations(digits , index + 1 , current + a , ans);
}
}
vector<string> letterCombinations(string digits) {
vector<string> ans;
if(digits.empty()){
return ans;
}
generateCombinations(digits , 0 , "" , ans);
return ans;
};
};
可能令人迷惑的问题
1. 为什么使用深度优先搜索?
深度优先搜索是一种递归的算法,它能够在问题的状态空间中搜索所有可能的解。在本问题中,我们需要从左到右处理每个数字,并逐个选择可能的字母,然后在下一层递归中继续处理后面的数字。深度优先搜索很适合解决这种需要逐步选择的组合问题。
2. 如何处理空输入?
在输入为空的情况下,我们希望返回一个空的结果集。在函数开头,我们可以通过检查输入长度是否为零来处理这种情况,并在这种情况下直接返回空的结果集。
3. 为什么可以使用index == digits.length()
条件作为递归出口呢?
使用条件index == digits.length()
作为递归出口的原因是因为我们要处理的是从0到digits.length() - 1
的数字索引,这代表了字符串中的每一个数字。
在深度优先搜索的过程中,我们首先处理的是索引为0的数字,然后递归处理索引为1的数字,以此类推。当我们处理到索引等于字符串长度时,意味着我们已经处理完了所有数字。考虑到字符串索引是从0开始的,如果索引等于字符串长度,说明我们已经处理过了整个字符串中的所有数字。
使用index == digits.length()
作为递归出口的好处是,它可以确保我们在递归的过程中逐步处理每个数字,并在处理完最后一个数字后终止递归。这样,我们就能够生成所有可能的字母组合,并且在每个递归步骤中都会构建正确的字符串。
4.和回溯法区别
回溯法和DFS在这个问题中几乎是等价的,因为它们的核心思想都是深度搜索。在实际操作中,回溯法可能会在选择时进行一些剪枝优化,以减少不必要的搜索。
- 回溯法的实现在函数命名上更体现了“
回溯
”的思想; - 而DFS则是
传统的深度优先搜索
在代码实现上,它们的逻辑几乎一样,都是通过递归来遍历数字和字母的组合。
回溯法通常被用于更加复杂的组合问题,其中需要在选择路径时进行条件判断,而DFS更加通用,适用于各种类型的深度搜索问题。在这个特定的问题中,两者可以互换使用。