1. 题目来源
链接:字符串的排列
来源:LeetCode——《剑指-Offer》专项
2. 题目说明
输入一个字符串,打印出该字符串中字符的所有排列。
你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。
示例:
输入:s = “abc”
输出:[“abc”,“acb”,“bac”,“bca”,“cab”,“cba”]
限制:
1 <= s 的长度 <= 8
3. 题目解析
方法一:STL next_permutation()+巧妙解法
下面对于 STL next_permutation() 的介绍转自:C++语言基础 —— STL —— 算法 —— 排列组合算法
首先要了解什么是 “下一个” 排列组合,什么是 “上一个” 排列组合。
假设有三个数字组成的序列:{a,b,c}
则这个序列有 6 种可能的排列组合:abc、acb、bac、bca、cab、cba
上述的排列组合是根据 less-than
操作符做字典顺序的排序,即:abc
处于第一,每一个元素都小于其后的元素,而 acb
是次一个排列组合,它是固定了序列内最小元素( a
)之后所做的新组合。
同理,序列中次小元素( b
)而做的排列组合,在次序上将先于那些固定最小元素( c
)而做的排列组合,以 bac、bca
为例,bac
在 bca
之前,因为次序 ac
小于序列 ca
,因此,对于 bca
,可以说其前一个排列组合是 bac
,其后一个排列组合是 cab
。
要注意的是,处于排列首的序列 abc
没有 “前一个” 排列组合,处于排列尾的序列 cba
没有 “后一个” 排列组合。
STL 中提供的算法
STL
提供了两个用来计算排列组合关系的算法,分别是 next_permutation()
与 prev_permutation()
其中,next_permutation()
是取出当前范围内的排列,并重新排序为下一个排列,prev_permutation()
是取出指定范围内的序列并将它重新排序为上一个序列。如果不存在下一个序列或上一个序列则返回 false
,否则返回 true
这个算法有两个版本,其一使用元素类别所提供的操作符来决定下一个或上一个排列组合,其二是以仿函数 comp
来决定
以 next_permutation()
为例:
- 从最尾端开始向前寻找两个相邻的元素,令第一元素为
*i
,第二元素为*ii
,且满足*i<*ii
- 找到上述的一组相邻元素后,从最尾端向前检验,找出第一个大于
*i
的元素,令其为*j
,然后将i、j
元素交换 - 再将
ii
之后的所有元素颠倒排序
假设存在序列 {0,1,2,3,4}
,下图即为寻找全排列的过程
next_permutation() 的用法
对于给定的任意一种排列组合,如果能求出下一个排列的情况,那么求得所有全排列情况就容易了。
利用 next_permutation()
的返回值,通过判断排列是否结束,即可求出全排列。
int a[N];
void all_permutation(int n)
{
sort(a,a+n);
do{
for(int i=0; i<n; i++)
printf("%d ",a[i]);
printf("\n");
}while(next_permutation(a,a+n));
}
next_permutation() 与 prev_permutation() 的区别
next_permutation()
函数默认的是从小到大的顺序,而 prev_permutation()
函数默认的是从大到小的顺序。
例如:对于序列 {3,1,2}
用 next_permutation()
函数得到的结果是:312、321
用 prev_permutation()
函数得到的结果是:312、231、213、132、123
大佬给总结的很明白了,使用 next_permutation()
的话,需要原序列是非严格增序列,就能够直接使用该函数得到全排列,在这个问题上得到了完美的呈现。但是若不进行 sort()
排序而使用该函数,则会导致输出不全。
参见代码如下:
// 执行用时 :24 ms, 在所有 C++ 提交中击败了93.02%的用户
// 内存消耗 :20.1 MB, 在所有 C++ 提交中击败了100.00%的用户
class Solution {
public:
vector<string> permutation(string s) {
vector<string> vt;
sort(s.begin(), s.end());
vt.emplace_back(s);
while (next_permutation(s.begin(), s.end()))
vt.emplace_back(s);
return vt;
}
};
方法二:递归+回溯+通用解法
全排列问题就离不开回溯和递归,再需要注意的点就是 set
去重以及回溯剪枝问题了。主要几点思路如下:
-
首先求所有可能出现在第一个位置的字符,即把第一个字符和后面的所有字符交换
-
然后固定第一个字符,求后面所有字符的排列
-
这时候仍然把后面的所有字符分成两部分:
- 后面字符的第一个字符
- 以及这个字符之后的所有字符。
-
然后把第一个字符逐一和它后面的字符交换。
即以字符串 "abc"
为例
在逛题解时发现的图,挺不错的,链接入下:
参见代码如下:
// 执行用时 :212 ms, 在所有 C++ 提交中击败了22.38%的用户
// 内存消耗 :30.9 MB, 在所有 C++ 提交中击败了100.00%的用户炫耀一下:
class Solution {
public:
vector<string> res;
set<string> tempRes;
vector<string> permutation(string s) {
if (s.size() == 0) return res;
help(s, &s[0]);
for (set<string>::iterator it=tempRes.begin();it!=tempRes.end();it++)
res.push_back(*it);
return res;
}
void help(string& s, char* sBegin)
{
if (*sBegin == '\0') tempRes.insert(s); // sBegin所指字符为空'\0',将当前字符串s存储起来
else {
for (char* sCh = sBegin; *sCh != '\0'; sCh++) { // sBegin所指字符为空'\0',将当前字符串s存储起来
char temp = *sBegin; // 交换位置
*sBegin = *sCh;
*sCh = temp;
help(s, sBegin + 1); // 交换完后继续全排列
temp = *sBegin; // 回溯
*sBegin = *sCh;
*sCh = temp;
}
}
}
};