LC 17 电话号码的字母组合:我的回溯算法,就是从这道题开窍的

刚接触回溯算法的时候,我总觉得这东西很 "玄学"------ 看着大佬写的代码短短几行,自己动手就处处卡壳。这道电话号码的字母组合,就是我的回溯启蒙题。说出来不怕你笑,我前前后后写了三遍才完全跑通,不是漏了边界就是递归参数写错。

题目说的啥

题意很简单,给你一串只包含 2-9 的数字,每个数字对应手机按键上的几个字母。你要返回所有可能的字母组合,顺序随便。比如输入"23",2 对应 abc,3 对应 def,两两组合下来就是 9 种结果。

一开始的笨想法

最开始我脑子没转过弯,想着不就是排列组合吗,写循环呗。可转念一想,数字长度是不固定的啊,最多有 4 位,难道我要写四层嵌套循环?太蠢了,而且扩展性极差,万一来个 10 位数字我总不能写 10 层吧。这个思路直接被我 pass 了。

回溯是怎么想到的

后来翻了翻算法笔记,才反应过来:这种 "多层循环但层数不固定" 的问题,天生就是回溯的主场。说白了,就是用递归帮我们 "自动" 叠循环层数。

每处理一个数字,就遍历它对应的所有字母,选一个字母加入当前路径,再递归处理下一个数字。等递归回来,再把刚才选的字母拿掉,换下一个,也就是常说的「做选择 - 递归 - 撤销选择」三步。

"23"举个例子:先处理第一位数字 2,有 a、b、c 三个选择。选 a 之后,再处理第二位数字 3,有 d、e、f 三个选择,拼出 ad、ae、af。选完一轮就退回去,把 a 拿掉,换 b 开头,再拼出 bd、be、bf,以此类推。其实就是深度优先遍历一棵决策树,走到叶子节点就收集答案。

我最常用的回溯写法

我现在刷回溯题,更喜欢用数组来存当前路径,配合 push 和 pop 做显式回溯。这种写法的模板感特别强,几乎所有组合、排列类题目都能套这个架子,写熟了上手特别快。

javascript 复制代码
var letterCombinations = function (digits) {
    // 空输入直接返回空数组!这个坑我第一次写必踩
    if (digits.length === 0) return []

    // 用二维数组做映射,下标直接对应数字,不用写对象
    const map = [[], [], ['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i'], 
                 ['j', 'k', 'l'], ['m', 'n', 'o'], ['p', 'q', 'r', 's'], 
                 ['t', 'u', 'v'], ['w', 'x', 'y', 'z']]
    const ans = []
    const n = digits.length
    // 用数组维护当前路径,比字符串拼接更有"回溯"那味儿
    const path = []

    // dfs函数:当前处理到第 i 个数字
    var dfs = function (i) {
        // 递归终点:所有数字都处理完了,把路径拼成字符串存起来
        if (i === n) {
            ans.push(path.join(''))
            return
        }
        // 取出当前数字对应的所有字母
        const cur = map[digits[i]]
        // 遍历每个字母,挨个选
        for (const c of cur) {
            // 1. 做选择:把当前字母加入路径
            path.push(c)
            // 2. 递归:处理下一个数字
            dfs(i + 1)
            // 3. 撤销选择:递归回来后,把刚才的字母拿掉,换下一个
            path.pop()
        }
    }

    // 从第 0 个数字开始搜
    dfs(0)
    return ans
}

说两个我踩过的坑:第一个就是空输入的判断。第一次写这种写法的时候我没加,空字符串输入居然返回了[""],直接 WA。别小看这个边界,面试的时候一紧张真的很容易漏掉。第二个就是映射表,7 和 9 是四个字母,我一开始图快写成三个了,测例"7"的时候少了一个结果,找了半天 bug。

复杂度

时间复杂度是 O (3^m * 4^n),m 是对应 3 个字母的数字个数,n 是对应 4 个字母的(也就是 7 和 9)。毕竟每个数字对应的字母都要遍历一遍,组合数就是这么多。空间复杂度主要是递归调用栈和 path 数组,深度就是数字的长度,最多 4 层,其实很小。

还有别的写法吗

除了这种显式回溯,也可以用字符串拼接的隐式写法,每次递归直接传拼接后的新字符串,不用手动 pop。两种思路本质完全一样,只是实现形式不同,我个人更偏爱数组 + pop 的写法,更贴合回溯的核心思想,也不容易乱。当然也能用 BFS 迭代一层层拼结果,不过回溯写起来最顺手,也最通用。

最后唠两句

这道题绝对是回溯算法的入门必刷题,没有复杂的剪枝,也没有烦人的去重,刚好能帮你理解 "递归代替多层循环" 和 "选择 - 撤销" 的核心思想。我自己最大的收获就是,终于摸透了回溯的通用模板 ------ 路径、选择列表、递归终止条件,再加三步选择法,大部分中等回溯题都能直接套。

你第一次接触回溯是刷的哪道题?有没有在递归边界或者撤销选择上栽过跟头?评论区聊聊你的踩坑经历,我每条评论都会看。如果觉得这篇对你有帮助,点个赞让更多小伙伴看到呀~

相关推荐
你怎么知道我是队长3 小时前
JavaScript的变量和数据类型介绍
开发语言·javascript·ecmascript
战族狼魂4 小时前
GPT-5.6与Grok 4.5重磅发布
人工智能·算法·大模型·大语言模型
白日焰火14 小时前
基于 OpenSpec 实现规范驱动开发
算法·交互
imuliuliang4 小时前
关于图搜索算法的性能建模与可预测性研究7
算法
名字还没想好☜5 小时前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司5 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能
心中有国也有家5 小时前
AtomGit Flutter 鸿蒙客户端:ModalBottomSheet 实战
android·javascript·学习·flutter·华为·harmonyos
ForDreamMusk6 小时前
批量归一化
人工智能·算法·机器学习
Ivanqhz6 小时前
刚体的自由度
人工智能·算法