一 目录
不折腾的前端,和咸鱼有什么区别
目录 |
---|
一 目录 |
二 前言 |
三 解题及测试 |
四 LeetCode Submit |
五 解题思路 |
六 进一步思考 |
二 前言
难度:简单
涉及知识:位运算、回溯算法
题目地址:https://leetcode-cn.com/problems/letter-case-permutation/
题目内容:
给定一个字符串 S,
通过将字符串 S 中的每个字母转变大小写,
我们可以获得一个新的字符串。
返回所有可能得到的字符串集合。
示例:
输入: S = "a1b2"
输出: ["a1b2", "a1B2", "A1b2", "A1B2"]
输入: S = "3z4"
输出: ["3z4", "3Z4"]
输入: S = "12345"
输出: ["12345"]
注意:
S 的长度不超过12。
S 仅由数字和字母组成。
三 解题及测试
小伙伴可以先自己在本地尝试解题,再回来看看 jsliang 的解题思路。
LeetCode 给定函数体:
/**
* @param {string} S
* @return {string[]}
*/
var letterCasePermutation = function(S) {
};
根据上面的已知函数,尝试破解本题吧~
确定了自己的答案再看下面代码哈~
index.js
/**
* @name 字母大小写全排列
* @param {string} S
* @return {string[]}
*/
const letterCasePermutation = S => {
const res = [];
const backtrack = (start, s) => {
res.push(s);
for (let i = start; i < s.length; i++) {
if (s[i] >= 'a' && s[i] <= 'z') {
backtrack(i + 1, s.slice(0, i) + s[i].toUpperCase() + s.slice(i + 1));
} else if (s[i] >= 'A' && s[i] <= 'Z') {
backtrack(i + 1, s.slice(0, i) + s[i].toLowerCase() + s.slice(i + 1));
}
}
};
backtrack(0, S);
return res;
};
console.log(letterCasePermutation("a1b2c3")); // ['a1b2c3', 'A1b2c3', 'a1B2c3', 'a1b2C3', 'A1B2c3', 'A1b2C3', 'a1B2C3', 'A1B2C3']
// console.log(letterCasePermutation('ab2')); // ['ab2', 'aB2', 'Ab2', 'AB2']
// console.log(letterCasePermutation('3z4')); // ['3z4', '3Z4']
// console.log(letterCasePermutation('123')); // ['123']
node index.js
返回:
[ 'a1b2c3',
'A1b2c3',
'a1B2c3',
'A1B2c3',
'a1b2C3',
'A1b2C3',
'a1B2C3',
'A1B2C3' ]
四 LeetCode Submit
Accepted
* 63/63 cases passed (88 ms)
* Your runtime beats 63.87 % of javascript submissions
* Your memory usage beats 55.17 % of javascript submissions (37.8 MB)
五 解题思路
首先,这道题,可以说我之前应该碰到过相似题目,但是总想不出来了,毕竟 回溯算法 这个词,还是挺刺激的。
但是 だいじょうぶ 啦,咱们哈希玩得溜,照样先拿个一血:
哈希表
const letterCasePermutation = (S) => {
let map = [];
for (let i = 0; i < S.length; i++) {
if (i === 0) {
map = [...new Set([S[0].toLowerCase(), S[0].toUpperCase()])];
} else {
map = map.filter(item => item.length === i);
const upper = S[i].toUpperCase();
const lower = S[i].toLowerCase();
const length = map.length;
for (let i = 0; i < length; i++) {
if (map.findIndex(item => item === map[i] + upper) === -1) {
map.push(map[i] + upper);
}
if (map.findIndex(item => item === map[i] + lower) === -1) {
map.push(map[i] + lower);
}
}
}
}
return map.filter(item => item.length === S.length);
};
思路为:
设置
map
为哈希表(没有使用Map
,后面会提及为啥);通过
for
遍历S
;判断:如果是第一次的时候,
i === 0
,进行map
的初始化,即[...new Set(arr)]
,这样可以去重开头为数字的情况。判断:如果不是第一次,
i !== 0
,那么我们做以下步骤:步骤 1:通过
filter
过滤掉长度不符合的字符串,表明之前的 out 了,被淘汰了。步骤 2:设置
upper
、lower
为当前S[i]
对应的大小写字母,当然,如果是数字也没问题,下面会过滤掉。步骤 3:通过
for
遍历map
的长度,注意这里使用length
来代替map.length
,因为下面步骤map
会不停增长长度,如果不写死,那么就造成死循环了。步骤 4:通过
findIndex
查找map
内里有没有元素,和当前元素相加是不会重复的。如果是,则添加到map
中最后,再通过一次
filter
将等同于S.length
长度的字符串暴露出去。
Submit 提交如下:
Accepted
* 63/63 cases passed (752 ms)
* Your runtime beats 5.04 % of javascript submissions
* Your memory usage beats 5.17 % of javascript submissions (52 MB)
提交信息残暴如斯,已经不再是哈希了,而是暴力破解了~
六 进一步思考
绞尽脑汁拿了个差生评价,咱们只能参考借鉴下优等生的题解了:
优生解法一:递归
const letterCasePermutation = function f(str) {
if (str.length === 0) {
return [''];
}
const a = str[0];
const b = [...new Set([a.toLowerCase(), a.toUpperCase()])];
if (str.length === 1) {
return b;
}
return letterCasePermutation(str.slice(1)).reduce((r, c) => [...r, ...b.map(m => m + c)], []);
};
好处是:代码精简
坏处是:看不懂,或者说,一下子难以理解
Submit 提交:
Accepted
* 63/63 cases passed (160 ms)
* Your runtime beats 7.56 % of javascript submissions
* Your memory usage beats 5.17 % of javascript submissions (43.7 MB)
优生解法二:回溯算法
const letterCasePermutation = S => {
const res = [];
const backtrack = (start, s) => {
res.push(s);
for (let i = start; i < s.length; i++) {
if (s[i] >= 'a' && s[i] <= 'z') {
backtrack(i + 1, s.slice(0, i) + s[i].toUpperCase() + s.slice(i + 1));
} else if (s[i] >= 'A' && s[i] <= 'Z') {
backtrack(i + 1, s.slice(0, i) + s[i].toLowerCase() + s.slice(i + 1));
}
}
};
backtrack(0, S);
return res;
};
假设已有字符串 ab2
,按照这边的回溯,是怎么一回事?
构造递归树;
依次递归遍历。
咱们按照代码理解:
// ...代码省略
const backtrack = (start, s) => {
console.log('------');
console.log(start);
console.log(s);
// ...代码省略
}
// ...代码省略
我们往代码中插入 3 个 console.log
,方便观察:
------
0
ab2
------
1
Ab2
------
2
AB2
------
2
aB2
它依次执行了:
backtrack(0, 'ab2'); ->
backtrack(1, 'Ab2');
backtrack(2, 'aB2'); ->
backtrack(2, 'AB2');
看到这里,如果你没有感到 恍然大悟,那么你可以将代码放到编辑器上,看看 'a1b2c3'
的打印情况。
简直妙不可言~
Submit 提交:
Accepted
* 63/63 cases passed (88 ms)
* Your runtime beats 63.87 % of javascript submissions
* Your memory usage beats 55.17 % of javascript submissions (37.8 MB)
题外话:
问:什么是回溯算法?
答:
回溯算法实际上是一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解时,就 “回溯” 返回,尝试别的路径。
回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。
但当探索到某一步时,发现原先选择并不优或达不到条件,就退回一步重新选择,这种走不通就退回再走的技术,就是回溯法。
在这当中,满足回溯条件的某个状态的点称为 “回溯点”。
注:这道题被标明是【简单】,但是大佬在解题的时候,也会吐槽【不简单】,可以看下 https://leetcode-cn.com/problems/letter-case-permutation/solution/shen-du-you-xian-bian-li-hui-su-suan-fa-python-dai/
不折腾的前端,和咸鱼有什么区别!
jsliang 会每天更新一道 LeetCode 题解,从而帮助小伙伴们夯实原生 JS 基础,了解与学习算法与数据结构。
浪子神剑 会每天更新面试题,以面试题为驱动来带动大家学习,坚持每天学习与思考,每天进步一点!
扫描上方二维码,关注 jsliang 的公众号(左)和 浪子神剑 的公众号(右),让我们一起折腾!
jsliang 的文档库 由 梁峻荣 采用 知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议进行许可。
基于https://github.com/LiangJunrong/document-library上的作品创作。
本许可协议授权之外的使用权限可以从 https://creativecommons.org/licenses/by-nc-sa/2.5/cn/ 处获得。