面试试题 一 (排列组合)

昨天有个同学去面试了,回来讨论关于面试的题,因为是提前批次的,所以有点难,有点绕,自己写总结了一下:

1.此题是有关24点游戏,(整体的实现在http://www.cnblogs.com/liuamin/p/6947028.html),其中用到了递归,比较重要的知识点是排列组合,针对此题,简单说就是4(n)个元素,按不同的排列组合方式,有4!(n! 也就是A(n,n))种方式。都说迭代的人,递归的是神,先利用递归进行实现:(自己觉得代码很简单明了)

#include<iostream>
#include<vector>
#include<string>
using namespace std;
void print(vector<string> &arr)                           //只是简单打印每次输出的结果
{
    for (const auto c : arr)
        cout << c << " ";
    cout << endl;
}
void perm(size_t t, vector<string> &arr, int &cnt)    //核心算法,进行排列组合,t从0开始,cnt计算有多少排列方式
{
    if (t >= arr.size()) 
    {
        print(arr);
        cnt++;
        return;
    }
    for (size_t i = t; i < arr.size(); i++)
    {
        if (i != t )
           swap(arr[i], arr[t]);
        perm(t + 1,arr,cnt);   //表示对剩下的元素进行排列组合,perm{b,c,d}/ perm{a,c,d}/ perm{b,a,d}/ perm{b,c,a}
        if (i != t)
           swap(arr[i], arr[t]);   //表示交换之后再交换回来,要在原有的基础上(a,b,c,d)进行交换
    }
}
 
int main()               //测试
{
    static int cnt=0;
    vector<string> arr{ "a", "b" , "c", "d"};
    perm(0,arr,cnt);
    cout << "共有" << cnt <<"种排列组合方法!"<< endl;
    return 0;
}

      这里详细讲解下有关递归方法实现原理:n个元素的排列组合表示为perm{x1,x2,x3....,xn},当有4个元素(a, b, c, d)时,则表示为perm{a, b, c, d},按递归的方法则perm{a,b,c,d}=a.perm{b,c,d}+b.perm{a,c,d}+c.perm{b,a,d}+d.perm{b,c,a},  这里加号表示并集的意思,对于为什么是c.perm{b,a,d}而不是c.perm{a,b,d},从代码中swap()函数可以得到它是交换了a和c,同理d.perm{b,c,a}也是交换了a和d。

    代码中有两个swap()函数,是因为总是在原有的组合上(a,b,c,d)进行排列交换,具体的过程为:第一轮:开始i=t=0,不交换,也就是a.perm{b,c,d}(递归对b,c,d进行排列组合),执行完后,不交换;第二轮:交换a和b(swap(arr[1],arr[0])),所以为b. perm{a,c,d}(执行递归),之后再交换回来(swap(arr[1],arr[0]))则恢复a,b,c,d;第三轮:交换a和c(swap(arr[2],arr[0]),是在a,b,c,d的基础上进行交换(第三轮已经恢复回来了)),所以为c.perm{b,a,d}(执行递归),之后再交换回来(swap(arr[2],arr[0]))为a.b.c.d;第四轮:交换a和d (swap(arr[3],arr[0]),也是在a,b,c,d的基础上进行的交换),为d.perm{b,c,a}(执行递归),之后再交换回来(swap(arr[3],arr[0])),依然为a,b,c,d,因此执行完整个代码,输入的vector<string> arr依然是保持不变的(为a,b,c,d)。

perm{a,b,c,d}=a.perm{b,c,d}+b.perm{a,c,d}+c.perm{b,a,d}+d.perm{b,c,a}

 =a.b.perm{c,d}+a.c.perm{b,d}+a.d.perm{c,b}+b.a.perm{c,d}+b.c.perm{a,d}+b.d.perm{c,a}+c.b.perm{a,d}+c.a.perm{b,d}+c.d.perm{a,b}+d.b.perm{c,a}+d.c.perm{b,a}+d.a.perm{c,b}

= a.b.c.d+a.b.d.c+a.c.b.d+a.c.d.b+a.d.c.b+a.d.b.c+

   b.a.c.d+b.a.d.c+b.c.a.d+b.c.d.a+b.d.c.a+b.d.a.c+

   c.b.a.d+c.b.d.a+c.a.b.d+c.a.d.b+c.d.a.b+c.d.b.a+

   d.b.c.a+d.b.a.c+d.c.b.a+d.c.a.b+d.a.c.b+d.a.b.c 

验证后和代码运行结果一致的。。

2 .简单迭代实现,穷举法,虽然迭代看起来逻辑简单,但是时间复杂度很高,当待排列的数目发生变化时,这种方法要改变程序for循环的层数。(程序没有一般性)

#include<iostream>
using namespace std;
/*这里用迭代实现的排列组合,因为有四个数,所以有四层迭代,也就是说每个位置的数都可以为1,2,3,4 ,但是4个位置同时不能有相等的数出现*/
/*缺点是当待排列组合的数越来越多,则时间复杂度会很高为n^4*/
void perm(int arr[],int &cnt)   //这里有四层循环
{
    for (int i = 0; i < 4; i++)
    {
        for (int j = 0; j < 4; j++)
        {
            if (j == i)       //i不能等于j
                continue;
            for (int k = 0; k < 4; k++)
            {
                if (k == i || k == j)   //k不能等于i,也不能等j
                    continue;
                for (int l = 0; l < 4; l++)
                {
                    if (l==i||l==j||l==k)
                        continue;
                    cout << arr[i] << ' ' << arr[j] << ' ' << arr[k] << ' ' << arr[l] << endl;
                    cnt++;
                }
            }                
        }    
    }        
}
int main()
{
    int arr[4] = { 1, 2, 3, 4 };
    static int cnt = 0;
    perm(arr,cnt);
    cout << "共有" << cnt << "种方法!" << endl;
    return 0;
}

3.对第二种迭代方法进行优化,循环变为三层,循环的次数正好等于排列组合的方法数。(也是待排序的数目发生变化时要改变程序,程序也没有一般性)

#include<iostream>
using namespace std;
/*这里用迭代实现的排列组合,对上面的迭代方法进行优化,能减少时间复杂度*/
/*但是仍然是当待排列组合的数越来越多,则时间复杂度会变高(为排列组合的方法数,这里为4!=24)*/
void perm(int arr[], int &cnt)   
{
    for (int i = 0; i < 2; i++)   
    {
        for (int j = 0; j < 3; j++)
        {
            for (int k = 0; k < 4; k++)  //当求余4时,结果都是在0,1,2,3之间,刚好为数组的下标
            {
                cout << arr[k % 4] << ' ' << arr[(k + j % 3 + 1) % 4] << ' ';
                cout << arr[(k + (j + i % 2 + 1) % 3 + 1) % 4] << ' ';
                cout << arr[(k + (j + (i + 1) % 2 + 1) % 3 + 1) % 4] << endl;
                  //arr[j%3]+" "+arr[(j+i%2+1)%3]+" "+arr[(j+(i+1)%2+1)%3]    3个数的情况
                cnt++;
            }
        }
    }
}
int main()
{
    int arr[4] = { 0, 1, 2, 3 };
    static int cnt = 0;
    perm(arr, cnt);
    cout << "共有" << cnt << "种排列组合方法!" << endl;
    return 0;
}

 4,当从一个数组中取m个数字,有多少种取法,利用穷举法,这里假设从一个数组中取3个数字。(此处为C(8,3),与上面不同的是取出来的数字不讲究顺序,也就是C(n,m),从n个元素中取m个元素有多少种取法(m<=n)),程序没有一般性。

#include<iostream>
#include<vector>
using namespace std;
/*穷举法*/
void combination(const vector<int> & arr, int &cnt)      //这里是三层循环,是从数组中取三个数,要是取4个数,程序就改成4层循环
{
    for (size_t i = 0; i < arr.size() - 2; i++)          //直接改为i<arr.size()也可以,因为j=i+1,k=j+1,且小于arr.size(),均有限定
    {
        for (size_t j = i + 1; j < arr.size() - 1; j++)    //直接改为i<arr.size()也可以,原因同上
        {
            for (size_t k = j + 1; k < arr.size(); k++)
            {
                cout << arr[i] << ' ' << arr[j] << ' ' << arr[k] << endl;
                cnt++;
            }
        }
    }
}
int main()
{
    vector<int> arr { 1, 2, 3, 4,5,6,7,8};
    static int cnt = 0;
    combination(arr, cnt);
    cout << "共有" << cnt << "种组合方法!" << endl;
    return 0;
}

5、对于问题4,在n个元素中取m个元素共有C(n,m)中取法,利用递归实现。

#include<iostream>
#include<vector>
using namespace std;
/*递归法,从n个元素中取m个元素,有C(n,m)种取法,这里index[]存放取出来的m个元素;k为要取得元素个数,开始为m,后来为m-1,m-2,...,1;
m为取出元素的个数,作用是打印取出的元素数组index;size为总元素个数n ; cnt统计多少种组合方式*/
void combine(vector<int> &arr, int index[], int k, const int m, int size,int &cnt)  
{  
    for (int i = size - 1; i >= k - 1; --i)  
    {  
        index[k - 1] = arr[i];  
        if (k > 1)              //当取出的元素个数为1时,则不再递归
        {  
            combine(arr, index, k - 1, m, i,cnt);  
        }  
        else  
        {  
            for(int i = 0; i < m; ++i)  
                cout << index[i]; 
            cnt++;
            cout << endl;  
        }  
    }  
}  
void combination(vector<int> &arr, int size, int m,int & cnt)  
{  
    if(m > size)  
        return;  
    
    int* out = new int[m];  
    combine(arr,out,m,m,size,cnt);  
    delete [] out;  
}  

int main()
{
    vector<int> arr {1,2,3,4,5,6,7,8};
    static int cnt = 0;
    combination(arr, arr.size(),3,cnt);   //这里是取3个元素
    cout << "共有" << cnt << "种组合方法!" << endl;
    return 0;
}

例如从5个元素{1,2,3,4,5}取3个元素,则取一个元素后,问题简化为从剩下的取2个元素,再取走一个元素后,则简化为从剩下的元素中取一个元素。

具体过程为:这里是从最后一个元素开始,例如从5个元素{1,2,3,4,5}取3个元素:则从i=size-1=4开始(因为下标都是从零开始),i>=2,k为3,输出的index[2]=5,

再递归从{1,2,3,4}中取2个元素i=3开始,i>=1,k为2,输出index[1]=4,

再递归从{1,2,3}中取一个元素,i=2开始,i>=0,k为1,此时是指取出一个元素,不再递归,index[0]=3/2/1,则依次输出3,4,5; 2,4,5; 1,4,5;

返回至上一层--i为2,从{1,2,3,4}中取出2个元素,此时index[1]=3,递归下去,同理从{1,2}中取一个元素,则不再递归,依次输出2,3,5; 1,3,5;

再返回上一层--i为1,从{1,2,3,4}中取出2个元素,此时index[1]=2,递归下去,同理从{1}中取一个元素,不再递归,输出1,2,5;

同理,index[2]=4时,index[1]=3输出为2,3,4;1,3,4;index[1]=2,输出1,2,4;index[2]=3时,index[1]=2输出为1,2,3。

补充:6 . 对问题1,要从n个元素中选择m个元素,也就是有A(n,m)中排列方法,在1的基础上修改:

#include<iostream>
#include<vector>
using namespace std;
void perm(size_t t, vector<int> &arr,const int m, int &cnt)     //核心算法,进行排列组合,t从0开始,m为取出的元素个数,cnt计算有多少排列方式,
{
    if (t >= m) 
    {
        for (int j=0;j!=m;j++)
          cout << arr[j]<< " ";
        cnt++;
        cout << endl;
        return;
    }
    for (size_t i = t; i < arr.size(); i++)
    {
        if (i != t )
           swap(arr[i], arr[t]);
        perm(t + 1,arr,m,cnt);
        if (i != t)
           swap(arr[i], arr[t]);                      //表示交换之后再交换回来,要在原有的基础上进行交换
    }
}
 
int main()               //测试
{
    static int cnt=0;
    vector<int> arr{1,2,3,4,5};
    perm(0,arr,3,cnt);  //这里是取3个元素
    cout << "共有" << cnt <<"种排列组合方法!"<< endl;
    return 0;
}
原文地址:https://www.cnblogs.com/liuamin/p/6940342.html