数据结构之全排列问题总结

我是自动化专业的应届研究生,最终拿到了tplink、华为、vivo等公司的ssp的offer,分享自己学习过的计算机基础知识(C语言+操作系统+计算机网络+linux)以及数据结构与算法的相关知识,保证看完让你有所成长。
欢迎关注我,学习资料免费分享给你哦!还有其他超多学习资源,都是我自己学习过的,经过过滤之后的资源,免去你还在因为拥有大量资源不知如何入手的纠结,让你体系化学习。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

递归

递归有两个关键的地方:1.基础部分。2.右侧的要有一个参数小于n。以计算n的阶乘为例
f ( n ) = { 1 , n<=1 n ∗ f ( n − 1 ) , n>1 f(n) = \begin{cases} 1, & \text{n<=1} \\ n*f(n-1), & \text{n>1} \end{cases} f(n)={1,nf(n1),n<=1n>1
基础部分,n小于等于1时,f(n)=1。当n大于1时,f(n)右侧的f的参数是n-1,比n小。计算f(3)=3*f(2)=3*2*f(1)=3*2*1=6.所以在解决递归问题时,就是找到一个递归公式,满足右侧的参数小于n,然后在找到基础部分,就是递归的终点,否则造成递归没有终止。

以斐波那契数列为例:
F 0 = 0 , F 1 = 1 , F n = F n − 1 + F n − 2 F_0=0,F_1=1,F_n=F_{n-1}+F_{n-2} F0=0,F1=1,Fn=Fn1+Fn2
F 0 F_0 F0 F 1 F_1 F1为基础部分,而后面部分为递归部分,右侧参数都比n小。代码实现:

int Fabio1(int n)
{
	if(n == 1||n ==2)
	{
		return 1;
	}
	else
	{
		return Fabio(n-1) + Fabio(n-2);
	}
}

递归常见题型总结:

1.全排列(letcode 46题)

给定一个 没有重复 数字的序列,返回其所有可能的全排列。

这个题对于编写非递归程序是很困难的,但是编写递归程序就很简单了。我们来分析一下,假设输入的序列为E={1,2,3},求E的所有排列,假设 E i E_i Ei表示的是除去第i个元素集合,即 E 1 E_1 E1表示{2,3}, E 2 E_2 E2表示{1,3},定义list[E]表示为E中元素的所有排列,比如list[ E 1 E_1 E1]表示的就是{[2,3],[3,2]},定义 e i e_i ei表示的是E中的第i个元素, e i e_i ei.list[ E i E_i Ei]表示的是在E中除去第i个元素的排列的前面添加一个 e i e_i ei的元素,比如前面的例子,e1=1,那么e1.lsit[E1]就是表示排列{[1,2,3],[1,3,2]}。

有了这个定义,我就可以按照递归来解决这个题了。对于E中只有一个元素的情况,那么排列就是它本身,这就是递归中的基本情况,对应于前面n的阶乘,n等于1的情况。对于n大于1,即E中的元素不是一个的情况,那么就需要递归求解,怎么递归呢?对于E中有n个元素,那么就是每次取出一个元素,加到其余元素排列的前面就可以了,即 e 1 e_1 e1.list[ E 1 E_1 E1], e 2 e_2 e2.list[ E 2 E_2 E2]直到 e n e_n en.list[ E n E_n En],组合起来不就是E中所有元素的全排列了嘛,然后在递归的求值 E 1 E_1 E1的排列,直到 E n E_n En的,就完成了求解。

在这里插入图片描述

那么如何用程序实现呢?假设E是一个数组来存储的,那么范围就是0-n,我们以k,m表示E集合在数组中的起始下标和终止下标,还是以图中的集合为例,数组num大小为3,那么E的集合存储在num中,下标对应于0-2,那么就是k=0,m=2.然后以i表示 e i e_i ei,就是表示从E中取出一个元素,那么剩下的元素,就是 E i E_i Ei了,用数组里怎么表示呢?从图中可以看到,i是按照数组的下标顺序,也就是从k开始直到m结束的,i所以可以用一个for循环,从k到m逐渐赋值,那么 E i E_i Ei呢?为了递归的简单,我们可以把i这个元素与k所在的元素交换,这样下标k+1到m不就是表示的是除了 e i e_i ei的其余元素了嘛。比如以上图的[1,2,3]所示,首先k等于0,m=2,i=0,那么将i与k的元素交换,k=1,m=2,此时不就是 E 1 E_1 E1=[2,3]了嘛,正好表示除了第一个元素的其余元素的集合。当i=0递归结束时,i加1变为i=1时,此时k还是等于0的,m还是等于2,因为绿色的是在一个递归层级,此时的k和m都是一样的。将num[0]与num[1]交换,此时num=[2,1,3],此时k=1,m=2,那么 E 2 E_2 E2=[1,3]不就是表示的是num除去第二个元素后其余元素的集合嘛,以此类推。可以写出如下的递归程序。

//nums就是输入的集合,answer用于存储最后的全排列,k是集合起点的下标,m是集合的终点。
void permutations(vector<int> &nums,int k,int m,vector<vector<int>>&answer)
{
    if(k==m) //k==m表示剩余集合只有一个元素
    {
        answer.push_back(nums);
    }
    else
    {
       for(int i=k;i<=m;i++) //从k到m逐渐递归
       {
          swap(&nums[k],&nums[i]);
          permutations(nums,k+1,m,answer);
          swap(&nums[k],&nums[i]); //恢复原来数组的形式,也就是恢复原来集合的顺序
       }
    }
}

在这里插入图片描述

整个程序的递归过程如图红线所示,从左到右。

整个题解如下所示:

class Solution {
public:
    vector<vector<int>> permute(vector<int>& nums) {
         vector<vector<int>> answer;
         if(nums.size()==0)
         {
             return answer;
         }
         permutations(nums,0,nums.size()-1,answer);
         return answer;
    }
    void permutations(vector<int> &nums,int k,int m,vector<vector<int>>&answer)
    {
        if(k==m)
        {
            answer.push_back(nums);
        }
        else
        {
            for(int i=k;i<=m;i++)
            {
                swap(&nums[k],&nums[i]);
                permutations(nums,k+1,m,answer);
                swap(&nums[k],&nums[i]);
            }
        }
    }
    void swap(int *a,int *b)
    {
        int tmp;
        tmp=*a;
        *a=*b;
        *b=tmp;
    }
};
2.全排列2(letcode 47)

相比于上一题,这个题的改变在于输入的数组有重复的数字,所以相比较于上一个代码,只需要在递归前判断是否是重复的就可以,如何判断是否重复呢?就是扫描k到i之间[k,i)之间是否与nums[i]有相同的数字,如果有,就是重复的,因为这一轮从k到m的递归之中相同的数据交换得到的集合也是相同的,全排列也是相同的。

在这里插入图片描述

 void permutations(vector<int> &nums,int k,int m,vector<vector<int>>&answer)
    {
        if(k==m)
        {
            answer.push_back(nums);
        }
        else
        {
            for(int i=k;i<=m;i++)
            {
                if(issame(nums,k,i)) //添加重复判断
                {
                    swap(&nums[k],&nums[i]);
                    permutations(nums,k+1,m,answer);
                    swap(&nums[k],&nums[i]);
                }
            }
        }
    }
    bool issame(vector<int> nums,int begin,int end)
    {
        for(int i=begin;i<end;i++)
        {
            if(nums[i]==nums[end])
            {
                return false;
            }
        }
        return true;
    }
3.按照最小变化要求全排列

所谓最小变化,就是相邻的两个排列,只能是改变一个位置,不能改变超过一个位置,什么意思呢,举个例子来说,123,132就是最小变化,而123,312就不是。

这个问题就是依靠减治法来实现的,即可以从下到上,也可以从上到下。假设需要求解3的全排列,那么首先排列1,然后在1的基础上插入2,从右向左插入,先插入到位置1,则得到一个序列12,然后插入到位置0,则得到下一个序列21,此时得到的序列{12,21},然后将3插入,为了得到最小变化的要求,需要按照先将一个序列从左到右,下一个序列就得从右向左插入,举例说明:先将3从右到左插入12得到{123,132,312},然后在将3从左到右插入21得到{321,231,213},这样得到了一个最小变化的全排列了{123,132,312,321,231,213}。

#include<iostream>
#include<string>
#include<vector>
using namespace std;
//不知道为啥codeblocks不支持to_string函数,在网上找了一个

#define max 100
string to_String(int n)
{
	int m = n;
	char s[max];
	char ss[max];
	int i=0,j=0;
	if (n < 0)// 处理负数

	{
	    m = 0 - m;
        j = 1;
        ss[0] = '-';
	}
	while (m>0)
	{
        s[i++] = m % 10 + '0';
        m /= 10;
	}
    s[i] = '\0';
    i = i - 1;
	while (i >= 0)
	{
	    ss[j++] = s[i--];
	}
    ss[j] = '\0';
    return ss;
}
//创建以最小变化要求的全排列
void creatPerm(vector<string> &answer,int n,int &flag)
{
    string tmp;
    string s;
    vector<string> tmp_answer;
    if(n==1)
    {
        tmp="1";
        answer.push_back(tmp);
	}
    else
    {
        //减一法
        creatPerm(answer,n-1,flag);
    //扫描answer中的值,也就是前面n-1个组成的所有序列
        for(int i=0;i<answer.size();i++)
        {
            tmp=answer[i];//取出每一个序列
        //flag=0,从左向右插入 即12,插入3的话,就是312,132,123
            if(flag==0)
            {
                for(int j=0;j<=tmp.size();j++)//按照一个序列12的大小为3,所以依次从左到右插入0,1,2
                {
                    s=tmp;
                    s.insert(j,to_String(n));
                    tmp_answer.push_back(s);
                    flag=1;
                }
            }
//flag=1,从右向左插入 即12,插入3的话,就是123,132,312
            else
            {
                for(int j=tmp.size();j>=0;j--)//按照一个序列12的大小为3,所以依次从右到左插入2,1,0
                {
                    s=tmp;
                    s.insert(j,to_String(n));
                    tmp_answer.push_back(s);
                    flag=0;
                }
            }
        }
    answer.clear();//将之前的n-1的排列删除
    answer=tmp_answer;//将新的n的排列赋值

	}
}
int main()
{
    vector<string> answer;
    int n;
    int flag=1;
    cin>>n;
    creatPerm(answer,n,flag);
    for(int i=0;i<answer.size();i++)
    {
        cout<<answer[i]<<" ";
    }
    return 0;
}

4.按照字典序的全排列

以字典序的全排列,就是按照数字大小或者字母顺序的排列,比如123,132,213就是字典序,而123,213,122就不是了。对于字典序,我们按照这种算法来实现,比如数字346987521的下一个字典序数字是啥?首先从后向前扫描到第一个下降的元素,以这个数字为例的话,就是6,因为从1-9都是升序,在A[2]<A[3],所以保存j=2,然后在从后面扫描找到第一个比6大的数,A[5]>A[2],即i=5,然后交换A[i]和A[j],此时A[j]之后的数字是从大到小的顺序,将这段数据反转,就得到了下一个字典序的排列。重复这个过程,直达从尾部到头部扫描都是递增的,说明已经全部完成,结束程序。

#include<iostream>
#include<string>
#include<vector>
using namespace std;
//不知道为啥codeblocks不支持to_string函数,在网上找了一个
#define max 100
 string to_String(int n)
 {
     int m = n;
      char s[max];
     char ss[max];
    int i=0,j=0;
     if (n < 0)// 处理负数
    {
         m = 0 - m;
         j = 1;
         ss[0] = '-';
     }
    while (m>0)
     {
         s[i++] = m % 10 + '0';
         m /= 10;
     }
     s[i] = '\0';
     i = i - 1;
     while (i >= 0)
     {
         ss[j++] = s[i--];
    }
     ss[j] = '\0';
     return ss;
}
void reverse_string(string &s,int start)
{
    int j=s.size()-1;
    while(start<=j)
    {
        swap(s[start],s[j]);
        start++;
        j--;
    }
}
//创建以字典序的全排列
void creatPerm(vector<string> &answer,int n,int &flag)
{
    string s;
    for(int i=1;i<=n;i++)
    {
        s=s+to_String(i);
    }
    answer.push_back(s);
    if(n==1)
    {
        return;
    }
    while(1)
    {
       int i=s.size()-1;
       int j=s.size()-2;
       while(j>=0&&s[j]>s[j+1])  //从后向前扫描,找到第一个下降的元素位置
       {
           j--;
       }
       if(j<0)
       {
           break;
       }
       while(i>j&&s[i]<s[j]) //从后向前扫描,找到第一个比j位置元素大的元素
       {
           i--;
       }
       swap(s[i],s[j]);
       reverse_string(s,j+1); //反转j+1后的元素
       answer.push_back(s);
    }
}
int main()
{
    vector<string> answer;
    int n;
    int flag=1;
    cin>>n;
    creatPerm(answer,n,flag);
    for(int i=0;i<answer.size();i++)
    {
        cout<<answer[i]<<" ";
    }
    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值