题目描述
无重复字符串的排列组合。编写一种方法,计算某字符串的所有排列组合,字符串每个字符均不相同。
示例1:
- 输入:S = “qwe”
输出:[“qwe”, “qew”, “wqe”, “weq”, “ewq”, “eqw”]
示例2:
- 输入:S = “ab”
输出:[“ab”, “ba”]
提示:
- 字符都是英文字母。
字符串长度在[1, 9]之间。
解题思路与代码
说实话,这道题我一看到,心里就冒出来了这道题要拿回溯法去解,因为这个问题是属于全排列问题。
回溯算法本质上是一种试探性的搜索算法它在解决某些组合问题,排列问题,优化问题上面非常有效,所以我们这道题可以选择回溯法去解答它。
方法一: 回溯法
在这道题中,我们可以把字符串的排列过程去想象成一颗多叉树。树的根节点是一个空字符串。其中每一层都是通过在当前排列的基础上交换字符来去构成新的排列。
也就是说,每一个字符,都可以去成为这颗树的一个分支。
那么对于一个长度为n的字符串,树的深度就为n。例如,对于字符串"ab"来说:
""
/ \
a b
/ \
ab ba
ab树就长这样。
现在我们来详细介绍一下回溯算法的实现过程:
-
首先,定义一个名为backtracking的辅助函数,它有三个参数,分别是题目给定的字符串S,正在处理的字符位置begin,以及用于存储结果的result。
-
对于当前位置begin来说,遍历字符串S中所有的字符,从位置begin开始,然后执行以下步骤
- 将字符begin的位置与当前字符的位置去做交换,这样做的目的是生成一个新的排列。
- 递归调用backtracking函数,将begin + 1作为新的起始位置。继续处理下一个字符。
- 在递归调用结束后,将begin与当前位置字符的顺序交换回来,这点很重要,它是回溯的关键,它使得我们能够恢复到之前的状态,然后继续去尝试新的排列组合。
-
整个算法的执行过程,可以想象成在一棵树上去做深度优先搜索,当我们在树的某一层上遍历完所有可能的字符组合后,我们回溯到上一层,继续尝试其他可能的组合。当我们遍历完整颗树时,我们将得到所有可能的排列组合。
class Solution {
public:
vector<string> permutation(string S) {
vector<string> result;
backtracking(S,result,0);
return result;
}
void backtracking(string& S,vector<string>& result,int begin){
if(begin == S.size()){
result.push_back(S);
return;
}
for(int i = begin; i < S.size(); ++i){
swap(S[i],S[begin]);
backtracking(S,result,begin+1);
swap(S[i],S[begin]);
}
}
};
我们可以拿输入字符串 S = "abc"来举例:
-
首先,我们从位置 0 开始。我们有三个字符可以放在第一个位置,分别是 ‘a’、‘b’ 和 ‘c’。我们将分别尝试这三个字符作为起始字符。这一步相当于从树的根节点扩展到第一层。
-
当我们选择 ‘a’ 作为起始字符时,我们需要处理剩余的字符 ‘b’ 和 ‘c’。此时 start = 1,我们可以在位置 1 尝试放置 ‘b’ 或 ‘c’。我们首先尝试将 ‘b’ 放在位置 1。现在字符串 S = “abc”。
-
继续递归,此时 start = 2。因为 start 等于字符串 S 的长度,我们将当前字符串 “abc” 添加到结果向量 result 中。
-
然后回溯,尝试将 ‘c’ 放在位置 1。为此,我们交换位置 1 和位置 2 的字符,得到字符串 S = “acb”。继续递归,此时 start = 2。同样地,因为 start 等于字符串 S 的长度,我们将当前字符串 “acb” 添加到结果向量 result 中。
-
我们已经尝试了所有以 ‘a’ 开头的排列,现在回溯到最开始的状态。接下来,我们尝试将 ‘b’ 作为起始字符。我们交换位置 0 和位置 1 的字符,得到字符串 S = “bac”。重复上述过程,我们将找到所有以 ‘b’ 开头的排列。
-
类似地,我们将尝试将 ‘c’ 作为起始字符,找到所有以 ‘c’ 开头的排列。
-
当我们遍历完整棵树时,结果向量 result 中将包含所有可能的排列组合:
-
[“abc”, “acb”, “bac”, “bca”, “cab”, “cba”]
有一点需要特别注意的是
:backtracking(S,result,begin+1);这行代码中不要把第三个参数写成++begin了。因为在做组合问题和排列问题的时候,对于这里的这种参数经常容易搞混,所以特点提醒一下。
复杂度分析
时间复杂度:
对于一个长度为 n 的字符串,我们需要对每个字符进行排列组合,这会产生 n! 个排列。在回溯算法中,我们会遍历整个排列空间,因此时间复杂度为 O(n!)。
空间复杂度:
空间复杂度主要取决于两个方面:递归调用栈的深度和结果向量所占用的空间。
递归调用栈的深度:在回溯算法中,递归调用栈的深度等于字符串的长度 n。因此,递归调用栈的空间复杂度为 O(n)。
结果向量所占用的空间:我们需要存储 n! 个排列,每个排列是一个长度为 n 的字符串。因此,结果向量所占用的空间复杂度为 O(n * n!)。
综合以上两个方面,总的空间复杂度为 O(n * n!)。
所以,这段代码的时间复杂度为 O(n!),空间复杂度为 O(n * n!)。
总结
这道题是一道很经典用回溯算法解决的全排列问题。受益匪浅~