剑指offer里的一道经典题目。
难度:中等
描述
输入一个长度为 n 字符串,打印出该字符串中字符的所有排列,你可以以任意顺序返回这个字符串数组。
例如输入字符串ABC,则输出由字符A,B,C所能排列出来的所有字符串ABC,ACB,BAC,BCA,CBA和CAB。
数据范围:n<10
要求:空间复杂度 O(n!),时间复杂度 O(n!)
思路:相当于求字符串的全排列,为了方便去重,先按照字典序给字符排序。主要利用递归与回溯解决此问题。每一层递归找到一个字符添加进临时字符串,当字符串长度合适时则存入字符串数组。而每一层递归相当于给剩下的字符再进行全排列。
具体做法:
- 第一步:先对字符串按照字典序排序,获取第一个排列情况。
- 第二步:准备一个空串暂存递归过程中组装的排列情况。使用额外的vis数组用于记录哪些位置的字符被加入了。
- 第三步:每次递归从头遍历字符串,获取字符加入:首先根据vis数组,已经加入的元素不能再次加入了;同时,如果当前的元素str[i]与同一层的前一个元素str[i-1]相同且str[i-1]已经使用过,也不需要将其纳入。
- 第四步:进入下一层递归前将vis数组当前位置标记为使用过。
- 第五步:回溯的时候需要修改vis数组当前位置标记,同时去掉刚刚加入字符串的元素,
- 第六步:临时字符串长度到达原串长度就是一种排列情况。
import java.util.*;
public class Solution {
public void recursion(ArrayList<String> res, char[] str, StringBuffer temp, boolean[] vis){
//临时字符串满了加入输出
if(temp.length() == str.length){
res.add(new String(temp));
return;
}
//遍历所有元素选取一个加入
for(int i = 0; i < str.length; i++){
//如果该元素已经被加入了则不需要再加入了
if(vis[i])
continue;
if(i > 0 && str[i - 1] == str[i] && vis[i - 1])
//当前的元素str[i]与同一层的前一个元素str[i-1]相同且str[i-1]已经用过了(为了去重)
continue;
//标记为使用过
vis[i] = true;
//加入临时字符串
temp.append(str[i]);
recursion(res, str, temp, vis);
//回溯
vis[i] = false;
temp.deleteCharAt(temp.length() - 1);
}
}
public ArrayList<String> Permutation(String str) {
ArrayList<String> res = new ArrayList<String>();
if(str == null || str.length() == 0)
return res;
//转字符数组
char[] charStr = str.toCharArray();
// 按字典序排序
Arrays.sort(charStr);
boolean[] vis = new boolean[str.length()];
//标记每个位置的字符是否被使用过
Arrays.fill(vis, false);
StringBuffer temp = new StringBuffer();
//递归获取
recursion(res, charStr, temp, vis);
return res;
}
}
复杂度分析:
时间复杂度
- 排序操作 `sort(str.begin(), str.end())` 的时间复杂度是 O(n log n),其中 n 是输入字符串 `str` 的长度。
- 递归函数 `recursion` 实现了全排列,对于一个长度为 n 的字符串,其全排列的数量是 n!(n 的阶乘)。
- 在每个排列中,我们需要执行的操作包括检查访问数组 `vis`、将字符添加到临时字符串 `temp` 中以及回溯时移除字符。这些操作都是 O(1) 的。
- 因此,除了初始的排序之外,生成所有排列的过程需要 O(n * n!) 的时间。这是因为对于每一个排列,我们都需要遍历整个字符串(O(n)),而总共有 n! 个排列。
综上所述,整个算法的时间复杂度是 O(n * n!) + O(n log n)。由于 n! 增长得非常快,当 n 较大时,n log n 可以忽略不计,所以我们可以简化为 O(n * n!)。
空间复杂度
- 输入字符串 `str` 需要 O(n) 的空间。
- 访问标记数组 `vis` 也需要 O(n) 的空间。
- 递归调用栈的最大深度是 n,因为每次递归调用都会增加临时字符串 `temp` 的长度,直到达到与 `str` 相同的长度。
- 结果列表 `res` 在最坏的情况下可以包含 n! 个不同的字符串,每个字符串长度为 n,因此 `res` 所需的空间是 O(n * n!)。
- 临时字符串 `temp` 在递归过程中最多会存储 n 个字符,因此它占用 O(n) 的额外空间。
综上所述,算法的空间复杂度主要由结果列表 `res` 决定,即 O(n * n!)。加上其他辅助数据结构所需的空间 O(n),总的额外空间复杂度仍然可以表示为 O(n * n!)。