上文在LeetCode 上做了一个关于下一个排列的问题,了解到了全排列这个知识点。搜索了一下发现这是一个经常在面试中遇到的问题。故写一篇文章整理一下有关知识点。
上文地址:点击这里进入
全排列简介:
从n个不同元素中任取m(m≤n)个元素,按照一定的顺序排列起来,叫做从n个不同元素中取出m个元素的一个排列。当m=n时所有的排列情况叫全排列。(来自百度百科)
全排列各种方法:
1.递归法
全排列经常会出现在互联网公司的面试题目中,如用C++写一个函数, 函数定义为void permute( char *str), 打印出 str 的全排列.
分析见上一篇博客.点击这里进入
#include <iostream>
#include <assert.h>
#include <string.h>
using namespace std;
void Swap( char * x, char * y ){
char temp = *x;
*x = *y;
*y = temp;
}
void permutation( char *str, int begin, int length ){
if( begin == length ){
static int num = 1;
cout << "The "<< num++ << " Permutation is " << str << endl;
}
else{
for( int i = begin; i <= length; i++ ){
Swap( str + begin , str + i );
permutation( str, begin + 1, length );
Swap( str + begin , str + i );
}
}
}
void permute( char *str){
assert( str != NULL );
permutation( str, 0, strlen( str ) - 1 );
}
int main()
{
char str[ ] = "abc";
permute(str);
return 0;
}
这个递归算法是根据位置来交换的,会有一个问题,如果这个字符串中有重复的元素的时候,这样会出现重复的值,那么加上一个限定条件的话如何实现呢。
去掉重复元素的全排列的递归算法:
我们可以很快的想到,从begin位置开始向后交换,如果后面和begin相同,则不交换,继续往下走。例如 112,则不交换1和1 只交换1 和2.如下图所示这样交换。
但是,仅仅这样做是不行的,例如下面的交换4个数字 1 1 2 2。
做到这里我们应该可以看出来,去重的全排列规则 就是从第一个位置起每个数分别与它后面非重复出现的数字交换。也就是说要遵守2点:
1.确保后面出现的数字不能和第一个位置上的数字相同。
2.确保后面出现的数字都不重复出现,如果重复出现了,则只交换第一个位置出现的那个元素。
有了这两点我们可以写代码了。(确保当前位置的元素在前面交换过的空间没有出现的情况下才交换)
代码:
void Swap( char * x, char * y ){
char temp = *x;
*x = *y;
*y = temp;
}
//end为当前需要交换元素的下标
bool SwapJudgement( char *str, int begin ,int current ){ //确保当前交换的元素在前面的控件没有重复出现
for( int i = begin; i < current; i++ ){
if( *( str + current) == *( str + i ) )
return false;
}
return true;
}
void permutation( char *str, int begin, int length ){
if( begin == length ){
static int num = 1;
cout << "The "<< num++ << " Permutation is " << str << endl;
}
else{
for( int i = begin; i <= length; i++ ){
if( SwapJudgement( str, begin, i ) ){
Swap( str + begin , str + i );
permutation( str, begin + 1, length );
Swap( str + begin , str + i );
}
}
}
}
void permute( char *str){
assert( str != NULL );
permutation( str, 0, strlen( str ) - 1 );
}
全排列的递归实现方法我们已经实现过了,也考虑到了有重复元素出现的情况,但是如果在面试的过程中,只能写出递归算法的话,对于一些大型的互联网公司来说,很难通过面试的。我们需要实现全排列的非递归算法。
全排列的非递归写法:
我们在上一篇文章中做了一个关于字符串序列的下一个排列,那么我们可以考虑一下,如果对一个有序的数组(从小到大排列)一直做它的下一个排列,就可以得到它的一个全排列。
关于下一个排列的解法:下一个排列的解法
代码:
void Swap( char * x, char * y ){
char temp = *x;
*x = *y;
*y = temp;
}
bool nextPermutation( char * str, int length ){
static int num = 1;
int i, k;
cout << "This is the " << num++ << " persutation " ;
for( i = length - 1 ; i > 0; i-- ){
if( str[ i - 1 ] < str [ i ])
break;
}
if( i == 0 ){
cout << str << endl;
return false;
}
for( k = length - 1; k >= i ; k -- ){
if( str[ k ] > str[ i - 1 ] )
break;
}
Swap( str + k, str + i - 1 );
sort( str + i, str + length );
cout << str << endl;
return true;
}
void AllPermutate(char *str ) {
assert( str != NULL );
sort( str, str + strlen( str ));
while ( nextPermutation( str, strlen( str ) ) ){
;
}
}
int main()
{
char str[] = "321457";
AllPermutate(str);
return 0;
}
STL中有一个next_persutation算法可以实现下一个排列这个功能,我们可以借助这个算法来实现这个功能,偷个懒。
void AllPermutate(char *str ) {
assert( str != NULL );
sort( str, str + strlen( str ));
static int sum = 1;
do{
cout << "This is the " << sum++ << " persutation "<< str << endl;
}
while ( next_permutation( str, str + strlen( str ) ) );
}
执行结果:
扩展2:上面我们做的是字符串的全排列,如果求的不是字符串的全排列,而是字符串的所有组合呢。该如何解呢。
假设字符串包含 n 个元素,则组合可以是 1 到n 个元素,而对于m个元素的组合( 1<=m<= n),又需要从n个元素中找m个元素来进行组合。对第一个字符,我们有两种选择:第一是把这个字符放到组合中去,接下来我们需要在剩下的n-1个字符中选取m-1个字符;第二是不把这个字符放到组合中去,接下来我们需要在剩下的n-1个字符中选择m个字符。这两种选择都很容易用递归实现。
实现代码:
#include <iostream>
#include <assert.h>
#include <vector>
#include <string.h>
using namespace std;
void CombinationWithNum( char* str ,unsigned int num, vector< char > &data );
void Combination( char * str ){
assert( str != NULL );
vector< char > data;
for( unsigned int i = 1; i <= strlen( str ); i++ ){
CombinationWithNum( str, i, data );
data.clear();
}
}
void CombinationWithNum( char* str ,unsigned int num, vector< char > &data ){
if( num == 0 ){
for( vector< char >::iterator it = data.begin(); it != data.end(); it++ )
cout << *it << " ";
cout << endl;
return;
}
if( *str == '\0' )
return ;
data.push_back( *str );
CombinationWithNum( str + 1, num - 1, data );
data.pop_back();
CombinationWithNum( str + 1,num ,data );
}
int main()
{
char s[] = "12345";
Combination( s );
return 0;
}
这里最后的边界条件要注意前后,一定是要先判断num是否为0,如果是0,打印输出,然后在判断str是否到了字符串'\0'处。
扩展3:
对于一个序列,我们已经知道了如何求它的下一个序列,那么对于一个包含n个数字的与列,如何求它的第k个序列。 (序列为1到n)
方法1:首先我们可以想到调用next_persutation 方法 k次
代码:
string getPermutation(int n, int k){
string s( n, '0' );
for( int i = 0; i < n; i++ )
s[ i ] += i + 1;
for( int i = 0; i < k -1; i++ )
next_permutation( s.begin(), s.end() );
return s;
}
在A题的过程中,这种方法要超时的。在面试中,也很难通过面试官的要求。 求k次的时候 也把前面k-1求了出来,做了很多不必要的操作。有没有一种方法和规律可以直接找到第k列。
方法2:
康托展开:
康托展开是一个全排列到一个自然数的双射,常用于构建哈希表时的空间压缩。 康托展开的实质是计算当前排列在所有由小到大全排列中的顺序,因此是可逆的。
公式:
X=a[n]*(n-1)!+a[n-1]*(n-2)!+...+a[i]*(i-1)!+...+a[1]*0!
其中,a[i]为整数,并且0<=a[i]<i,1<=i<=n。
a[i]的意义参见举例中的解释部分
举例[编辑]
例如,3 5 7 4 1 2 9 6 8 展开为 98884。因为X=2*8!+3*7!+4*6!+2*5!+0*4!+0*3!+2*2!+0*1!+0*0!=98884.
解释:
排列的第一位是3,比3小的数有两个,以这样的数开始的排列有8!个,因此第一项为2*8!
排列的第二位是5,比5小的数有1、2、3、4,由于3已经出现,因此共有3个比5小的数,这样的排列有7!个,因此第二项为3*7!
以此类推,直至0*0!
其中,a[i]为整数,并且0<=a[i]<i,1<=i<=n。
a[i]的意义参见举例中的解释部分
举例[编辑]
例如,3 5 7 4 1 2 9 6 8 展开为 98884。因为X=2*8!+3*7!+4*6!+2*5!+0*4!+0*3!+2*2!+0*1!+0*0!=98884.
解释:
排列的第一位是3,比3小的数有两个,以这样的数开始的排列有8!个,因此第一项为2*8!
排列的第二位是5,比5小的数有1、2、3、4,由于3已经出现,因此共有3个比5小的数,这样的排列有7!个,因此第二项为3*7!
以此类推,直至0*0!
全排列的编码:
{1,2,3,4,...,n}的排列总共有n!种,将它们从小到大排序,怎样知道其中一种排列是有序序列中的第几个?如 {1,2,3} 按从小到大排列一共6个:123 132 213 231 312 321。想知道321是{1,2,3}中第几个大的数。
{1,2,3,4,...,n}的排列总共有n!种,将它们从小到大排序,怎样知道其中一种排列是有序序列中的第几个?如 {1,2,3} 按从小到大排列一共6个:123 132 213 231 312 321。想知道321是{1,2,3}中第几个大的数。
这样考虑:第一位是3,小于3的数有1、2 。所以有2*2!个。再看小于第二位,小于2的数只有一个就是1 ,所以有1*1!=1 所以小于32的{1,2,3}排列数有2*2!+1*1!=5个。所以321是第6个大的数。2*2!+1*1!是康托展开。(注意判断排列是第几个时要在康托展开的结果后+1)再举个例子:1324是{1,2,3,4}排列数中第几个大的数:第一位是1小于1的数没有,是0个,0*3!,第二位是3小于3的数有1和2,但1已经在第一位了,所以只有一个数2,1*2! 。第三位是2小于2的数是1,但1在第一位,所以有0个数,0*1!,所以比1324小的排列有0*3!+1*2!+0*1!=2个,1324是第三个大数。
如何知道当前的排列是第几个排列的代码:
#include <iostream>
#include <algorithm>
using namespace std;
string getPermutation(int n, int k){
string s( n, '0' );
for( int i = 0; i < n; i++ )
s[ i ] += i + 1;
for( int i = 0; i < k -1; i++ )
next_permutation( s.begin(), s.end() );
return s;
}
long int fac( int n ){
if( n == 1 || n == 0 )
return 1;
else
return n * fac( n - 1 );
}
int findKthOfPersutation( int data[], int length ){
int i ,j , sum;
int num;
sum = 0;
for( i = 0; i < length; i++ ){
num = 0;
for( j = i + 1; j < length; j++ ){
if( data[ j ] < data[ i ] )
++num;
}
sum += num * fac( length - i - 1 );
}
return sum;
}
int main()
{
string s = getPermutation( 8, 8590 );
int s1[] = {5,4,3,2,1};
cout << findKthOfPersutation( s1,5 ) + 1 << endl;
cout << s << endl;
return 0;
}
康托展开是可以逆序的,我们可以根据k值直接求出来当前的排列。
方法:
如n=5,x=96时:
首先用96-1得到95,说明x之前有95个排列.(将此数本身减去!)
用95去除4! 得到3余23,说明有3个数比第1位小,所以第一位是4.
用23去除3! 得到3余5,说明有3个数比第2位小,所以是4,但是4已出现过,因此是5.
用5去除2!得到2余1,类似地,这一位是3.
用1去除1!得到1余0,这一位是2.
最后一位只能是1.
所以这个数是45321.
首先用96-1得到95,说明x之前有95个排列.(将此数本身减去!)
用95去除4! 得到3余23,说明有3个数比第1位小,所以第一位是4.
用23去除3! 得到3余5,说明有3个数比第2位小,所以是4,但是4已出现过,因此是5.
用5去除2!得到2余1,类似地,这一位是3.
用1去除1!得到1余0,这一位是2.
最后一位只能是1.
所以这个数是45321.
代码:
LeetCode题目:点击这里进入
class Solution {
public:
string getPermutation(int n, int k) {
string s( n, '0' );
for( int i = 0; i < n; i++ )
s[ i ] += i + 1;
return findKthOfPersutation( s,k );
}
long int fac( int n ){
if( 0 == n || 1 == n )
return 1;
int result = 1;
for( int i = 1;i <= n; i++ )
result *= i;
return result;
}
string findKthOfPersutation( string s, int num ){
const int length = s.size();
int i,j,temp;
string result ( length, '0' );
bool * isVisited = new bool[ length + 1 ];
memset( isVisited, 0, length + 1 );
num --;
for( i = 0; i< length ; i++ ){
temp = num / fac( length - i - 1 );
for( j = 1; j <= length; j++ )
if( !isVisited [ j ] ){
if( temp == 0 )
break;
temp --;
}
result[ i ] = j + '0';
isVisited[ j ] = true;
num = num % fac( length - i - 1);
}
return result;
}
};