题目链接
https://leetcode.cn/problems/letter-case-permutation/
题目
我的思路
因为Java中的String类是一个final类,代表着不可变的字符序列。
所以在本题中,先将string转换成字符数组,将字符数组作为形参供递归方法调用,在递归方法中,可以很方便地通过下标修改数组中的元素值
可以结合Debug调试和下图理解
遇到大小写字母就不断深入,当已经将字符数组遍历完成后,就将字符数组转换成String添加到结果集中
在回溯的过程中将大写字母转换成小写字母或者将小写字母转换成大写字母,继续不断深度即可
import java.util.ArrayList;
import java.util.List;
class Solution {
private ArrayList<String> list = new ArrayList<>();
public List<String> letterCasePermutation(String s) {
char[] chars = s.toCharArray();
change(chars, 0);
return list;
}
private void change(char[] chars, int index) {
int i;
for (i = index; i < chars.length; i++) {
if (chars[i] >= 'a' && chars[i] <= 'z') {
change(chars, i + 1);
chars[i] = (char) (chars[i] - 32);
} else if (chars[i] >= 'A' && chars[i] <= 'Z') {
change(chars, i + 1);
chars[i] = (char) (chars[i] + 32);
}
}
if (i == chars.length) {
list.add(new String(chars));
}
}
}
官方思路
方案一 广度优先搜索
从左往右一次遍历字符串,在队列中存储当前已遍历过字符的字符大小全排列。
例如当前字符串为:s=“abc”
假设我们当前已经遍历到字符串的第二个字符b时,此时队列中已经存储的序列为:ab,Ab,aB,AB
当我们遍历下一个字符c时:
- 如果c为一个数字,则队列中所有的序列的末尾均加上c,将修改后的序列再次进入到队列中
- 如果c为一个字母,此时我们在上述序列的末尾一次分别加上c的小写形式lowercase©和c的大写形式uppercase©后,再次将上述序列放入队列
- 如果队列中当前序列的长度等于s的长度,则表示当前序列已经搜索完成,该序列为全排列中的一个合法序列
由于每个字符的大小写形式刚好差了32,因此在大小写转换时可以使用按位异或 用字符 异或 32 实现。
博客里面详细解释了小写转大写,大写转小写,大小写互换如何通过位运算实现
使用队列,永远是对队首元素进行操作,根据队首元素的长度,去获取字符串相应位置的值,如果是字母,则创建一个新的字符串,对应位置上的字母进行转换,如果不是字母,则只是在末尾添加;如果队首元素的长度和字符串的长度相等,那么就弹出队列,添加到结果集arrayList中
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
class Solution {
public List<String> letterCasePermutation(String s) {
ArrayList<String> list = new ArrayList<>();
LinkedList<StringBuilder> q = new LinkedList<>();
q.add(new StringBuilder());
while (!q.isEmpty()) {
StringBuilder first = q.getFirst();
if (first.length() == s.length()) {
list.add(first.toString());
q.removeFirst();
} else {
char c = s.charAt(first.length());
if (Character.isLetter(c)) {
StringBuilder next = new StringBuilder(first);
next.append((char) (c ^ 32));
q.add(next);
}
first.append(c);
}
}
return list;
}
}
复杂度分析
时间复杂度 O ( n × 2 n ) O(n \times 2^n) O(n×2n) ,其中n表示字符串的长度。全排列数目最多为 2 n 2^n 2n个,每次生成一个新的序列的时间为 O ( n ) O(n) O(n),因此时间复杂度为 O ( n × 2 n ) O(n \times 2^n) O(n×2n)
空间复杂度 O ( n × 2 n ) O(n \times 2^n) O(n×2n),其中n表示字符串的长度,队列中的元素数目最多为 2 n 2^n 2n个,每个序列需要的空间为 O ( n ) O(n) O(n),因此空间复杂度为 O ( n × 2 n ) O(n \times 2^n) O(n×2n)
方案二 回溯
从左往右依次遍历字符,当在进行搜索时,搜索到字符串s的第i个字符c时:
- 如果c是一个数字,则继续检测下一个字符
- 如果c是一个字母,将字符中的第i个字符改变大小写后,往后继续搜索,在以该状态为起点的后续所有情况都搜索完毕后,将字符c转换回来,继续往后搜索
完完全全是DFS思想
import java.util.ArrayList;
import java.util.List;
class Solution {
private ArrayList<String> list = new ArrayList<>();
public List<String> letterCasePermutation(String s) {
dfs(s.toCharArray(), 0);
return list;
}
private void dfs(char[] arr, int index) {
while (index < arr.length && Character.isDigit(arr[index])) {
index++;
}
if (index == arr.length) {
list.add(new String(arr));
return;
}
dfs(arr, index + 1);
arr[index] ^= 32;
dfs(arr, index + 1);
}
}
复杂度分析
时间复杂度 O ( n × 2 n ) O(n \times 2^n) O(n×2n) ,其中n表示字符串的长度。递归深度最多为n,所有可能的递归子状态最多为 O ( 2 n ) O(2^n) O(2n),每个子状态的搜索时间为 O ( n ) O(n) O(n),因此时间复杂度为 O ( n × 2 n ) O(n \times 2^n) O(n×2n)
空间复杂度 O ( n × 2 n ) O(n \times 2^n) O(n×2n),其中n表示字符串的长度,递归深度最多为n,所有可能的递归子状态最多为 O ( 2 n ) O(2^n) O(2n),因此空间复杂度为 O ( n × 2 n ) O(n \times 2^n) O(n×2n)
方案三 二进制位图
假设字符串s有m个字母,那么全排列就有 2 m 2^m 2m个字符串序列,且可以用位掩码bits唯一地表示一个字符串
- bits的第i为0,表示字符串s中从左往右第i个字母为小写形式
- bits的第i为1,表示字符串s中从左往右第i个字母为大写形式
采用的位掩码只计算字符串s中的字母,对于数字直接跳过,通过位图计算从而构造正确的全排列。我们依次检测字符串第i个字符c:
- 如果字符c是数字,则直接在当前的序列中添加字符串c
- 如果字符c是字母,且c为字符串中的第k个字母,如果掩码bits中的第k位为0,则添加字符串c的小写形式;如果掩码bits中的第k位为1,则添加字符串c的大写形式
import java.util.ArrayList;
import java.util.List;
class Solution {
public List<String> letterCasePermutation(String s) {
int n = s.length();
//m用来记录字符串中,字母的数量
int m = 0;
for (int i = 0; i < n; i++) {
if (Character.isLetter(s.charAt(i))) {
m++;
}
}
ArrayList<String> list = new ArrayList<>();
//比如"abc",m=3,1<<3 = 8,结果集中包含8个字符串
for (int mask = 0; mask < (1 << m); mask++) {
StringBuilder builder = new StringBuilder();
for (int j = 0, k = 0; j < n; j++) {
if (Character.isLetter(s.charAt(j)) && ((mask & (1 << k++)) != 0)) {
builder.append(Character.toUpperCase(s.charAt(j)));
} else {
builder.append(Character.toLowerCase(s.charAt(j)));
}
}
list.add(builder.toString());
}
return list;
}
}
以"abc"为例,m=3
mask的取值是[0,1,2,3,4,5,6,7]
对应的二进制是[000,001,010,011,100,101,110,111]
mask=0时,即000,这时对应的应该是abc,mask与任何数字进行按位与运算,都是0,所以,list中添加的是abc
mask=1时,即001,字符a是字符串中的第1个字母,且掩码中的第1位(这里的第一位是从右往左看来说的)为1,则添加字符c的大写形式,掩码中的其他位都是0,所以添加的是Abc
mask=2时,即010,字符b是字符串中的第2个字母,且掩码中的第2位是1,其余位都是0,所以添加的是aBc,以此类推
…
list的结果是:[“abc”,“Abc”,“aBc”,“ABc”,“abC”,“AbC”,“aBC”,“ABC”] 与 [000,001,010,011,100,101,110,111]有对应关系