问题一:阶乘
对于阶乘n!,也就是从1一直乘到n,我们可以很简单的使用一个for循环来解决这个问题,但是如果使用递归的思路,那么我们需要思考如果将当前的问题分解为规模更小的问题,对于n的阶乘,我们如何将n的规模减少呢,想象比如我们现在已经有了n-1的阶乘,那我们就可以很容易地得出 n = n ∗ ( n − 1 ) ! n=n*(n-1)! n=n∗(n−1)!
这样就可以把求n的阶乘转化为求n-1的阶乘,我们进而对n-2求阶乘,这样一直往下分解直到边界条件,众所周知递归三要素:函数,边界,递推公式,接着我们思考这个问题的边界条件,我们可以一直减一直到无穷无尽吗?这肯定是不行的,我们一直减一直到最小的0,0的阶乘是1,那我们就可以直接返回1,当然你也可以递归到1就返回1,这会出现一个问题也就是输入0或者小于0会出现bug,很有可能会遇到死循环,这个代码会一直计算负数的阶乘,所以说为了代码的健壮性,防止用户输入负数,我们需要判断一下,如果n已经小于0那么我们返回-1。为什么返回-1?因为根据我局限的知识,负数好像没有阶乘所以我们直接返回-1。
// 求n的阶乘
int factorial(int n) {
if(n==0) return 1;
else if (n<0) return -1;
else return n*factorial(n-1);
}
问题二:斐波那契数列
斐波那契数列(Fibonacci sequence),又称黄金分割数列,因数学家莱昂纳多·斐波那契(Leonardo Fibonacci)以兔子繁殖为例子而引入,故又称“兔子数列”,其数值为:1、1、2、3、5、8、13、21、34……在数学上,这一数列以如下递推的方法定义:F(0)=1,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N*)。
斐波那契数列简单来说就是第一个月有一对小兔子,小兔子需要花两个月的时间长大,长大之后每个月都会生下一对小兔子并且永远不会死亡,新出生的小兔子同样需要两个月时间长大,长大之后也是每个月都可以生下一对小兔子,我们所求的第n项也就是第n个月当前兔子的数目。
所以斐波那契数列的第一项和第二项都是1,因为兔子需要两个月的时间长大,长大之后第三个月剩下一对小兔子,所以斐波那契数列的第三项是2,分析这个数列,我们很容易得出当前的一项等于这一项前两项的和。
那么对于斐波那契数列的第n项,我们可以直接转化为n-1项和n-2项的和值,也就是 F ( n ) = F ( n − 1 ) + F ( n − 2 ) F(n)=F(n-1)+F(n-2) F(n)=F(n−1)+F(n−2),接着来想边界条件,对于n很大的话-1-2当然不会出问题,但是如果当前计算的是F(1)呢?你没办法把F(1)转变为F(0)+F(-1),所以说这是一个边界条件,我们直接返回1即可,F(2)同理,如果你继续用公式的话需要F(0)的值,但是我们是从第一项开始的没办法计算同样返回1。那么你就能写出下面的代码,前两行为边界条件,第三行为递推公式。
// 求斐波那契数列的第n项
int fibonacci(int n) {
if(n==1) return 1;
else if(n==2) return 1;
else return fibonacci(n-1)+fibonacci(n-2);
}
问题三:汉诺塔问题
相传在古印度圣庙中,有一种被称为汉诺塔(Hanoi)的游戏。该游戏是在一块铜板装置上,有三根杆(编号A、B、C),在A杆自下而上、由大到小按顺序放置64个金盘(如图1)。游戏的目标:把A杆上的金盘全部移到C杆上,并仍保持原有顺序叠好。操作规则:每次只能移动一个盘子,并且在移动过程中三根杆上都始终保持大盘在下,小盘在上,操作过程中盘子可以置于A、B、C任一杆上。
简单来说就是我们需要把圆盘从a柱移动到c柱,并且要遵循小盘子放在大盘子的上面的原则,如何解决这个问题?比方说我们要移动n个盘子从a柱到c柱,我们先将问题规模缩小,先想想能不能移动n-1个盘子,如果我们能把n-1个盘子移动a柱移动到b柱,那我们只需要将最下面的那个大盘子移动到c柱上,接着再将n-1个盘子移动到c柱即可。
所以说对于n个圆盘的移动,我们可以先移动n-1个盘子,然后考虑n-1个盘子的时候继续缩小问题规模考虑移动n-2个盘子,一直将问题规模缩小到将一个盘子移动到另一个柱子,那么这其实非常地简单只需要直接移动即可。
让我们重新捋一下汉诺塔问题的思路:移动n个盘子从a柱到c柱,先将n-1个盘子从a柱移动到b柱借助c柱,接着将最下面的盘子移动到c柱,接着将n-1个盘子从b柱移动到c柱借助a柱,边界条件是当前只需要移动一个盘子,你就可以得到如下的代码。
// 将n个盘子从a柱移动到c柱借助b柱
void hanoi(int n,char a,char b,char c) {
// 如果当前n为1也就是只剩一个盘子直接移动即可
if(n==1) {
// 输出当前是第几个盘子,从哪个柱子移动另一个柱子
cout<<n<<" "<<a<<"->"<<c<<"\n";
return;
}
// 先移动前n-1个盘子从a借助c移动到b
hanoi(n-1,a,c,b);
// 将第n个盘子从a柱移动到c柱
cout<<n<<" "<<a<<"->"<<c<<"\n";
// 将n-1个盘子从b柱借助a移动到c
hanoi(n-1,b,a,c);
}
调用及输出结果展示
hanoi(4,'A','B','C');
问题四:排列问题
问题描述:对n个元素进行全排列,列出所有情况,例如1,2,3三个数字会得到1 2 3,1 3 2,2 1 3,2 3 1,3 1 2,3 2 1这6中情况
思路:设n为元素个数,元素集合为R(r1,r2,r3…rn),计算方法为Perm(n)
当n = 1时,则只有一种情况 r;
当n > 1时,则有(r1)Perm(R1),(r2)Perm(R2),(r3)Perm(R3) … … (rn)Perm(Rn)
以1,2,3为例全排列,共有以下排列:
1 Perm(2,3) 即:以1为前缀的所有组合
2 Perm(1,3) 即:以2为前缀的所有组合
3 Perm(2,3) 即:以3为前缀的所有组合
注:Perm(k,m)利用递归的思想即可不断划分前缀,直到只剩下1个元素,则只有一种情况,即为找到了一种排列。
也就是全排列问题,对于一串数字a1到an,输出这n个数所有排列顺序,使用递归的想法来解决这个问题,递归的核心是缩小规模,所以比方说我们先把第一个数定下来,那么接着我们只需要输出剩下n-1个数的全排列即可,这样就可以缩小问题规模。
比如我们要对1,2,3,4进行全排列,先把第一个数1定下来之后对2、3、4全排列,这一趟完成之后,我们要改变第一个数,因为第一个数可以选1也可以选其他的数,我们将第一个数跟第二个数交换,这样就变成了2,1,3,4;我们这次把第一个数当成了2,然后对1,3,4全排列,一直做类似的动作,一直到第一个数跟第n个数交换。
跟前面思路相同,这次我们固定第一个数,剩下的1,3,4要想获得全部的全排列也需要第一个数跟第二个数交换,交换之后需要交换回去否则会影响,重复这个步骤直到确定前n-1个数,接着只剩下一个元素,此时已经构成了一个排列直接输出即可。所以我们需要一个循环遍历所有交换,同时需要两个变量同时记录第一个数和最后一个数,我们使用start来代表第一个数的位置,end来表示最后一个数的位置。这个start以前的数都已经确定好了,我们接着要确定从start到end这区间的数。
int a[1010];
// 对数组a进行全排列,start为开始交换的位置,end为结束位置
void permutation(int* a,int start,int end) {
// 如果开始和结束位置重叠表明这已经是最后一个数,输出当前的排列
if(start==end) {
for(int i=1;i<=end;i++) {
cout<<a[i]<<" ";
}
cout<<endl;
return;
}
// 遍历所有交换情况
for(int i=start;i<=end;i++) {
swap(a[i],a[start]);
permutation(a,start+1,end);
// 恢复原状否则会相互冲突
swap(a[i],a[start]);
}
}
调用及输出结果展示
for(int i=1;i<=4;i++) {
a[i]=i;
}
permutation(a,1,4);
问题五:整数划分问题
将正整数n表示为一系列正整数之和,
n=n1+n2+n3+n4+…+nk (其中,n1>=n2>=n3>=n4…>=nk>0,k>=1)
正整数n的这种表示成为正整数n的划分。正整数n的不同划分个数成为正整数n的划分数,记作p(n)。
例如,正整数6有如下11种划分,所以p(6)=11。
6;
5+1;
4+2,4+1+1;
3+3,3+2+1,3+1+1+1;
2+2+2,2+2+1+1,2+1+1+1+1;
1+1+1+1+1+1。
而我们所要求的也就是这个正整数n总共有多少种划分办法,上面每一行都是最大的那个加数分别为6,5,4,3,2,1,所以我们可以定义m为最大加数,然后分类讨论,当n=1时,这时只有一种划分情况就是1,当m=1时,这时不管n为几,我们都只能将n划分为所有1的和,我们设函数q(n,m)为当前n和m时的划分个数,那么
q
(
1
,
m
)
=
q
(
n
,
1
)
=
1
q(1,m)=q(n,1)=1
q(1,m)=q(n,1)=1
当n=m时,也就是我们要划分的数为n,最大加数也是n,我们将这种情况分为两类,如果最大加数是n以及最大加数小于n,最大加数是n其实只有一种情况,最大加数小于n有多种情况我们放在其他类讨论,那么总结 q ( n , n ) = 1 + q ( n , m − 1 ) q(n,n)=1+q(n,m-1) q(n,n)=1+q(n,m−1)
当n>m时,也就是我们划分的话,最大加数要小于n,比方说此时n=6,m=5,我们可以继续将这种情况分成两类,一种是最大加数是m,另一种是最大加数小于m,当最大加数小于m,那么也就是q(n,m-1),最大加数是m的话,6=5+1,m是固定的,也就是说我们要划分的此时不再是n而是n-m,想想为什么,如果我们此时的划分不包括m,那么就属于另一类,这一种情况一定会包含最大加数m,所以m是一定有的,我们所要做的就是划分剩下的n-m即可,总结来说就是 q ( n , m ) = q ( n , m − 1 ) + q ( n − m , m ) , n > m q(n,m)=q(n,m-1)+q(n-m,m),n>m q(n,m)=q(n,m−1)+q(n−m,m),n>m
最后一种情况是n<m,这时我们最大加数m大于要划分的数n ,这是不可能的,比如要划分数字6,你不能把7从6里面划分出来,因为我们只能包含正整数,所以我们可以将m直接替换为n,这种情况则在上面已经讨论过 q ( n , m ) = q ( n , n ) , n < m q(n,m)=q(n,n),n<m q(n,m)=q(n,n),n<m
最后我们将这所有情况写为代码即可。
// 输出n的划分个数,m为最大加数
int q(int n,int m) {
// 保证健壮性,如果都小于1那么直接返回0
if(n<1||m<1) return 0;
// 划分数为或者最大加数为1,那么都只有一种划分方法
else if(n==1||m==1) return 1;
// 两者相等,分为最大加数有m和最大加数没有m
else if(n==m) return 1+q(n,m-1);
// 划分为两种情况,划分中有最大加数和划分中没有最大加数
// 如果划分中有最大加数,那么我们要计算的是n-m这个数的划分个数
else if(n>m) return q(n,m-1)+q(n-m,m);
// 最大加数大于要划分的数,直接将最大加数替换为要划分的数
else if(n<m) return q(n,n);
}
调用时,只需要输入q(n,n)即可,比如想要得到整数6有多少种划分方法直接使用q(6,6)调用
问题六:合并两个有序链表
本题为力扣例题
大致意思就是把两个有序的链表按照从小到大的顺序排列起来
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
既然这两个链表已经有序了,那我们只需要依次比较两个链表的首部,如果第一个链表的首部小于第二个链表,那我们就拿第一个链表的首部当作答案链表中的一项,否则就是第二个链表当作答案链表中的一项,我们要使用递归来解决这道题,那么必须要考虑的一个问题就是如何缩小规模使得缩小后的问题与当前的问题解决思路一致
我们可以首先拿出这两个链表中首部较小的一项,然后剩下来的问题就变成了,对于这两个剩下的链表继续合并变成一个有序链表,这里我们就可以发现我们成功将问题的规模缩小了,如果递归函数的名称设为F(l1,l2),那么我们这一步之后就变成了F(l1->next,l2)或者F(l1,l2->next),如果我们先拿了第一个链表的第一项,那么最终链表的下一项我们直接交给递归函数即可,而我们要做的是更新参数,比如拿走了list1的第一项,参数中的list1就需要指向下一个元素,然后我们把他们连接起来,就是当前的list1->next指向递归函数,大概意思就是我们将list1的下一项指向了函数,这个函数会选择较小的下一项然后返回。最后思考边界条件,如果list1当前已经为空指针,我们直接返回list2,list2后面的元素肯定都不小于当前,list2为空同理。
最终代码如下
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
// 将两个有序链表合并成为一个更大的有序链表
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
// 如果list1已经到结尾,我们直接返回剩下的list2,反之同理
if(list1==nullptr) return list2;
if(list2==nullptr) return list1;
// 如果list1的首部小于list2,list1后面跟着递归函数即可,函数会依次排列好链表
if(list1->val<=list2->val) {
list1->next=mergeTwoLists(list1->next,list2);
return list1;
}
else {
list2->next=mergeTwoLists(list1,list2->next);
return list2;
}
}
};
问题七:两数相加
力扣原题在这里
题目大意就是给两个链表,让你将他们两个相加,倒序,进位放在链表后面的结点上
我们在原函数中增加一个新的参数carry来代表前面的进位,默认为0,然后将当前两个链表的首项相加,如果大于10那么就进位,将carry表示为1,每次相加的时候同时加上carry,然后我们永远保证l1这个链表是比较长的链表,如果l1和l2当前都是空指针并且carry有值,那么就创建一个新指针使用carry这个值并返回,如果carry没有值直接返回空指针即可,如果l1和l2不满足前面那个条件说明此时这两个必定有一个有值,在这个情况下如果l1是空指针,那就代表l2是有值的,我们直接交换l1和l2即可,接着我们计算过和之后,将l1的next指向递归函数计算下一项即可,然后返回l1
代码如下
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
// 计算链表l1和链表l2的和,carry为上一项的进位
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2, int carry=0) {
// 如果两项都为空指针并且carry有值创建新指针,否则直接返回null
if(l1==nullptr&&l2==nullptr) {
if(carry) return new ListNode(carry);
return nullptr;
}
// 不满足上面那个条件说明至少有一项有值,在这个情况下l1为空,那么就交换l1和l2,保证l1永远不短于l2
if(l1==nullptr) swap(l1,l2);
// 将和值存放在l1中,需要判断一下l2当前是否为空指针,如果为空指针就加上0,还需要加上carry
l1->val+=(l2?l2->val:0)+carry;
// carry记录是否有进位
carry=(l1->val)/10;
// 确保这位数是个位数
l1->val%=10;
// 将后续的情况链接到l1,l2如果为空,那么就填入空指针
l1->next=addTwoNumbers(l1->next,(l2?l2->next:nullptr),carry);
return l1;
}
};