这是我参与8月更文挑战的第17天,活动详情查看:8月更文挑战
题目
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入: digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
示例 2:
输入: digits = ""
输出: []
示例 3:
输入: digits = "2"
输出: ["a","b","c"]
提示:
0 <= digits.length <= 4digits[i]是范围['2', '9']的一个数字。
解题思路
思路1
其实就是将数字串“翻译”成字母串,找出所有的翻译可能。
翻译第一个数字有 3 / 4 种选择,翻译第二个数字又有 3 / 4 种选择……
从首位翻译到末位,会展开成一棵递归树。指针 i 是当前考察的字符的索引。
当指针越界时,此时生成了一个解,加入解集,结束当前递归,去别的分支,找齐所有的解。
时间复杂度:遍历所有节点,最坏的情况去到 O(4n)O(4^n)O(4n),nnn 为数字串的长度。
空间复杂度:O(n)O(n)O(n),递归栈调用的深度。
代码
const letterCombinations = (digits) => {
if (digits.length == 0) return [];
const res = [];
const map = { '2': 'abc', '3': 'def', '4': 'ghi', '5': 'jkl', '6': 'mno', '7': 'pqrs', '8': 'tuv', '9': 'wxyz' };
// dfs: 当前构建的字符串为curStr,现在“翻译”到第i个数字,基于此继续“翻译”
const dfs = (curStr, i) => { // curStr是当前字符串,i是扫描的指针
if (i > digits.length - 1) { // 指针越界,递归的出口
res.push(curStr); // 将解推入res
return; // 结束当前递归分支
}
const letters = map[digits[i]]; // 当前数字对应的字母
for (const letter of letters) { // 一个字母是一个选择,对应一个递归分支
dfs(curStr + letter, i + 1); // 选择翻译成letter,生成新字符串,i指针右移继续翻译(递归)
}
};
dfs('', 0); // 递归的入口,初始字符串为'',从下标0开始翻译
return res;
};
我所理解的回溯
回溯本质是暴力搜索,在问题的解空间树中,用 DFS 的方式,从根节点出发搜索整个解空间。
如果要找出所有的解,则要搜索整个子树,如果只用找出一个解,则搜到一个解就可以结束搜索。
「找出所有可能的组合」的问题,适合用回溯算法。
回溯算法有三个要点:
- 选择
决定了你每个节点有哪些分支,帮助你构建出解的空间树。
本题的选择就是,每个数字对应的多个字母,选择翻译成其中一个字母,就继续递归 - 约束条件
用来剪枝,剪去不满足约束条件的子树,避免无效的搜索。这题好像没怎么体现 - 目标
决定了何时捕获解,或者剪去得不到解的子树,提前回溯。扫描数字的指针到头了就可以将解加入解集了。
思路2 BFS 解法
维护一个队列。起初让空字符串入列,让当前层的字符串逐个出列,出列的字符串,会构建它下一层的新字母串,并入列。
一层层地,考察到数字串的最末位,就到了最底一层,此时队列中存的是所有构建完毕的字母串,返回 queue 即可。
这里控制了层序遍历的深度,为 digits 的长度,而不是while(queue.length){...}那样让所有的节点入列出列,最后还会剩下最后一层节点,留在 queue 中返回。
BFS 代码
const letterCombinations = (digits) => {
if (digits.length == 0) return [];
const map = { '2': 'abc', '3': 'def', '4': 'ghi', '5': 'jkl', '6': 'mno', '7': 'pqrs', '8': 'tuv', '9': 'wxyz' };
const queue = [];
queue.push('');
for (let i = 0; i < digits.length; i++) { // bfs的层数,即digits的长度
const levelSize = queue.length; // 当前层的节点个数
for (let j = 0; j < levelSize; j++) { // 逐个让当前层的节点出列
const curStr = queue.shift(); // 出列
const letters = map[digits[i]];
for (const l of letters) {
queue.push(curStr + l); // 生成新的字母串入列
}
}
}
return queue; // 队列中全是最后一层生成的字母串
};
思路3
理解本题后,要解决如下三个问题:
- 数字和字母如何映射
- 两个字母就两个for循环,三个字符我就三个for循环,以此类推,然后发现代码根本写不出来
- 输入1 * #按键等等异常情况
数字和字母如何映射
可以使用map或者定义一个二位数组,例如:string letterMap[10],来做映射,我这里定义一个二维数组,代码如下:
const string letterMap[10] = {
"", // 0
"", // 1
"abc", // 2
"def", // 3
"ghi", // 4
"jkl", // 5
"mno", // 6
"pqrs", // 7
"tuv", // 8
"wxyz", // 9
};
回溯法来解决n个for循环的问题
例如:输入:"23",抽象为树形结构,如图所示:
图中可以看出遍历的深度,就是输入"23"的长度,而叶子节点就是我们要收集的结果,输出["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]。
回溯三部曲:
- 确定回溯函数参数
首先需要一个字符串s来收集叶子节点的结果,然后用一个字符串数组result保存起来,这两个变量我依然定义为全局。
再来看参数,参数指定是有题目中给的string digits,然后还要有一个参数就是int型的index。
这个index是记录遍历第几个数字了,就是用来遍历digits的(题目中给出数字字符串),同时index也表示树的深度。
- 确定终止条件
例如输入用例"23",两个数字,那么根节点往下递归两层就可以了,叶子节点就是要收集的结果集。
那么终止条件就是如果index 等于 输入的数字个数(digits.size)了(本来index就是用来遍历digits的)。
然后收集结果,结束本层递归。
- 确定单层遍历逻辑
首先要取index指向的数字,并找到对应的字符集(手机键盘的字符集)。
然后for循环来处理这个字符集
代码
var letterCombinations = function(digits) {
const k = digits.length;
const map = ["","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"];
if(!k) return [];
if(k === 1) return map[digits].split("");
const res = [], path = [];
backtracking(digits, k, 0);
return res;
function backtracking(n, k, a) {
if(path.length === k) {
res.push(path.join(""));
return;
}
for(const v of map[n[a]]) {
path.push(v);
backtracking(n, k, a + 1);
path.pop();
}
}
};
最后
曾梦想仗剑走天涯
看一看世界的繁华
年少的心总有些轻狂
终究不过是个普通人
无怨无悔我走我路
「前端刷题」No.17