全排列的实现(经典面试题目)

        

        上文在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!

        全排列的编码:

        {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.


       代码:
        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;
    }
    
};



       



        



        

   
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值