🔥个人主页:代码不加冰(欢迎来访)
🎬作者简介:java后端学习者
❄️个人专栏:LeetCode刷题日记 , 苍穹外卖日记,SSM框架深入,JavaWeb,
✨命运的结局尽可永在,不屈的挑战却不可须臾或缺!
前言:
大家好,我是代码不加冰,今天来到了我们每日的刷题环节,昨天因为考试,被耽误了一些,也算是正式进入期末周了,事情还是挺多的,抽空发,但还是会保持每日两更的,时间就像海绵里的水,挤挤总是有的,这真是鲁迅说的,哈哈。
摘要:
本文介绍了LeetCode第17题"电话号码的字母组合"的解题思路。题目要求根据数字字符串返回所有可能的字母组合,采用回溯算法解决多层循环问题。通过建立数字到字母的映射关系,使用StringBuilder高效构建组合字符串,并详细分析了回溯过程。代码实现了异常处理(空输入和非法字符),通过递归遍历所有可能的组合情况,最终将结果存入列表返回。时间复杂度为O(3^N ×4^M),其中N和M分别对应3字母和4字母的数字个数。
题目背景:17.电话号码的字母组合
给定一个仅包含数字
2-9的字符串,返回所有它能表示的字母组合。答案可以按任意顺序返回。给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = "23"输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]示例 2:
输入:digits = "2"输出:["a","b","c"]提示:
1 <= digits.length <= 4digits[i]是范围['2', '9']的一个数字。
题目解析:
从示例上来说,输入"23",最直接的想法就是两层for循环遍历了吧,正好把组合的情况都输出了。
如果输入"233"呢,那么就三层for循环,如果"2333"呢,就四层for循环.......
大家应该感觉出和前面遇到的一样的问题,就是这for循环的层数如何写出来,此时又是回溯法登场的时候了。
理解本题后,要解决如下三个问题:
- 数字和字母如何映射
- 两个字母就两个for循环,三个字符我就三个for循环,以此类推,然后发现代码根本写不出来
- 输入1 * #按键等等异常情况
数字和字母如何映射
可以使用map或者定义一个二维数组,例如:string letterMap[10],来做映射,我这里定义一个二维数组
回溯法来解决n个for循环的问题
例如:输入:"23",抽象为树形结构,如图所示:
图中可以看出遍历的深度,就是输入"23"的长度,而叶子节点就是我们要收集的结果,输出["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]。
整体逻辑
首先需要一个字符串s来收集叶子节点的结果,然后用一个字符串数组result保存起来,这两个变量我依然定义为全局。
再来看参数,参数指定是有题目中给的string digits,然后还要有一个参数就是int型的index。
注意这个index不是我们前面77.组合中的startIndex了。
这个index是记录遍历第几个数字了,就是用来遍历digits的(题目中给出数字字符串),同时index也表示树的深度。
例如输入用例"23",两个数字,那么根节点往下递归两层就可以了,叶子节点就是要收集的结果集。
那么终止条件就是如果index 等于 输入的数字个数(digits.size)了(本来index就是用来遍历digits的)。
然后收集结果,结束本层递归。
首先要取index指向的数字,并找到对应的字符集(手机键盘的字符集)。
然后for循环来处理这个字符集
以上就是这道题目的整体逻辑了,看着还是很清晰的。
然后就是具体的代码逻辑了,跟我们上面的逻辑一一对应即可
1.用数组存储
private static final String[] PHONE_MAP = { "", // 0 "", // 1 "abc", // 2 "def", // 3 "ghi", // 4 "jkl", // 5 "mno", // 6 "pqrs", // 7 "tuv", // 8 "wxyz" // 9 };2.边界情况的处理
if (digits == null || digits.length() == 0) { return result; }3.开始回溯
// 开始回溯 backtrack(result, new StringBuilder(), digits, 0); return result;StringBuilder在这里的作用是动态构建字符串,用来临时存储当前正在生成的字母组合。
核心作用
1. 高效拼接字符串
回溯过程中需要频繁地添加和删除字符,StringBuilder比普通字符串拼接效率高得多。
java // 使用 StringBuilder(高效) StringBuilder sb = new StringBuilder(); sb.append('a'); // 添加字符 sb.deleteCharAt(sb.length() - 1); // 删除最后一个字符 // 使用 String(低效,每次都会创建新对象) String str = ""; str += 'a'; // 创建新的 String 对象 str = str.substring(0, str.length() - 1); // 又创建新对象2. 在回溯中保存临时路径
看这个具体过程:
java // 初始状态 StringBuilder current = new StringBuilder(); // current = "" // 第1步:选择 'a' current.append('a'); // current = "a" // 第2步:选择 'd' current.append('d'); // current = "ad" // 到达底部,将 "ad" 加入结果 result.add(current.toString()); // 转换为字符串 "ad" // 回溯:删除 'd' current.deleteCharAt(current.length() - 1); // current = "a" // 尝试下一个字母 'e' current.append('e'); // current = "ae"3.然后就是回溯代码的执行
整体跟我们写的没什么区别,注意一下代码细节
digits.charAt(index) - '0'
将字符数字转换为整数
java '2' - '0' = 2 // 字符相减得到整数 '3' - '0' = 3 '9' - '0' = 9 // 原理:ASCII 码 // '0' 的 ASCII 是 48 // '2' 的 ASCII 是 50 // 50 - 48 = 2逐步执行的代码追踪
java // 初始调用 letterCombinations("23") ├─ result = [] ├─ digits = "23", length=2 └─ backtrack([], "", "23", 0) // 第1次调用 backtrack (index=0) ├─ index=0, digits.length()=2 → 继续 ├─ digit='2', letters="abc" ├─ 循环 i=0: current.append('a') → current="a" │ └─ 调用 backtrack([], "a", "23", 1) │ ├─ index=1 != 2 → 继续 │ ├─ digit='3', letters="def" │ ├─ 循环 i=0: current.append('d') → current="ad" │ │ └─ 调用 backtrack([], "ad", "23", 2) │ │ ├─ index=2 == digits.length() → result.add("ad") │ │ └─ 返回 │ ├─ current.deleteCharAt() → current="a" │ ├─ 循环 i=1: current.append('e') → current="ae" │ │ └─ 调用 backtrack([], "ae", "23", 2) │ │ ├─ index=2 == digits.length() → result.add("ae") │ │ └─ 返回 │ ├─ current.deleteCharAt() → current="a" │ ├─ 循环 i=2: current.append('f') → current="af" │ │ └─ 调用 backtrack([], "af", "23", 2) │ │ ├─ index=2 == digits.length() → result.add("af") │ │ └─ 返回 │ └─ current.deleteCharAt() → current="a" │ ├─ 循环 i=1: current.append('b') (类似过程) │ └─ 得到 "bd", "be", "bf" │ ├─ 循环 i=2: current.append('c') (类似过程) │ └─ 得到 "cd", "ce", "cf" │ └─ 最终 result = ["ad","ae","af","bd","be","bf","cd","ce","cf"]异常处理:
// 异常1: 输入为 null if (digits == null) { return result; // 或抛出异常:throw new IllegalArgumentException("Input cannot be null"); } // 异常2: 空字符串 if (digits.length() == 0) { return result; } // 异常3: 验证所有字符都是有效数字(2-9) for (int i = 0; i < digits.length(); i++) { char c = digits.charAt(i); if (c < '2' || c > '9') { // 可以选择抛出异常或直接返回空结果 throw new IllegalArgumentException("Invalid digit: " + c + ". Only digits 2-9 are allowed."); // 或者: return result; // 静默返回空结果 } }题目答案:
class Solution { private static final String[] PHONE_MAP = { "", // 0 "", // 1 "abc", // 2 "def", // 3 "ghi", // 4 "jkl", // 5 "mno", // 6 "pqrs", // 7 "tuv", // 8 "wxyz" // 9 }; public List<String> letterCombinations(String digits) { List<String> result = new ArrayList<>(); if (digits == null || digits.length() == 0) { return result; } backtrack(result, new StringBuilder(), digits, 0); return result; } private void backtrack(List<String> result, StringBuilder current, String digits, int index) { if (index == digits.length()) { result.add(current.toString()); return; } // 获取当前数字对应的字母字符串 String letters = PHONE_MAP[digits.charAt(index) - '0']; for (int i = 0; i < letters.length(); i++) { current.append(letters.charAt(i)); backtrack(result, current, digits, index + 1); current.deleteCharAt(current.length() - 1); } } }结语:
如果对你有帮助,请点赞,关注,收藏,你的支持就是我最大的鼓励!