LeetCode 17:电话号码的字母组合——Java DFS 回溯法详解

一、题目描述

给定一个仅包含数字 2~9 的字符串,返回这些数字在电话按键上能够表示的所有字母组合,答案顺序不限。

数字和字母的对应关系如下:

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

例如输入:

复制代码
digits = "23"

数字 2 可以选择 a、b、c,数字 3 可以选择 d、e、f,因此结果为:

复制代码
["ad","ae","af","bd","be","bf","cd","ce","cf"]

这道题的关键不是按照某种规律计算字符串,而是把每个数字对应的字母全部组合出来。只要题目要求"列出所有可能",通常就可以考虑 DFS 与回溯。

二、核心思路:把组合过程看成决策树

假设输入为 "23",整个选择过程可以看成一棵决策树。

  • 第 0 层处理数字 2,可以选择 a、b、c

  • 第 1 层处理数字 3,可以选择 d、e、f

  • 当处理完全部数字时,就得到一个完整组合。

例如先选择 a,再选择 d,得到 ad;回到上一层后撤销 d,再尝试 ef,就能依次得到 aeaf

因此,每一层递归只需要完成三件事:

  1. 找到当前数字对应的字母集合;

  2. 依次选择其中一个字母,并递归处理下一个数字;

  3. 递归返回后撤销刚才的选择,继续尝试其他字母。

这就是回溯的经典过程:

复制代码
做出选择 -> 进入下一层 -> 撤销选择

DFS 负责沿着一条路径不断向下搜索,回溯则负责在一条路径搜索完成后恢复现场,使程序能够继续探索其他分支。

三、递归参数如何设计

定义递归方法:

复制代码
dfs(digits, index, path)

三个参数的含义分别是:

  • digits:原始数字字符串;

  • index:当前正在处理第几个数字;

  • path:当前已经选择的字母组合。

例如输入 "23"

复制代码
index = 0:处理数字 2
index = 1:处理数字 3
index = 2:所有数字处理完成

所以递归终止条件为:

复制代码
if (index == digits.length()) {
    result.add(path.toString());
    return;
}

index 等于数字字符串的长度时,说明每个数字都已经选择了一个字母,此时 path 就是一个完整答案。

四、数字与字母的映射

可以使用字符串数组保存电话按键映射:

复制代码
private final String[] mapping = {
    "", "", "abc", "def", "ghi",
    "jkl", "mno", "pqrs", "tuv", "wxyz"
};

数组下标直接对应数字。当前字符为 digits.charAt(index),减去字符 '0' 后即可得到真正的数字下标:

复制代码
String letters = mapping[digits.charAt(index) - '0'];

例如字符 '3' - '0' 的结果是整数 3,因此可以得到 mapping[3],也就是 "def"

五、完整 Java 代码

复制代码
import java.util.ArrayList;
import java.util.List;

class Solution {
    private final List<String> result = new ArrayList<>();

    private final String[] mapping = {
        "", "", "abc", "def", "ghi",
        "jkl", "mno", "pqrs", "tuv", "wxyz"
    };

    public List<String> letterCombinations(String digits) {
        if (digits == null || digits.length() == 0) {
            return result;
        }

        dfs(digits, 0, new StringBuilder());
        return result;
    }

    private void dfs(String digits, int index, StringBuilder path) {
        if (index == digits.length()) {
            result.add(path.toString());
            return;
        }

        String letters = mapping[digits.charAt(index) - '0'];

        for (int i = 0; i < letters.length(); i++) {
            path.append(letters.charAt(i));
            dfs(digits, index + 1, path);
            path.deleteCharAt(path.length() - 1);
        }
    }
}

六、以 "23" 为例推演

程序首先从 index = 0 开始,当前数字是 2,对应字母 abc

第一次循环选择 a

复制代码
path = "a"

随后递归到 index = 1,处理数字 3 对应的 def。依次选择 d、e、f,得到:

