全排列的求解问题
求一段数字的全排列,即是求这些数字所有排列组合。
按字典序输出的全排列,可以理解为某种意义上的“从小到大”排列顺序。
例如:{1,2,3}按字典序的全排列为:123,132,213,231,312,321
填数法:它的函数实现可以理解为,将原数组的数,优先选择较小的数,填入新数组中(当然,较大的数也会填入,不过要在较小数的所有情况都填完之后)
int a[3]={1,2,3};
int b[3]={0};
①将1填入b[0],此时b[1]还有2,3两种选择。
②将2填入b[0],此时b[1]还有1,3两种选择。
③将3填入b[0],此时b[1]还有1,2两种选择。
注意:这里选择数字填入是有顺序的,要优先选择当前可选择的数中,自己未选过的最小的。
它的函数实现为:
#include<iostream>
#include<algorithm>
using namespace std;
int a[5] = { 0 }; //用来标记这个数是否使用过
int b[5] = { 1, 4, 3, 2, 5 }; //用来存放数字
int c[5] = { 0 }; //b中的数字依次填入c
void print_per(int b[], int m, int n)
//按字典序输出数组b的全排列
//m表示已经排了几个数,n表示总共有几个数
{
if (m == n)//如果全部排好了
{
for (int i = 0; i < n; i++)
{
cout << c[i] << " ";
}
cout << endl;
return;
}
else
{
for (int i = 0; i < n; i++)
{
if (a[i] == 0) //如果第i个数还未使用
{
a[i] = 1; //标记为使用了
c[m] = b[i];//填入数组c
print_per(b, m + 1, n);
a[i] = 0;
}
}
}
}
int main()
{
sort(b, b + 5); //对数组b,从小到大排列
print_per(b, 0, 5);
return 0;
}
运行结果:部分截图
但是,当原数组中存在相同大小的数时,前面那种方法就会出错(排列出相同的组合),下面介绍另一种思路(其实’填数法’修改修改也能用,改起来挺麻烦的就是,这里主要是为了多介绍一种方法)
交换法:{1,2,3,4,5},将2,3,4,5依次换到1的位置上当“老大”,同理,在递归的下一层将3,4,5依次换到2的位置上当“老二”。由于递归的结算次序,实际输出的排列是先交换后面的位置,即4和5的位置。(实际操作的时候,1和1自身也要进行形式上的交换,以保证一开始1是在老大的位置)
那么,如果数组中存在重复的时候,如{1,2,2,3,4},应该怎么处理呢?
当1和第二个2想要进行交换的时候,它发现它已经和第一个2交换过了,所以此时不进行交换,叫下一个数来交换(continue;)。用代码判断就是,当前要交换的数a[i]和上一个数a[i-1]是不是相同的,如果是,就不交换(即只交换遇到的第一个’2’)。
函数实现为:
#include<iostream>
#include<algorithm>
using namespace std;
void swap(int a[], int i, int j)
{
int t = a[i];
a[i] = a[j];
a[j] = t;
}
void print_per2(int a[], int index, int n)
//index表示正在使用第几个数进行交换
//n表示总共有几个数
{
if (index == n)
{
for (int i = 0; i < n; i++)
{
cout << a[i] << " ";
}
cout << endl;
return;
}
int c[5];
for (int i = 0; i < n; i++)
//将数组a复制一遍,防止接下来的操作破坏a的顺序
{
c[i] = a[i];
}
sort(c + index, c + n);
for (int i = index; i < n; i++)
{
if (c[i] == c[i - 1] && i > index)
//如果这次要交换的数,和上次交换过的数一样,就不交换
continue;
swap(c, index, i); //交换数组c的第index和第i个的值
print_per2(c, index + 1, n);
swap(c, i, index); //还原成一开始的样子
}
}
int main()
{
int a[5] = { 1, 2, 2, 3, 4 };
sort(a, a + 5);
print_per2(a, 0, 5);
return 0;
}
运行结果:部分截图
PS.上述两种方法都是给定原数组,求全排列。如果给你一个全排列中的某种排列,让你求按字典序的下一个排列?怎么办?
由字典序我们知道,下一个排列肯定比上一个排列更大,另外还有一个附加条件,大的尽量少(即第一个比它大的排列)。so,求出全排列依次去比较,不行(求全排列->麻烦,比较->也麻烦)。
其实理解了交换法的话,我们知道,每一种排列之间其实就只交换了两个数字而已,所以我们只需要找到下一步应该交换哪两个数?
ex:{4,2,3,2,1}->{4,3,1,2,2},交换了a[1]==2和a[2]==3,然后把a[2]到a[4]从小到大排序。
步骤为:
①从右向左,找到第一个非递增的数,用i标记
②从右向左,找第一个大于a[i]的数,用j标记
③交换第i和第j个数
④对i+1之后的数,从小到大排序
函数实现:
#include<iostream>
#include<algorithm>
using namespace std;
int get_next_permutation(int a[])
{
//1.从右向左,找到第一个非递增的数,用i标记
//2.从右向左,找第一个大于a[i]的数,用j标记
//3.交换第i和第j个数
//4.对i+1之后的数,从小到大排序
bool f = false;
int i, j;
for (i = 4; i >= 0; i--)
{
if (a[i - 1] < a[i] && i - 1 >= 0)
{
i--;
f = true;
break;
}
}
if (!f)
{
return 0; //查找下一个全排列失败;
}
for (j = 4; j >= 0; j--)
{
if (a[j]>a[i])
break;
}
int t = a[j];
a[j] = a[i];
a[i] = t;
sort(a + i + 1, a + 5);
return 1;
}
int main()
{
int a[5] = { 4, 2, 3, 2, 1 };
get_next_permutation(a);
//next_permutation(a, a + 5);
//添加<algorithm>头文件后,使用上面这句有同样效果
for (int i = 0; i < 5; i++)
cout << a[i];
return 0;
}
结果:
理解不了的话?那就来点黑科技
①#include"algorithm"
②next_permutation(a, a + 5);
求全排列的时候就:
do
{输出语句或判断语句}
while(next_permutation(a, a + 5));
//如果是最后一个排列就会退出
//顺便说一下,此时就是使用非递归的方式求出全排列
求下一个排列的时候就:
next_permutation(a, a + 5);