今日算法(组合问题)(回溯解法)

题目描述

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按任意顺序返回。 数字到字母的映射与电话按键相同,1 不对应任何字母。

示例 1:

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

示例 2:

  • 输入:digits = "2"
  • 输出:["a","b","c"]

解法一:回溯法(递归)

核心思路

这道题本质上是多叉树的深度优先搜索(DFS)

  • 每个数字对应一组字母,相当于树的一层
  • 我们需要遍历每一层的所有字母,组合出所有可能的路径
  • 当路径长度等于输入数字的长度时,就得到了一个有效组合

代码实现(C++)

复制代码
class Solution {
public:
    vector<string> letterCombinations(string digits) {
        vector<string> res;
        if (digits.empty()) return res;

        // 数字到字母的映射表
        unordered_map<char, string> phoneMap = {
            {'2', "abc"},
            {'3', "def"},
            {'4', "ghi"},
            {'5', "jkl"},
            {'6', "mno"},
            {'7', "pqrs"},
            {'8', "tuv"},
            {'9', "wxyz"}
        };

        string path; // 保存当前路径
        backtrack(digits, 0, phoneMap, path, res);
        return res;
    }

private:
    // index:当前处理到第几个数字
    void backtrack(const string& digits, int index, 
                   const unordered_map<char, string>& phoneMap, 
                   string& path, vector<string>& res) {
        // 终止条件:路径长度等于数字长度
        if (index == digits.size()) {
            res.push_back(path);
            return;
        }

        // 获取当前数字对应的所有字母
        char digit = digits[index];
        const string& letters = phoneMap.at(digit);

        // 遍历所有可能的字母
        for (char letter : letters) {
            // 1. 选择当前字母
            path.push_back(letter);
            // 2. 递归处理下一个数字
            backtrack(digits, index + 1, phoneMap, path, res);
            // 3. 回溯:撤销选择,尝试下一个字母
            path.pop_back();
        }
    }
};

详细步骤解析

以输入 "23" 为例,模拟回溯过程:

  1. 初始状态path = ""index = 0(处理第一个数字 '2'
  2. 处理数字 '2' ,对应字母 "abc"
    • 'a'path = "a",递归处理下一个数字 index=1
      • 处理数字 '3',对应字母 "def"
        • 'd'path = "ad",长度等于 2,加入结果集
        • 'e'path = "ae",加入结果集
        • 'f'path = "af",加入结果集
      • 回溯:path = "a"
    • 'b'path = "b",递归处理下一个数字,生成 "bd", "be", "bf"
    • 'c'path = "c",递归处理下一个数字,生成 "cd", "ce", "cf"
  3. 最终结果 :所有组合都已加入 res,返回结果集。

复杂度分析

  • 时间复杂度 :\(O(3^N \times 4^M)\)
    • N 是对应 3 个字母的数字个数(2,3,4,5,6,8)
    • M 是对应 4 个字母的数字个数(7,9)
  • 空间复杂度 :\(O(N+M)\)
    • 递归栈深度等于输入数字的长度,路径 path 的最大长度也为 N+M

解法二:迭代法(队列模拟)

核心思路

用队列模拟组合的生成过程,每次取出队列中已有的组合,与当前数字的所有字母拼接,生成新的组合,再放回队列。

代码实现(C++)

复制代码
class Solution {
public:
    vector<string> letterCombinations(string digits) {
        vector<string> res;
        if (digits.empty()) return res;

        // 数字到字母的映射表
        vector<string> phoneMap = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};

        queue<string> q;
        q.push(""); // 初始空字符串

        for (char digit : digits) {
            int levelSize = q.size(); // 当前队列中的组合数量
            string letters = phoneMap[digit - '0'];

            // 处理当前层的所有组合
            for (int i = 0; i < levelSize; ++i) {
                string current = q.front();
                q.pop();

                // 拼接当前数字的所有字母,生成新组合
                for (char letter : letters) {
                    q.push(current + letter);
                }
            }
        }

        // 队列中剩余的元素就是所有组合
        while (!q.empty()) {
            res.push_back(q.front());
            q.pop();
        }

        return res;
    }
};

详细步骤解析

同样以输入 "23" 为例:

  1. 初始状态 :队列 q = [""]
  2. 处理数字 '2' (对应 "abc"):
    • 取出 "",拼接 "a", "b", "c",得到 "a", "b", "c",放回队列
    • 队列变为:["a", "b", "c"]
  3. 处理数字 '3' (对应 "def"):
    • 取出 "a",拼接 "d", "e", "f""ad", "ae", "af",放回队列
    • 取出 "b",拼接 "d", "e", "f""bd", "be", "bf",放回队列
    • 取出 "c",拼接 "d", "e", "f""cd", "ce", "cf",放回队列
    • 队列变为:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]
  4. 遍历队列,得到最终结果

两种解法对比

表格

对比维度 回溯法(递归) 迭代法(队列)
代码简洁度 更直观,逻辑清晰 稍复杂,需要手动维护队列
理解难度 符合人类思维,容易理解 需要理解组合生成的过程
空间复杂度 递归栈深度为数字长度 队列需要存储中间结果
适用场景 数字长度不大时(本题长度≤4) 无递归深度限制,适合所有场景

总结

  • 这道题是回溯算法的入门经典题,核心是理解 "选择 - 递归 - 回溯" 的过程。
  • 回溯法的关键是:用一个路径变量保存当前组合,递归处理每一层,处理完后撤销选择
  • 迭代法是回溯法的非递归实现,用队列模拟组合的生成过程,避免了递归栈溢出的风险。
相关推荐
imuliuliang5 小时前
关于数据结构在算法设计中的核心作用解析7
算法
2401_894915536 小时前
GEO 搜索优化完整源码从零部署:环境配置、集群搭建全流程
开发语言·python·tcp/ip·算法·unity
普通网友7 小时前
共识算法实现:从工作量证明到权益证明的演进
算法·区块链·共识算法
ldmd2849 小时前
地图生成算法(噪声篇-Perlin,Simplex,Value noise)
算法·go·地图生成
国科安芯9 小时前
FreeRTOS RISC-V 浮点上下文切换移植:在 IAR 工程中完整保存 FPU 寄存器
java·开发语言·单片机·嵌入式硬件·算法·系统架构·risc-v
小陈phd9 小时前
集成检索介绍
算法
小poop9 小时前
轮转数组:从暴力到最优,一题掌握算法复杂度分析
数据结构·算法·leetcode
樱桃读报僵尸10 小时前
说说 LLMRouter,Agent 执行过程中怎么动态的选择 LLM
算法
gis开发之家11 小时前
《Vue3 从入门到大神40篇》Vue3 源码详解(十):diff 算法全解析 —— 为什么 Vue3 比 Vue2 更快?
javascript·算法·typescript·前端框架·vue3·vue3源码