复制代码
ad、ae、af

生成 ad 后,程序执行:

复制代码
path.deleteCharAt(path.length() - 1);

d 删除,使 path 恢复为 "a",然后才能继续选择 e。数字 3 的所有字母处理完后,递归返回第一层,再删除 a,接着尝试 bc,最终得到全部九种组合。

整个过程可以概括为:

复制代码
选择 a -> 选择 d -> 保存 ad -> 删除 d
       -> 选择 e -> 保存 ae -> 删除 e
       -> 选择 f -> 保存 af -> 删除 f
删除 a,继续选择 b、c

七、为什么必须回溯

StringBuilder 是可变对象,所有递归层操作的是同一个 path。如果递归结束后不删除最后加入的字符,旧路径就会影响后续分支。

例如得到 ad 后,如果不删除 d,下一次追加 e 就会变成 ade,而不是正确的 ae

因此下面两行必须成对出现:

复制代码
path.append(letters.charAt(i));
path.deleteCharAt(path.length() - 1);

前者表示做出选择,后者表示撤销选择。回溯不是重新创建一套状态,而是在递归返回时把共享状态恢复到进入当前分支之前的样子。

另外,将答案加入结果集时需要调用 path.toString(),保存当前组合对应的字符串,而不是保存可继续变化的构造器对象。

八、复杂度分析

数字 79 各对应 4 个字母,其他数字对应 3 个字母。若输入长度为 n,组合数量最多为 4^n

  • 时间复杂度:最坏为 O(n × 4^n)。一共最多生成 4^n 个组合,每个组合转换为字符串需要 O(n)

  • 空间复杂度:不计算返回结果时为 O(n),主要来自递归栈和 StringBuilder;如果计算结果集,则需要 O(n × 4^n)

九、常见错误

1. 空字符串返回包含空串的集合

当输入为 "" 时,题目要求返回空列表,因此应在启动 DFS 前单独判断,不能让递归直接把空字符串加入结果集。

2. 递归时没有让 index + 1

每层递归处理一个数字。若仍传入原来的 index,程序会反复处理同一数字,无法正确终止。

3. 忘记删除最后一个字符

这是最常见的错误。每次递归返回后,都要撤销当前层刚刚追加的字符,保证同层不同分支互不影响。

4. 使用组合长度代替索引却混淆含义

本题中组合长度通常等于当前层数,但直接使用 index 表达"正在处理哪个数字"更清晰,也更不容易写错。

十、总结

这道题是 DFS 回溯的典型入门题。每个数字对应决策树的一层,每个字母对应当前层的一种选择:先把字母加入路径,再递归处理下一个数字,递归返回后删除刚加入的字母。当 index == digits.length() 时,说明已经走到叶子节点,可以保存一个完整组合。

相关推荐
2401_850481171 小时前
UVA10391
java
UQR1 小时前
一、Java基础高频面试题
java·开发语言
AC赳赳老秦1 小时前
网页公开附件自动采集:OpenClaw 批量下载页面内嵌 Word/Excel 附件,统一格式后结构化入库
java·python·word·php·excel·deepseek·openclaw
上海云盾商务经理杨杨1 小时前
Tomcat 弱配置漏洞渗透实战!批量 getshell 高频漏洞
java·web安全·tomcat
10mAh2 小时前
【Linux】error while loading shared libraries 怎么解决?——ldd、RPATH 与动态链接排错
java·linux·前端
深念Y2 小时前
stable-diffusion.cpp 的 FLUX.2 Klein 9B 分步
java·前端·数据库
阿pin2 小时前
Java随笔-红黑树
java·python·算法·红黑树
Hi李耶2 小时前
【LeetCode】17.电话号码的字母组合
算法·leetcode·职场和发展
我命由我123452 小时前
Jetpack Compose - @Preview 注解
android·java·java-ee·android studio·android jetpack·android-studio·android runtime