【C++】电话号码的字母组合:从有限处理到通用解法

引言

LeetCode第17题"电话号码的字母组合"是一个经典的算法问题,它要求我们根据手机键盘的数字-字母映射,生成所有可能的字母组合。对于初学者来说,很容易写出只处理特定长度输入的代码。今天,我们就从这种有限处理开始,逐步探讨如何将其扩展为通用解法。

目录

引言

问题回顾

第一步:有限处理(长度1或2)

思路分析

代码实现

这种方法的局限性

第二步:发现问题,寻找通用模式

第三步:通用解法(迭代构建)

算法思路

通用代码实现

逐步解析通用解法

第四步:另一种通用解法(递归/回溯)

递归代码实现

递归解法分析

第五步:两种通用解法的比较

第六步:优化与扩展

[1. 处理边界情况](#1. 处理边界情况)

[2. 性能优化](#2. 性能优化)

[3. 扩展到其他问题](#3. 扩展到其他问题)

总结


问题回顾

给定一个仅包含数字2-9的字符串,返回所有它能表示的字母组合。数字到字母的映射如下:

  • 2: abc

  • 3: def

  • 4: ghi

  • 5: jkl

  • 6: mno

  • 7: pqrs

  • 8: tuv

  • 9: wxyz

示例:

  • 输入:"23"

  • 输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]

第一步:有限处理(长度1或2)

思路分析

当我们刚开始接触这个问题时,可能会先考虑简单情况。最直观的思路是:根据输入长度分别处理。

  • 如果输入长度为1,直接返回该数字对应的所有字母

  • 如果输入长度为2,使用两层循环生成所有组合

  • 对于长度大于2的情况...暂时忽略或简单处理

代码实现

cpp

复制代码
#include <string>
#include <vector>
using namespace std;

class Solution {
public:
    vector<string> letterCombinations(string digits) {
        string str[10] =  {"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
        vector<string> ret;
        
        if (digits.empty()) 
            return ret;  // 空字符串返回空数组
        
        int len = digits.size();

        // 情况1:输入长度为1
        if(len == 1)
        {
            string aa = str[digits[0] - '0'];
            for(auto e : aa)
            {
                ret.push_back(string(1,e));            
            }
            return ret;
        }

        // 情况2:输入长度为2
        if(len == 2)
        {
            string array1 = str[digits[0] - '0'];
            string array2 = str[digits[1] - '0'];

            for(auto e1 : array1)
            {
                for(auto e2 : array2)
                {
                    ret.push_back(string(1, e1) + e2);
                }
            }
            return ret;
        }

        // 长度大于2的情况没有处理
        return ret; // 对于更长的输入,这会返回错误结果
    }
};

这种方法的局限性

  1. 不通用:只能处理特定长度的输入

  2. 代码重复:每个长度都需要独立的逻辑

  3. 可维护性差:新增长度需要修改代码

  4. 边界情况处理困难:比如输入"0"或"1"时

第二步:发现问题,寻找通用模式

仔细观察有限处理的代码,我们可以发现一个模式:

  1. 对于长度为1的情况:遍历第一个数字的所有字母

  2. 对于长度为2的情况:遍历第一个数字的所有字母 × 第二个数字的所有字母

实际上,这是一个组合扩展的过程。无论输入多长,我们都可以:

  • 从空字符串开始

  • 依次处理每个数字

  • 将现有结果与当前数字的每个字母组合

这种思路启发我们找到通用解法。

第三步:通用解法(迭代构建)

算法思路

我们可以采用**广度优先搜索(BFS)**的思想,逐位扩展:

  1. 初始化结果列表,包含一个空字符串

  2. 遍历输入字符串的每个数字:

    • 获取该数字对应的字母集合

    • 创建临时列表存储新组合

    • 将现有组合与每个字母拼接,生成新组合

    • 用新组合更新结果

  3. 返回最终结果

通用代码实现

cpp

复制代码
#include <string>
#include <vector>
using namespace std;

class Solution {
public:
    vector<string> letterCombinations(string digits) {
        vector<string> ret;
        if (digits.empty()) return ret;
        
        // 数字到字母的映射
        string str[10] = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
        
        // 初始化一个空字符串作为起点
        ret.push_back("");
        
        // 遍历每个数字
        for (char digit : digits) {
            string letters = str[digit - '0'];
            vector<string> tmp;
            
            // 将现有组合与当前数字的每个字母组合
            for (const string& s : ret) {
                for (char letter : letters) {
                    tmp.push_back(s + letter);
                }
            }
            
            // 更新结果
            ret = tmp;
        }
        return ret;
    }
};

逐步解析通用解法

让我们通过具体例子来理解通用解法的执行过程:

输入:"23"

text

复制代码
初始状态:ret = [""]

处理第一个数字'2'(letters="abc"):
- 将""与"a","b","c"分别组合 → ["a", "b", "c"]
- ret更新为["a", "b", "c"]

处理第二个数字'3'(letters="def"):
- 将"a"与"d","e","f"组合 → ["ad", "ae", "af"]
- 将"b"与"d","e","f"组合 → ["bd", "be", "bf"]
- 将"c"与"d","e","f"组合 → ["cd", "ce", "cf"]
- ret更新为["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]

输入:"234"

text

复制代码
初始状态:ret = [""]

处理'2' → ["a", "b", "c"]
处理'3' → ["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]
处理'4' → 将9个组合分别与"g","h","i"组合 → 27个结果

第四步:另一种通用解法(递归/回溯)

除了迭代方法,递归也是一个很好的选择。递归解法体现了**深度优先搜索(DFS)**的思想:

递归代码实现

cpp

复制代码
class Solution {
private:
    vector<string> result;
    string mapping[10] = {"", "", "abc", "def", "ghi", "jkl", 
                          "mno", "pqrs", "tuv", "wxyz"};
    
    void backtrack(string& digits, int index, string& current) {
        // 终止条件:已处理完所有数字
        if (index == digits.size()) {
            result.push_back(current);
            return;
        }
        
        // 获取当前数字对应的字母
        string letters = mapping[digits[index] - '0'];
        
        // 遍历当前数字的所有字母
        for (char letter : letters) {
            current.push_back(letter);      // 做出选择
            backtrack(digits, index + 1, current);  // 递归处理下一个数字
            current.pop_back();             // 撤销选择
        }
    }
    
public:
    vector<string> letterCombinations(string digits) {
        if (digits.empty()) return {};
        string current;
        backtrack(digits, 0, current);
        return result;
    }
};

递归解法分析

递归解法的核心思想:

  1. 选择:将当前数字的一个字母添加到组合中

  2. 探索:递归处理下一个数字

  3. 撤销:回溯到之前的状态,尝试其他字母

这种方法与迭代方法在本质上相同,都是通过组合扩展来生成所有可能。

第五步:两种通用解法的比较

方面 迭代解法(BFS) 递归解法(DFS)
思路 从空字符串开始,逐层扩展 深度优先探索所有可能路径
代码结构 循环嵌套 递归函数
空间复杂度 O(4^n × n) O(n) 递归栈 + O(4^n × n) 结果
可读性 直观,容易理解过程 简洁,体现回溯思想
适用场景 适合迭代思维 适合递归思维,易于扩展

第六步:优化与扩展

1. 处理边界情况

通用解法已经能够处理各种边界情况:

  • 空输入:直接返回空结果

  • 包含"0"或"1":对应空字符串,不会产生新组合

  • 长输入:自动处理任意长度

2. 性能优化

对于特别长的输入,可以考虑以下优化:

  • 提前计算总组合数,预分配内存

  • 使用原地修改减少临时对象创建

3. 扩展到其他问题

这种"逐步构建组合"的思路可以应用于许多类似问题:

  • 生成所有可能的括号组合

  • 子集生成

  • 排列组合问题

总结

从只处理长度1或2的有限代码,到通用的迭代和递归解法,我们看到了算法设计思维的演进:

  1. 从特殊到一般:先解决简单情况,再寻找通用规律

  2. 模式识别:发现组合扩展的通用模式

  3. 抽象思维:将具体问题抽象为组合构建过程

  4. 多种解法:同一问题可以有不同解决思路

通过这个例子,我们不仅学会了解决电话号码字母组合问题,更重要的是掌握了"组合扩展"这一重要算法思想。这种思想在解决许多其他问题时同样有用,是算法学习中的一个重要里程碑。

在实际编程中,我们经常需要从有限的特例开始思考,然后逐步推广到通用情况。这个过程体现了算法设计的精髓:发现问题本质,找到通用规律,实现高效解决。

相关推荐
JJay.2 小时前
Android Kotlin 协程使用指南
android·开发语言·kotlin
MicrosoftReactor2 小时前
技术速递|使用 Copilot SDK 构建 AI 驱动的 GitHub Issue 分类系统
人工智能·github·copilot
csbysj20202 小时前
jQuery 捕获详解
开发语言
AI成长日志2 小时前
【GitHub开源项目专栏】AI推理优化框架深度解析(上):vLLM架构设计与核心实现
人工智能·开源·github
计算机安禾2 小时前
【数据结构与算法】第35篇:归并排序与基数排序
c语言·数据结构·vscode·算法·排序算法·哈希算法·visual studio
CV-deeplearning2 小时前
【开源】字节跳动开源 DeerFlow 2.0:一站式 SuperAgent 开发框架,GitHub 星标 5.9 万!
开源·github·deerflow·deerflow 2.0·superagent
私人珍藏库2 小时前
【Windows】PDF超能助手(1.0.13)
windows·pdf·工具·软件·多功能
仟人斩2 小时前
Windows 下把 VSCode 加入右键菜单(注册表方案)
windows·vscode·上下文菜单
C++ 老炮儿的技术栈2 小时前
GCC编译时无法向/tmp 目录写入临时汇编文件,因为设备空间不足,解决
linux·运维·开发语言·汇编·c++·git·qt