递归法全排列
通过交换的方法得到位置k的字符
void permutation(int* a, int n, int k){
/*
:param a: array
:param n: length of array
:param k: location
*/
if (k == n-1){
for (int i = 0; i < n; ++i)
cout << a[i] << ' ';
cout << endl;
} else{
for (int i = k; i < n; ++i){ // 在k位上固定一个数,然后去递归生成[k+1, n)位的字符
swap(a[i], a[k]);
permutation(a, n, k+1);
swap(a[i], a[k]);
}
}
}
int main(){
int a[] = {1,2,3,4};
permutation(a, 4, 0);
return 0;
}
选择一个当前k位置没用过的字符
void helper(string& str, string cur, vector<bool>& visited, vector<string>& res){
if(cur.size() == str.size()) {
res.push_back(cur);
return;
}
for(int i = 0; i < str.size(); ++i){
if(visited[i]) continue;
visited[i] = true;
cur += str[i];
helper(str, cur, visited, res);
visited[i] = false;
cur = cur.substr(0, cur.size()-1);
} return;
}
vector<string> permutation(string str){
if(str == "") return vector<string>();
vector<bool> visited(str.size(), false);
vector<string> res;
string cur = "";
helper(str, cur, visited, res);
return res;
}
上面的方法很容易扩展为按照字典序排列+去重效果:
class Solution {
public:
vector<string> Permutation(string str) {
res = vector<string>();
if(str == "") return res;
sort(str.begin(), str.end());
visited = vector<bool>(str.size(), false);
helper(str, "");
//sort(res.begin(), res.end()); // 该方法本身就是升序的,而且去过重复str了
return res;
}
private:
vector<string> res;
vector<bool> visited;
void helper(string& str, string cur){
if(cur.size() == str.size()){
res.push_back(cur);
} else{
for(int i = 0; i < str.size(); ++i){ // str是排好序的字符串,那么每次都选一个当前最大/最小的字符
if(visited[i]) continue;
/* visited[i-1]=false,则str[i-1]是被上一轮访问过放在当前位置的。
如果str[i-1] = str[i],则上一轮选择的字符和这一轮的字符相同,会导致重复。*/
if(i>0 && visited[i-1] == false && str[i-1] == str[i]) continue;
visited[i] = true;
cur += str[i];
helper(str, cur);
visited[i] = false;
cur = cur.substr(0, cur.size()-1);
}
}
}
};
插入法
采用插入的方法生成全排列, 出自 Cracking the Coding Interview (《程序员面试金典》)
"ab"的排列有: _a_b_, _b_a_, 我们可以把c插入空位(用下划线_代替)得到:
cab, acb, abc, cba, bca, bac
// ref: https://www.cnblogs.com/grandyang/p/4358848.html
vector<string> permutation(string str){
if(str == "") return vector<string>(1, ""); // n, v
string subs = str.substr(1, str.size()-1);
vector<string> str_list = permutation(subs);
vector<string> new_str_list;
for(int i = 0; i < str_list.size(); ++i){
for(int j = 0; j <= str_list[i].size(); ++j){
new_str_list.push_back(str_list[i].substr(0, j) + str[0] + str_list[i].substr(j, str.size()-j)); // str.substr(str.size(), 0), 这个能运行估计是因为'\0'
} //new_str_list.push_back(str_list[i] + str[0]); // substr(start, len) start 超过了范围? 能运行
} return new_str_list;
}
迭代的方法生成全排列
插入法
上述的插入法生成全排列可以容易得到迭代的版本:
vector<string> permutation_iter(string str){
/*
迭代版本插入法生成全排列 */
if(str == "") return vector<string>();
vector<string> str_list(1, ""), new_str_list;
vector<string> *a = &str_list, *b = &new_str_list;
for(char c: str){
for(string substring: (*a)){
for(int i = 0; i <= substring.size(); ++i){
(*b).push_back(substring.substr(0, i) + c + substring.substr(i, substring.size()-i));
}
} swap(a, b); // swap pointer, 我觉得这操作复杂度比较低
(*b) = vector<string>();
} return *a;
}
只能生成连续元素的全排列,例如1~4
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;
const int MAXN = 10;
int n;
int x[MAXN]; // number in position i
bool is_appeared(int i, int x_i){
/*
judge whether x[i] has been appeared before.
:param i: current position of ordered number.
:param x_i: target number to appear in position i.
*/
for(int j=0; j<i; ++j){
if(x[j] == x_i)
return false;
} return true;
}
void my_print(){
for(int i = 0; i<n; ++i){
cout << x[i] << ' ';
} cout << endl;
}
void permutation(){
int i = 0;
while(i >= 0){
while(i<n && x[i]<n){
++x[i];
if(is_appeared(i, x[i]))
if(++i == n)
my_print();
} x[i--] = 0;
}
}
void init(){ // initialization
n = 4;
memset(x, 0, sizeof(x));
}
int main(){
init();
permutation();
return 0;
}
permutation
我是省略表达的,其实可以拆开变成下面这种。
void permutation(){
int i = 0;
while(i >= 0){
while(i<n && x[i]<n){
++x[i];
if(is_appeared(i, x[i])){
if(i == n-1){
//my_print();
} else{
++i;
}
}
}
x[i] = 0;
--i;
}
}
这个迭代的方法有明显可以进一步的改进的地方。体现在++x[i]
和++i
操作上了。应该可以通过堆
来维护一个没有被插入数字的x[i]的列表,这样就不用通过调用is_appeared
方法来判断是否重复了。不过鉴于全排列O(n!)的复杂度,n不会很大,所以估计这里浪费的计算不会很多。
生成下一个全排列
这个部分我整理、汇总并改写自其它博客(见最后的参考资料部分)
因为 一个全排列可看做一个字符串,字符串可有前缀、后缀 。 我们定义,生成对于给定全排列的下一个全排列:
所谓下一个就是这一个与下一个之间没有其他的序列。这就要求这一个与下一个有尽可能长的共同前缀,也即变化限制在尽可能短的后缀上。
[例] 839647521是1–9的排列。1—9的全排列按照字典序排列,最前面的是123456789,最后面的是987654321。对于全排列987654321,从左到右扫描都是非增的,因此就没有更大的全排列序列了。此时可以定义最大的全排列的下一个是最小的全排列,即将排列整个颠倒得到最小的排列"123456789"。
举例来说明,对于给定一个序列
要如何才能生成下一个序列
:
对于长度为4的序列
= (3,6,4,2),。
1. 观察
可以发现,其子序列(6,4,2)已经为减序,那么这个子序列不可能通过交换元素位置得出更大的序列了。因此必须改变
的最高位,即最左侧的3,且要在子序列<6,4,2>中找一个数来取代3。
2. 子序列 (6,4,2) 中6和4都比3大,但6大于4。如果用6去替换3得到的序列一定会大于4替换3得到的序列,因此只能选4。将4和3的位置对调后形成排列(4,6,3,2)。对调后得到的子序列(6,3,2)仍保持减序,即这3个数能够生成的最大的一种序列。
3. 而4是第1次作为首位的,需要右边的子序列最小,因此4右边的子序列应为(2,3,6),这样就得到了正确的一个序列
= (4,2,3,6)。
总结上述想法
1)从后向前找第一对相邻的递增数字
和
. 如果找不到,则返回该排列的逆序。
2)从
后面找一个比
大的最小的数,这个数必然存在(至少能找到
).
3)将
后的序列逆序
上述想法的伪代码:
是长度为
的序列,
=
find:
= max{
|
},
= max{
|
<
}
1, 交换
,
,
2, 将
逆序/翻转
=
即为
的下一个全排列
代码如下:
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
string next_permutation(string str){
if(str.size() <= 1) return str;
int j, k;
for(j = str.size()-2; j >= 0; --j){
if(str[j] < str[j+1]) break;
} if(j < 0){ // str是递减序列,是最大的序列
reverse(str.begin(), str.end());
return str;
} k = j+1;
for(int i = j+1; i < str.size(); ++i){
if(str[j] < str[i] && str[i] <= str[k]) { // 注意<=: 23133 -> 23313, k为符合条件的最右边的元素下标
k = i;
}
} swap(str[j], str[k]);
for(int i = 1; j+i < str.size()-i; ++i){
swap(str[j+i], str[str.size()-i]);
}
return str;
}
int main(){
string str = "1234";
int n = str.size();
int t = 30;
while(t--){
str = next_permutation(str);
cout << str << endl;
}
return 0;
}
参考资料
-
https://blog.csdn.net/morewindows/article/details/7370155/
一篇经典的csdn博客。1)介绍了一种使用swap递归生成去重复的全排列的方法,
去重的全排列就是从第一个数字起,每个数分别与它后面非重复出现的数字交换,这样就保证每个位置上出现的数字是不同的。2)介绍了如何按照字典序生成下一个序列 (next_perumutation) -
http://www.cnblogs.com/pmars/archive/2013/12/04/3458289.html
形式化了next_perumutation稳定的定义,并给出伪代码 -
https://www.jianshu.com/p/db90675cb82b
1)给了next_perumutation一些较为细致的解释。 2)介绍了如何按照字典序生成第k大的字符串序列 -
https://www.cnblogs.com/grandyang/p/4358848.html
介绍了CareerCup书上的方法,插入法生成全排列。