17. 电话号码的字母组合(Letter Combinations of a Phone Number)

难度:中等 | 主题:回溯——每层独立候选池

题目

给定一个仅包含数字 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"]
 

思路

先讲个故事:九宫格打字

老式诺基亚手机上,按 “23” 会怎样?

  • 按 2 → a/b/c 里选一个

  • 按 3 → d/e/f 里选一个

组合起来就是 3×3=9 种可能。每一个数字按键就是一层选择,每层的候选字母完全不同。

这和全排列的区别在于:全排列是同一堆东西排顺序,这里是每层从不同的池子里挑一个。


引导式推导:从树到模板

第 1 步:画决策树

输入 “23”:


graph TD

    root --> a["a"]

    root --> b["b"]

    root --> c["c"]

    a --> ad["ad"]

    a --> ae["ae"]

    a --> af["af"]

    b --> bd["bd"]

    b --> be["be"]

    b --> bf["bf"]

    c --> cd["cd"]

    c --> ce["ce"]

    c --> cf["cf"]

每个叶子路径就是一个完整组合。

第 2 步:看规律

  • 树深度 = 输入长度

  • 每层分支 = 当前数字对应的字母数

  • 叶子数 = 3^m × 4

第 3 步:模板化


flowchart TD

    A["digits 空?"] -->|"是"| B["return []"]

    A -->|"否"| C["backtrack(0)"]

    C --> D["index == len(digits)?"]

    D -->|"是"| E["记录组合"]

    D -->|"否"| F["遍历 phone[digits[index]]"]

    F --> G["选一个字母 → 递归 index+1"]

    G --> H["回溯撤销"]

    H --> F


和全排列的关键区别

维度全排列(46)字母组合(17)
候选池每层相同(全数组)每层不同(当前数字的字母)
去重方式used 数组index 参数控制深度
回溯参数start,有 usedindex,无 used

graph LR

    A["全排列<br/>同一筐苹果排顺序"] --> B["每层从筐里挑<br/>用了的放一边"]

    C["字母组合<br/>不同层不同水果篮"] --> D["第一层苹果篮<br/>第二层香蕉篮"]


代码

 
class Solution:
 
    def letterCombinations(self, digits: str) -> list[str]:
 
        if not digits:
 
            return []
 
        phone = {
 
            '2': 'abc', '3': 'def', '4': 'ghi',
 
            '5': 'jkl', '6': 'mno', '7': 'pqrs',
 
            '8': 'tuv', '9': 'wxyz'
 
        }
 
        res, path = [], []
 
        def backtrack(index: int):
 
            if index == len(digits):
 
                res.append(''.join(path))
 
                return
 
            for ch in phone[digits[index]]:
 
                path.append(ch)
 
                backtrack(index + 1)
 
                path.pop()
 
        backtrack(0)
 
        return res
 

复杂度

指标解释
时间O(3ᵐ × 4ⁿ)m=3字母键数, n=4字母键数
空间O(m+n)递归栈深度 + path

实战考量

频率分析

出现在:华为/字节 一面回溯入门题,约 20% 从这题开始热身。难度友好,主要看你能不能说清楚和全排列的区别

延伸思考

Q:为什么不需要 used 数组?

A:每层选的数字不同,天然不会重复选同一个元素。index 参数控制深度就够了。

Q:时间复杂度怎么算?

A:每个数字选一个字母,7 和 9 对应 4 个字母,其余 3 个。总组合数 = 3ᵐ × 4ⁿ。

Q:和全排列(46 题)的区别是什么?

A:排列每层候选池相同,需要 used 去重;本题每层候选池独立,只需要 index 控制深度。

Q:空输入为什么返回 [] 而不是 [""]

A:题目说 digits 长度为 0 时返回空列表。返回 [""] 会多一个空字符串组合,不符合题意。

Q:如果输入包含 0 或 1 呢?

A:按题目约定 digits 只包含 2-9,不需要处理。如果真遇到了,可以映射为空字符串然后跳过。

易错点

  • 空输入返回 [] 不是 [""]

  • res.append(''.join(path)) 把列表转字符串

  • path.pop() 不能忘


生活类比

九宫格点菜

每个按键是一个独立的菜系:2 号是川菜菜单、3 号是粤菜菜单……

你要从每个菜单里各选一道菜,凑一桌。

选完川菜的水煮鱼,不影响选粤菜的白切鸡——每层候选池互不干扰。

各层自治,互不相干——这就是”每层独立候选池”的回溯。


相关题目

题目关系
22括号生成回溯同族(条件放括号)
46全排列回溯基础(used 数组)
77组合回溯基础(start 参数)
17电话号码的字母组合本题

→ 返回题单:LeetCode学习路线图 > 十、回溯

速记卡(面试闪卡)

Q1:一句话讲清「17. 电话号码的字母组合(Letter Combinations of a Phone Number)」到底是什么?

A:按电话按键映射,用回溯从每层独立字母池里各挑一个,拼出所有组合。

Q2:一、题目与按键映射 —— 怎么理解?

A:像老诺基亚按 “23”,2 出 abc、3 出 def,组合出 9 种可能。输入数字串返回所有字母组合,1 无字母(Phone Keypad Map)。

Q3:二、每层独立候选池 —— 怎么理解?

A:像每层是不同的菜系菜单,各选一道凑一桌互不相干。与全排列不同:每层候选池独立,只需 index 控深度(Independent Candidate Pool)。

Q4:三、回溯模板 —— 怎么理解?

A:像沿着决策树往下走,选一个字母递归下一层,回来再撤销。index==len 记录结果,空输入返回 [] 而非 [""](Backtracking)。

Q5:四、复杂度与易错点 —— 怎么理解?

A:像组合数爆炸,时间 O(3ᵐ×4ⁿ) 空间 O(m+n)。易错在 path.pop() 不能忘、空输入返回 [](Time/Space Complexity)。

Q6:核心速记主线有哪些?

  • 每个数字映射一组字母,组合所有按键

  • 每层候选池独立,用 index 控深度无需 used

  • 回溯:选字母→递归→撤销 path.pop()

  • 时间 O(3ᵐ×4ⁿ),空输入返回 []

口诀

A:按键各一篮,

层层不相干;

回溯往下走,

组合全拼完。

相关链接