力扣hot100之17:电话号码的字母组合
力扣hot100之17:电话号码的字母组合

LeetCode 17:电话号码的字母组合(Letter Combinations of a Phone Number)题解

Letter Combinations of a Phone Number 是 LeetCode Hot 100 里面一道非常典型的回溯题。

这道题本身不难,代码量也不大,但它非常适合用来理解 回溯算法的基本写法。很多人第一次接触回溯的时候,都会对“递归到底在做什么”“路径怎么记录”“回退又是什么意思”这些问题感到有些抽象。而这道题因为场景直观、结构清晰,所以特别适合作为回溯入门题来掌握。

你给出的这份 Java 代码,就是这道题最经典的写法之一: 通过一个映射表,把数字映射成对应字母,然后使用递归一层层构造所有可能的字符串组合。

本文就结合这段代码,系统地讲一下这道题的解法思路。


题目介绍

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

数字和字母之间的映射关系与手机按键相同:

  • 2 对应 "abc"

  • 3 对应 "def"

  • 4 对应 "ghi"

  • 5 对应 "jkl"

  • 6 对应 "mno"

  • 7 对应 "pqrs"

  • 8 对应 "tuv"

  • 9 对应 "wxyz"

例如输入:

"23"

因为:

  • 2 可以表示 abc

  • 3 可以表示 def

所以最终所有组合就是:

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

题目要求我们把这些组合全部返回。


示例

示例 1

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

因为第一个位置有 3 种选择,第二个位置也有 3 种选择,所以总共有:

3 × 3 = 9

种组合。

示例 2

输入:digits = ""
输出:[]

如果输入是空字符串,就没有任何可组合的结果,因此直接返回空列表。

示例 3

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

因为数字 2 对应 "abc",所以答案就是这三个单字符字符串。


这道题为什么适合用回溯?

我们先从问题本质来看。

假设输入是:

digits = "23"

那么第一位可以从 "abc" 中选一个,第二位可以从 "def" 中选一个。

也就是说,我们需要:

  • 先决定第 1 个位置填什么

  • 再决定第 2 个位置填什么

  • 当所有位置都填完之后,就得到一个完整答案

这其实就是一个非常标准的“逐层决策”问题。

再比如输入:

"279"

那么流程就变成:

  • 第 1 层:从 2 -> abc 中选一个

  • 第 2 层:从 7 -> pqrs 中选一个

  • 第 3 层:从 9 -> wxyz 中选一个

每一层都要做选择,而所有路径走完之后,就能得到全部结果。

这正是回溯算法最擅长处理的场景。


核心思路

我们可以把整个过程想象成一棵树。

digits = "23" 为例:

  • 第一层可以选择 abc

  • 对于每一个选择,第二层又可以继续选择 def

整棵树展开之后,大概就是这样:

a -> d
   -> e
   -> f
b -> d
   -> e
   -> f
c -> d
   -> e
   -> f

每一条从根到叶子的路径,都是一个合法答案。

所以我们只需要写一个递归函数,不断往路径里添加字符;当路径长度等于 digits.length() 时,就说明一个组合已经构造完成,把它加入结果集中即可。


Java 代码

下面就是你给出的代码,我保留原始写法,并加上注释方便理解:

class Solution {
    String[] mapping = {"0", "1", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
    List<String> result = new ArrayList<>();

    public List<String> letterCombinations(String digits) {
        // 特殊情况:输入为空,直接返回空结果
        if (digits == null || digits.isEmpty()) return result;

        // 从下标 0 开始回溯,当前路径为空
        backtrack(digits, 0, new StringBuilder());
        return result;
    }

    private void backtrack(String digits, int index, StringBuilder sb) {
        // 如果已经处理到最后一个数字之后,说明当前组合构造完成
        if (index == digits.length()) {
            result.add(sb.toString());
            return;
        }

        // 获取当前数字
        char digit = digits.charAt(index);

        // 根据映射表找到它对应的字母
        String letters = mapping[digit - '0'];

        // 枚举当前数字对应的每一个字母
        for (char letter : letters.toCharArray()) {
            sb.append(letter);                  // 做选择
            backtrack(digits, index + 1, sb);  // 进入下一层
            sb.deleteCharAt(sb.length() - 1);  // 撤销选择
        }
    }
}

代码拆解

这段代码不长,但它把回溯的核心结构体现得非常完整。 如果你正在学回溯,这份代码其实很有代表性。


1. 映射表的作用

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

这里用一个字符串数组来保存数字和字母的对应关系。

比如:

  • mapping[2] = "abc"

  • mapping[3] = "def"

  • mapping[7] = "pqrs"

当我们拿到某个数字字符,比如 '2' 时,就可以通过:

mapping[digit - '0']

快速找到它对应的字母集合。

这里的 digit - '0',本质上是把字符数字转换成真正的整数下标。

例如:

  • '2' - '0' = 2

  • '7' - '0' = 7

这是 Java 里一个很常见的小技巧。


2. 为什么空字符串要直接返回?

if (digits == null || digits.isEmpty()) return result;

因为题目要求返回所有可能的字母组合。

如果输入本身就是空字符串,那么根本没有任何数字可以参与组合,自然也就没有结果。

所以这里直接返回空列表即可。

这一步虽然简单,但一定不能漏掉,否则有些测试用例会出问题。


3. backtrack 函数的含义是什么?

private void backtrack(String digits, int index, StringBuilder sb)

这个函数有三个参数:

