LeetCode 17. 电话号码的字母组合

题目描述

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

答案可以按任意顺序返回。

数字到字母的映射与电话按键相同:

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

注意:1 不对应任何字母。

例如:

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

初始思路

一开始我把这题当成了全排列问题处理。

我的想法是:用 onPath 记录已经选择过的字母,然后在每一层递归中遍历所有 digits 对应的字母,避免同一个字母重复选择。

这个思路的问题在于:它套用了全排列模板,但这题不是全排列。

全排列关注的是:

复制代码
从一堆候选元素里选出一个排列,每个元素通常只能用一次。

而电话号码的字母组合关注的是:

复制代码
每个数字位置,只能从这个数字对应的字母中选一个。

所以这题不需要 onPath,也不应该每层遍历所有数字。

解题思路

这题的关键是先明确递归函数的含义。

定义:

复制代码
dfs(i):当前正在决定 digits[i] 这一位应该选择哪个字母

对于 digits = "23"

复制代码
第 0 位数字是 2,只能从 "abc" 中选一个
第 1 位数字是 3,只能从 "def" 中选一个

搜索过程是:

复制代码
a -> d/e/f
b -> d/e/f
c -> d/e/f

也就是每一层只处理当前位置 digits[i],而不是重新遍历所有数字。

递归流程:

复制代码
1. 如果 digits 为空,直接返回空列表
2. dfs(i) 表示正在决定第 i 位数字对应的字母
3. 找到 digits[i] 对应的字符串 letters
4. 遍历 letters 中的每个字符 c
5. 把 c 放入 path[i]
6. 递归 dfs(i + 1)
7. 当 i == digits.length 时,path 已经填满,加入答案

代码实现

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

    List<String> ans = new ArrayList<>();

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

        char[] path = new char[digits.length()];
        dfs(digits.toCharArray(), 0, path);
        return ans;
    }

    public void dfs(char[] digits, int i, char[] path) {
        if (i == digits.length) {
            ans.add(new String(path));
            return;
        }

        int idx = digits[i] - '0';
        for (char c : mapping[idx].toCharArray()) {
            path[i] = c;
            dfs(digits, i + 1, path);
        }
    }
}

为什么不用 onPath

onPath 常用于全排列问题,用来表示某个元素在当前路径里是否已经被使用过。

比如全排列中:

复制代码
nums = [1, 2, 3]

同一个排列里,1 不能重复使用。

但这题不是这样。每一位数字都独立选择一个对应字母。

比如:

复制代码
digits = "22"

合法结果包括:

复制代码
aa, ab, ac, ba, bb, bc, ca, cb, cc

如果使用 onPath 禁止重复字母,aabbcc 就会被错误排除。

所以这题的核心不是"字母能不能重复使用",而是:

复制代码
当前位置的数字,决定了当前位置可以选择哪些字母。

易错点

1. 把题目误套成全排列模板

错误方向是:

复制代码
每一层遍历所有 digits,再遍历每个 digit 对应的字母。

这样会打乱数字位置和字母选择之间的关系。

正确方向是:

复制代码
第 i 层只处理 digits[i]

2. 错误使用 onPath

这题不需要记录某个字母是否已经选过。

每个数字位置只负责选自己的字母,递归进入下一层时自然会处理下一个数字。

3. 漏掉空字符串特判

digits = "" 时,题目要求返回:

复制代码
[]

如果不特判,递归一开始就会满足:

复制代码
i == digits.length

然后把空字符串加入答案,返回:

复制代码
[""]

这是不符合题意的。

复杂度分析

n = digits.length()

每个数字最多对应 4 个字母,所以组合数量最多是 4^n

  • 时间复杂度:O(n * 4^n)。最多有 4^n 个组合,每个组合转成字符串需要 O(n)
  • 空间复杂度:O(n)。递归栈和 path 长度都是 n;如果把返回结果也计入空间,则为 O(n * 4^n)

复盘

这题最重要的是不要把所有回溯题都套成同一个模板。

全排列的模型是:

复制代码
每一层从所有未使用元素中选一个。

电话号码字母组合的模型是:

复制代码
每一层只处理当前位置的数字,从这个数字对应的字母中选一个。

所以递归定义应该从"当前处理第几个数字"出发:

复制代码
dfs(i):决定 digits[i] 这一位的字母

只要这个定义清楚,path[i] = cdfs(i + 1)i == digits.length 这些代码就都很自然。

Tips

这题可以记住一句话:

复制代码
一个数字位置选一个对应字母,不是从所有字母里做排列。

遇到回溯题时,先判断当前层到底是在"填位置",还是在"选或不选",不要直接套模板。

相关推荐
hhzz14 小时前
机器学习-算法模型系列文章:07-SVM 一巴掌拍出来的超平面:SVM核函数选错,再干净的数据也救不了你
算法·机器学习·支持向量机
geovindu14 小时前
java: Gale-Shapley Algorithm
java·开发语言·后端·算法
冻柠檬飞冰走茶14 小时前
PTA基础编程题目集 7-34 通讯录的录入与显示(C语言实现)
c语言·开发语言·数据结构·算法
zander25814 小时前
LeetCode 79. 单词搜索
算法·深度优先
老洋葱Mr_Onion14 小时前
【C++】高精度模板
开发语言·c++·算法
脚踏实地皮皮晨15 小时前
003003002_WPF Grid 基类官方类定义逐行深度解析
开发语言·windows·算法·c#·wpf·visual studio
aaaameliaaa1 天前
字符函数和字符串函数
c语言·笔记·算法
城管不管1 天前
ReAct、Plan-and-Execute、Reflection 三大智能 Agent 范式核心区别
java·人工智能·算法·spring·ai·动态规划
月疯1 天前
二分法算法(水平等分图形面积)
算法
豆瓣鸡1 天前
算法日记 - Day3
java·开发语言·算法