2026/4/3 23:26:49
网站建设
项目流程
做资源共享网站,电商平台设计包括哪些内容,wordpress 媒体库多选,韶山seo快速排名Java版LeetCode热题100之电话号码的字母组合#xff1a;回溯算法的经典应用摘要#xff1a;本文将深入剖析 LeetCode 热题 100 中的经典回溯问题——电话号码的字母组合#xff08;Letter Combinations of a Phone Number#xff09;。我们将从题目出发#xff0c;系统讲解…Java版LeetCode热题100之电话号码的字母组合回溯算法的经典应用摘要本文将深入剖析 LeetCode 热题 100 中的经典回溯问题——电话号码的字母组合Letter Combinations of a Phone Number。我们将从题目出发系统讲解回溯算法的核心思想、递归结构设计并提供完整可运行的 Java 实现。文章涵盖时间/空间复杂度分析、常见面试问题、实际应用场景、相关题目推荐等模块帮助读者不仅掌握解题技巧更能理解其背后的算法思想与工程价值。一、原题回顾题目名称电话号码的字母组合题目编号LeetCode 17难度等级中等题目描述给定一个仅包含数字2-9的字符串返回所有它能表示的字母组合。答案可以按任意顺序返回。数字到字母的映射如下与电话按键相同2→abc3→def4→ghi5→jkl6→mno7→pqrs8→tuv9→wxyz注意数字1不对应任何字母。示例示例 1 输入digits 23 输出[ad,ae,af,bd,be,bf,cd,ce,cf] 示例 2 输入digits 2 输出[a,b,c] 示例 3 输入digits 输出[]提示1 digits.length 4digits[i]是范围[2, 9]的一个数字二、原题分析本题是一个典型的**笛卡尔积Cartesian Product**问题给定多个集合求它们所有可能的元素组合。例如23对应集合1{a,b,c}集合2{d,e,f}笛卡尔积{(a,d), (a,e), ..., (c,f)}由于digits.length ≤ 4且每个数字最多对应 4 个字母最大组合数为4⁴ 256属于可枚举范围适合使用回溯法Backtracking。关键观察这是一个多层嵌套循环问题但层数不固定由digits长度决定因此不能用固定层数的 for 循环解决而回溯法天然适合处理这种可变深度的组合问题。三、答案构思3.1 回溯法的核心思想回溯法是一种通过递归状态恢复系统搜索所有可能解的算法。对于本题路径Path当前已构建的字母组合如ad选择列表Choices当前数字对应的所有字母结束条件Base Case已处理完所有数字3.2 算法步骤预处理建立数字到字母的映射哈希表边界处理若输入为空直接返回空列表回溯函数若已处理完所有数字将当前组合加入结果否则获取当前数字对应的字母字符串遍历每个字母做选择将字母加入当前组合递归处理下一个数字撤销选择移除刚加入的字母状态恢复3.3 为什么用 StringBuffer频繁修改字符串回溯过程中需要不断添加和删除字符StringBuffer vs StringBuilder两者都可变但StringBuffer线程安全本题单线程用StringBuilder更高效官方题解用StringBuffer是历史原因优化建议实际开发中优先使用StringBuilder。四、完整答案Java 实现4.1 官方题解StringBuffer 版classSolution{publicListStringletterCombinations(Stringdigits){ListStringcombinationsnewArrayList();if(digits.length()0){returncombinations;}// 建立数字到字母的映射MapCharacter,StringphoneMapnewHashMapCharacter,String(){{put(2,abc);put(3,def);put(4,ghi);put(5,jkl);put(6,mno);put(7,pqrs);put(8,tuv);put(9,wxyz);}};backtrack(combinations,phoneMap,digits,0,newStringBuffer());returncombinations;}privatevoidbacktrack(ListStringcombinations,MapCharacter,StringphoneMap,Stringdigits,intindex,StringBuffercombination){// 结束条件已处理完所有数字if(indexdigits.length()){combinations.add(combination.toString());}else{chardigitdigits.charAt(index);StringlettersphoneMap.get(digit);// 遍历当前数字对应的所有字母for(inti0;iletters.length();i){// 做选择combination.append(letters.charAt(i));// 递归处理下一个数字backtrack(combinations,phoneMap,digits,index1,combination);// 撤销选择关键combination.deleteCharAt(index);}}}}4.2 优化版StringBuilder 静态映射classSolution{// 静态映射避免每次创建privatestaticfinalString[]LETTERS{,,abc,def,ghi,jkl,mno,pqrs,tuv,wxyz};publicListStringletterCombinations(Stringdigits){ListStringresultnewArrayList();if(digitsnull||digits.isEmpty()){returnresult;}backtrack(digits,0,newStringBuilder(),result);returnresult;}privatevoidbacktrack(Stringdigits,intindex,StringBuilderpath,ListStringresult){if(indexdigits.length()){result.add(path.toString());return;}// 获取当前数字对应的字母StringlettersLETTERS[digits.charAt(index)-0];for(charc:letters.toCharArray()){path.append(c);// 做选择backtrack(digits,index1,path,result);// 递归path.deleteCharAt(path.length()-1);// 撤销选择}}}✅优化点使用String[]替代HashMap访问更快使用StringBuilder替代StringBuffer边界检查更严谨null处理五、代码分析5.1 回溯函数详解参数含义digits原始数字字符串index当前处理到的数字索引path当前构建的字母组合可变字符串result结果列表递归过程以digits 23为例初始: path, index0 ├─ 选 a: patha, index1 │ ├─ 选 d: pathad, index2 → 加入结果 │ ├─ 选 e: pathae, index2 → 加入结果 │ └─ 选 f: pathaf, index2 → 加入结果 ├─ 选 b: pathb, index1 │ ├─ 选 d: pathbd, index2 → 加入结果 │ ... └─ 选 c: pathc, index1 ...状态恢复的关键path.deleteCharAt(path.length()-1);为什么不是deleteCharAt(index)因为path的长度始终等于index所以path.length() - 1 index - 1不实际上path.length() index因为每层递归添加一个字符所以path.length() - 1 index - 1是错误的纠正在官方题解中combination.deleteCharAt(index)是正确的因为初始时combination为空第 0 层添加字符后combination长度为 1要删除索引 0第 1 层添加后长度为 2要删除索引 1所以删除位置确实是index但在优化版中我们使用path.length() - 1因为path始终只包含已选择的字符当前要删除的就是最后一个字符两种方式都正确但优化版更直观。5.2 边界情况处理空输入digits → 返回空列表非null单数字digits 2→ 返回[a,b,c]含 7/9digits 79→ 每个数字对应 4 个字母5.3 映射表的设计方案对比方案优点缺点HashMapCharacter, String可读性强需装箱/拆箱稍慢String[]访问快无装箱需处理索引偏移switch-case无额外空间代码冗长推荐String[]方案性能最优。六、时间复杂度与空间复杂度分析6.1 时间复杂度O(3ᵐ × 4ⁿ)m对应 3 个字母的数字个数2,3,4,5,6,8n对应 4 个字母的数字个数7,9总组合数3ᵐ × 4ⁿ每种组合的构建成本O(mn)字符串拼接但通常简化为 O(3ᵐ × 4ⁿ)因为 mn ≤ 4最坏情况digits 7777→ 4⁴ 256 种组合6.2 空间复杂度O(mn)递归栈深度mn等于digits.length()每层栈空间O(1)局部变量结果集空间O((mn) × 3ᵐ × 4ⁿ)但通常不计入额外空间映射表O(1)固定大小StringBuilderO(mn)当前路径标准答案的空间复杂度为 O(mn)仅考虑算法运行所需额外空间。七、常见问题解答FAQQ1: 为什么不用 BFS答可以用但回溯更自然BFS用队列存储中间结果每层扩展回溯直接构建完整结果空间更省本题深度小≤4两者差异不大但回溯代码更简洁Q2: 能否用迭代非递归实现答可以逐个数字扩展结果publicListStringletterCombinations(Stringdigits){if(digits.isEmpty())returnnewArrayList();ListStringresultArrays.asList();String[]letters{,,abc,def,ghi,jkl,mno,pqrs,tuv,wxyz};for(chardigit:digits.toCharArray()){ListStringnewResultnewArrayList();for(Stringprefix:result){for(charc:letters[digit-0].toCharArray()){newResult.add(prefixc);}}resultnewResult;}returnresult;}优点无递归易理解缺点频繁创建新字符串内存开销大Q3: 如何按字典序输出答本题天然按字典序输出因为数字按顺序处理每个数字的字母按abc顺序遍历例如23→[ad,ae,af,bd,...]已是字典序Q4: 如果数字包含 ‘0’ 或 ‘1’ 怎么办答题目保证只含 ‘2’-‘9’但实际可扩展0→ 空格1→空字符串处理时跳过空字符串的数字八、优化思路8.1 预分配结果集大小计算总组合数预设ArrayList容量// 计算总组合数inttotal1;for(charc:digits.toCharArray()){total*LETTERS[c-0].length();}ListStringresultnewArrayList(total);避免动态扩容的内存拷贝。8.2 使用 char[] 代替 StringBuilder减少对象创建privatevoidbacktrack(Stringdigits,intindex,char[]path,ListStringresult){if(indexdigits.length()){result.add(newString(path));return;}StringlettersLETTERS[digits.charAt(index)-0];for(inti0;iletters.length();i){path[index]letters.charAt(i);backtrack(digits,index1,path,result);}}调用char[]pathnewchar[digits.length()];backtrack(digits,0,path,result);优点无 append/delete 开销缺点代码稍复杂8.3 并行化不推荐最大 256 个结果串行已足够快并行 overhead 远大于收益递归结构难以并行九、数据结构与算法基础知识点回顾9.1 什么是回溯算法定义一种通过递归剪枝系统搜索解空间的算法适用场景组合、排列、子集、N皇后、数独等核心要素选择Choose约束Constraint目标Goal9.2 回溯 vs DFS特性回溯DFS目的找所有解遍历图/树状态需显式维护和恢复通常无需恢复剪枝常见提前终止无效分支较少数据结构通用图/树回溯可视为带状态恢复的 DFS。9.3 笛卡尔积Cartesian Product定义两个集合 A 和 B 的笛卡尔积是所有有序对 (a,b) 的集合扩展多个集合的笛卡尔积应用数据库 JOIN、组合生成、测试用例生成9.4 决策树模型以23为例 / | \ a b c /|\ /|\ /|\ ad...af ... cf树深度digits.length叶节点数总组合数回溯 从根到叶的路径探索 返回十、面试官提问环节模拟Q1: 请手写电话号码的字母组合并解释回溯的过程。答写出优化版代码后解释我们用index表示当前处理的数字位置对每个数字遍历其对应的所有字母将字母加入当前路径递归处理下一个数字返回后移除该字母尝试下一个字母当index等于digits.length()时得到一个完整组合Q2: 时间复杂度为什么是 O(3ᵐ × 4ⁿ)能否优化答每个数字 2-6,8 对应 3 个字母7,9 对应 4 个总组合数 3ᵐ × 4ⁿ必须全部生成无法优化时间复杂度因为必须输出所有组合但可优化常数因子如预分配空间、用 char[]Q3: 如果输入很长如 20 位还能用回溯吗答不能。4²⁰ ≈ 10¹²远超计算能力此时应使用生成器模式按需生成组合或问题本身有特殊约束可剪枝如只求前 k 个Q4: 如何修改代码以支持数字 ‘0’ 和 ‘1’答扩展映射表0 → ,1 → 在回溯时若字母字符串为空直接递归下一层或预处理输入过滤掉 ‘0’/‘1’Q5: 回溯中的“状态恢复”为什么重要答因为回溯是共享状态的递归。若不恢复后续分支会基于错误的状态计算例如第一次选择 ‘a’ 后未移除第二次选择 ‘b’ 时路径变成 “ab”导致结果错误如 “abd” 而非 “bd”十一、这道算法题在实际开发中的应用11.1 输入法预测用户输入数字序列预测可能的单词如 T9 输入法老式手机11.2 密码恢复已知密码的数字模式如生日生成所有可能字母组合用于安全测试需授权11.3 测试用例生成API 参数有多个选项生成所有参数组合例如{color: [red, blue], size: [S, M, L]}11.4 配置组合软件配置项的笛卡尔积用于兼容性测试或默认配置生成11.5 自然语言处理语音识别中的候选词生成拼写纠错的可能替换组合 虽然实际中很少直接处理电话号码但笛卡尔积生成是许多系统的底层需求。十二、相关题目推荐题号题目难度关联点78子集中等回溯生成组合90子集 II中等含重复元素77组合中等回溯生成固定大小组合39组合总和中等可重复选择 目标和40组合总和 II中等不可重复 去重46全排列中等回溯生成排列22括号生成中等有效括号的回溯51N 皇后困难经典回溯问题784字母大小写全排列中等类似笛卡尔积1286字母组合迭代器中等按需生成组合学习路径建议掌握本题笛卡尔积→ 78子集→ 77固定大小组合→ 39/40带目标和→ 46排列十三、总结与延伸13.1 核心收获电话号码问题是笛卡尔积的经典应用回溯法是解决可变深度组合问题的标准方法状态恢复是回溯算法的关键时间复杂度由输出规模决定无法优化13.2 延伸思考如何按需生成组合迭代器模式实现IteratorString接口内部维护当前组合的索引数组next()方法计算下一个组合适用于大数据量场景支持通配符如digits 2**表示任意数字需先展开通配符再生成组合或在回溯时动态处理并行生成对第一个数字的字母并行处理每个线程负责一个前缀但小数据量下不划算13.3 学习建议动手实现手写回溯模板选择/递归/撤销理解状态明确“路径”、“选择列表”、“结束条件”掌握笛卡尔积这是组合生成的基础刷相关题巩固回溯在不同场景的应用思考扩展如支持更多字符、按需生成等结语电话号码的字母组合虽是一道“简单”的回溯题却完美展示了如何将现实问题转化为算法模型。掌握它不仅是为了解决这一道题更是为了建立起解决更复杂组合问题的能力。希望本文能助你透彻理解回溯算法在面试与实战中游刃有余