  • digits:输入的数字字符串

  • index:当前处理到了第几个数字

  • sb:当前已经构造出的路径

换句话说,它表示的是:

当前已经确定了前 index 个位置的字符,现在要继续处理第 index 个数字。

这个定义非常关键。

只要你把它理解清楚,整个回溯流程就会变得很自然。


4. 什么时候递归结束?

if (index == digits.length()) {
    result.add(sb.toString());
    return;
}

index == digits.length() 时,说明所有数字都已经处理完了。

也就是说,当前这条路径已经构造成了一个完整答案。

这时候就应该把它加入结果集,然后返回上一层。

这就是回溯中的“终止条件”。


5. 当前层到底在做什么?

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

这里先拿到当前数字,再找到它对应的字母集合。

例如:

  • 如果当前数字是 '2'

  • 那么 letters = "abc"

接下来这一层要做的事情,就是从 "abc" 中依次选择一个字母,放到当前路径里。

这就是所谓的“枚举当前层的所有选择”。


6. 回溯三部曲

for (char letter : letters.toCharArray()) {
    sb.append(letter);
    backtrack(digits, index + 1, sb);
    sb.deleteCharAt(sb.length() - 1);
}

这一段就是最典型的回溯模板。

可以拆成三步来看:

第一步:做选择

sb.append(letter);

把当前字母加入路径。

第二步:递归进入下一层

backtrack(digits, index + 1, sb);

去处理下一个数字。

第三步:撤销选择

sb.deleteCharAt(sb.length() - 1);

把刚才加入的字符删掉,让路径恢复到进入这一层之前的状态。

这样下一轮循环时,才能尝试别的字母。

这一步就是“回溯”这个名字的来源。


手动模拟一遍

我们用最经典的例子来走一下流程:

digits = "23"

一开始调用:

backtrack("23", 0, "")

第 1 层

当前数字是:

'2'

对应字母:

"abc"

所以第一层有三个选择:

  • a

  • b

  • c


先选 a

路径变成:

"a"

然后递归进入下一层。

第 2 层

当前数字是:

'3'

对应字母:

"def"

于是继续有三个选择:

  • d,得到 "ad"

  • e,得到 "ae"

  • f,得到 "af"

当走到 "ad" 时,index == digits.length(),说明一个完整组合已经形成,加入结果集。

之后回退,再尝试 "ae""af"


再选 b

同理,会得到:

  • "bd"

  • "be"

  • "bf"

再选 c

会得到:

  • "cd"

  • "ce"

  • "cf"

最终结果就是:

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

为什么要用 StringBuilder

很多人看到这里会问,为什么不用普通的字符串拼接,而要用:

StringBuilder sb

原因主要有两个:

第一,效率更高

如果每次递归都用字符串拼接,比如:

path + letter

那么会频繁创建新的字符串对象,效率会差一些。

StringBuilder 可以在原有基础上直接追加和删除字符,更适合这种“不断修改路径”的场景。

第二,更符合回溯模型

回溯的本质就是:

  • 添加一个选择

  • 递归

  • 再撤销这个选择

StringBuilderappenddeleteCharAt 正好非常适合这种操作。


复杂度分析

假设输入字符串长度为 n

每个数字最多对应 4 个字母(比如 79)。

所以总的组合数量大约是:

3^n 或 4^n

更准确地说,时间复杂度可以写成:

O(4^n * n)

因为总共有这么多种组合,而每个组合生成时大约要处理 n 个字符。

空间复杂度

空间复杂度主要来自递归调用栈和路径构造:

O(n)

如果把最终结果集也算进去,那么结果所占空间会更大,但通常分析时单独讨论返回结果。


这道题真正想考什么?

这道题表面上是在考手机按键映射,实际上真正考的是:

你会不会写最基础的回溯模板。

因为它的结构特别标准:

  1. 先判断终止条件

  2. 找到当前层可选的元素

  3. 枚举每一种选择

  4. 做选择、递归、撤销选择

这几步几乎就是之后所有回溯题的共同骨架。

比如后面你再学到:

  • 全排列

  • 子集

  • 组合总和

  • N 皇后

你会发现它们的代码框架都和这道题非常像。

所以这道题虽然不算难,但非常值得认真理解。


总结

这道题的核心思路其实很简单:

  • 每个数字对应一组字母

  • 我们从左到右,依次为每个位置选择一个字母

  • 当所有位置都选完时,就得到一个完整答案

因此最适合的做法就是:

回溯 + 递归构造路径

这道题的关键点主要有三个:

  1. 用映射表保存数字到字母的对应关系

  2. index 表示当前处理到第几个数字

  3. StringBuilder 记录路径,并在递归后撤销选择

如果你刚开始学习回溯,这道题非常适合作为入门练习。 因为它没有复杂的剪枝,也没有特别绕的边界条件,重点非常纯粹,就是让你熟悉“路径、选择、递归、回退”这一整套流程。

把这道题真正写熟之后,再去做全排列、组合、子集这些题,你会明显感觉顺手很多。 因为电话号码的字母组合,本质上就是一棵标准的回溯搜索树。

如果对您有帮助的话,能否支持一下博主?
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