我是自动化专业的应届研究生,最终拿到了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,n∗f(n−1),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=Fn−1+Fn−2
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;
}