LeetCode 17电话号码的字母组合:回溯算法(DFS)详解

一、 题目来源与描述

题目来源:LeetCode 第 17 题 - 电话号码的字母组合 (Medium)

题目描述 :

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

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

cpp 复制代码
2: abc   3: def
4: ghi   5: jkl   6: mno
7: pqrs  8: tuv   9: wxyz

示例:

输入:digits = "23"

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

二、 核心算法:回溯法(Backtracking / DFS)

1. 思路解析

这道题本质上是一道排列组合问题。每一个数字对应多个字母,我们需要从每个数字对应的字母集合中挑选一个字母,拼接成一个完整的字符串。

由于字符串的长度不定(最多为 4),且需要枚举所有可能的组合,暴力循环(嵌套 for 循环)无法动态适应长度 。因此,我们需要使用回溯算法(深度优先搜索 DFS)。

回溯法的核心思想:

将问题抽象为一棵决策树(N 叉树)。

  • 树的深度 :由输入字符串 digits 的长度决定。

  • 节点的分支:由当前数字对应的字母数量决定(3 或 4 个分支)。

我们通过递归沿着树的深度遍历,当到达叶子节点(即处理完所有数字)时,记录当前路径,然后"回溯"到上一层,尝试其他分支。

2. 回溯三步曲

  1. 确定递归函数的参数与返回值:

    • 参数需要包含:给定的数字字符串 digits,以及一个索引 index 用来标记当前遍历到了哪一个数字。

    • 返回值:不需要返回值,结果直接存入全局变量(或类成员变量)中。

  2. 确定终止条件:

    • 当 index == digits.size() 时,说明已经处理完了所有的数字,此时将当前拼接好的字符串加入结果集,并结束本层递归。
  3. 确定单层遍历逻辑:

    • 获取当前 index 指向的数字对应的字母集 letters。

    • 使用 for 循环遍历 letters:

      • 将当前字母加入当前路径 path。

      • 递归调用下一层,index + 1。

      • 回溯操作 :将当前字母从 path 中移除,以便尝试下一个字母。

三、 代码实现 (C++)

cpp 复制代码
class Solution {
public:
    // 1. 建立数字到字母的映射表 (使用数组下标直接映射,效率极高)
    const string phone_map[10] = {
        "",     // 0
        "",     // 1
        "abc",  // 2
        "def",  // 3
        "ghi",  // 4
        "jkl",  // 5
        "mno",  // 6
        "pqrs", // 7
        "tuv",  // 8
        "wxyz"  // 9
    };

    // 存放最终结果和当前路径的成员变量
    vector<string> result;
    string current_path;

    // 2. 回溯函数
    void backtracking(const string& digits, int index) {
        // 终止条件:index 走到头了,说明生成了一个完整组合
        if (index == digits.size()) {
            result.push_back(current_path);
            return;
        }

        // 获取当前数字对应的字符集
        int digit = digits[index] - '0'; // 字符转整数
        string letters = phone_map[digit];

        // 遍历当前数字对应的所有字母
        for (int i = 0; i < letters.size(); i++) {
            current_path.push_back(letters[i]);       // 处理节点:加入
            backtracking(digits, index + 1);          // 递归:进入下一层
            current_path.pop_back();                  // 回溯:撤销处理
        }
    }

    // 3. 主函数
    vector<string> letterCombinations(string digits) {
        // 每次调用前清空全局状态
        result.clear();
        current_path.clear();

        // 特殊情况:如果输入为空,直接返回空数组
        if (digits.empty()) {
            return result;
        }

        backtracking(digits, 0);
        return result;
    }
};

四、 复杂度分析

时间复杂度:O(3M×4N)

其中M是输入中对应 3 个字母的数字个数(例如 2, 3, 4, 5, 6, 8),N是输入中对应 4 个字母的数字个数(例如 7, 9),M+N 是输入数字的总个数。

一共有 3M×4N种组合,每种组合需要 O(1)的时间拼接字符串(在 C++ 中 push_back 和 pop_back 均摊为 O(1))。

空间复杂度:O(M+N)

主要取决于递归调用栈的深度,最大深度为输入字符串的长度(即 M+N)。此外,结果集 result 的空间不计入算法的辅助空间复杂度。

五、 递归与回溯模板总结

本题是标准的回溯模板:

cpp 复制代码
void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果;
    }
}
相关推荐
apcipot_rain11 小时前
C++转Java日记(03)
java·开发语言·c++
布莱克60511 小时前
C++ 中 map 的 insert 与 emplace:key 已存在时会覆盖吗?
开发语言·数据结构·c++·算法·map
xys_67812 小时前
深挖C++:类和对象(中)
开发语言·c++
D e re k ZH14 小时前
C++ 系统学习手记:从语法细节到工程实践
开发语言·数据结构·c++·算法
石小石Orz15 小时前
AI 正在蚕食我们的编码能力
前端·后端·算法
xiaoye-duck15 小时前
《C++ 项目:手写大模型接入 SDK》Ollama接口设计(上):初始化、全量响应实现和测试
开发语言·网络·c++·语言模型
zaizai100716 小时前
【Effective Modern C++】学习第一章
c++
Fruiticecake16 小时前
C 程序员速通现代 C++(一):lambda、std::function、移动语义与智能指针
c语言·c++
民乐团扒谱机17 小时前
【微实验】NSGA-II(非支配排序遗传算法)——解决多目标优化问题的“自然选择模拟器”
linux·人工智能·算法