递归入门,例题详解,汉诺塔问题,全排列问题,整数划分问题,两数相加

news2024/10/5 14:21:54

问题一:阶乘

对于阶乘n!,也就是从1一直乘到n,我们可以很简单的使用一个for循环来解决这个问题,但是如果使用递归的思路,那么我们需要思考如果将当前的问题分解为规模更小的问题,对于n的阶乘,我们如何将n的规模减少呢,想象比如我们现在已经有了n-1的阶乘,那我们就可以很容易地得出 n = n ∗ ( n − 1 ) ! n=n*(n-1)! n=n(n1)!

这样就可以把求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(n1)+F(n2),接着来想边界条件,对于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,m1)

当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,m1)+q(nm,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;
    }
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/975159.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

论文解读 | KPConv——点云上的可形变卷积网络

原创 | 文 BFT机器人 《KPConv: Flexible and Deformable Convolution for Point Clouds》是一篇发表于2019年的研究论文&#xff0c;作者为Hugues Thomas、Charles R. Qi、Jean-Emmanuel Deschaud、Beatriz Marcotegui和Franois Goulette。这篇论文关注于点云数据上的卷积操作…

前置微小信号放大器是什么

前置微小信号放大器是一种专门用于放大微弱输入信号的电子设备。它常用于电子测量、信号传输、音频放大等领域&#xff0c;能够将微小的输入信号放大到足够大的幅度&#xff0c;以便后续处理或传输。下面我们将从工作原理、应用和发展趋势三个方面&#xff0c;详细探讨前置微小…

在国内PMP的含金量高吗?

首先我们需要了解一下PMP证书的用处 PMP含金量是毋庸置疑的&#xff0c;从事项目/产品/运营/管理/IT行业的社会人基本都会将这个证收入囊中。其他有升职涨薪计划的也在悄咪咪报考蓄力中&#xff0c;能在职业生涯锦上添花&#xff0c;精益求精。 一&#xff0c;PMP证书的优势体…

什么是安全运营中心(SOC),应该了解什么

安全运营中心&#xff08;SOC&#xff09; 是一种企业监视和警报设施&#xff0c;可帮助组织检测安全威胁、监视安全事件和分析性能数据以改进公司运营。 什么是安全运营中心&#xff08;SOC&#xff09; 安全运营中心&#xff08;SOC&#xff09;是一个中央监视和监视中心&a…

安装气象站的重要意义是什么?

气象站是一种用于观测、记录和报告天气数据的设备或系统。它通常包括各种传感器、供电系统和环境监控主机&#xff0c;用于测量和记录气温、湿度、风速、风向、气压、降雨量、雪深等气象参数。 气象站有多种类型&#xff0c;包括自动气象站、人工气象站和便携式气象站。自动气…

elementUI可拖拉宽度抽屉

1&#xff0c;需求&#xff1a; 在elementUI的抽屉基础上&#xff0c;添加可拖动侧边栏宽度的功能&#xff0c;实现效果如下&#xff1a; 2&#xff0c;在原组件上添加自定义命令 <el-drawer v-drawerDrag"left" :visible.sync"drawerVisible" direc…

【狂神】SpringMVC 怎样才能直接手动输入.jsp的页面就可以访问了?

看秦老师视频的时候&#xff0c;觉得非常疑惑&#xff0c;为什么可以直接输入form.jsp就能跳转到相应地页面。如果你和我一样眼瞎&#xff0c;那确实是有点崩溃。注意看&#xff1a; 发现了吗&#xff1f;这几个文件并没有放在WEB-INF文件下&#xff0c;所以视图解析器便不生效…

实用技巧:使用Python进行文本处理

引言 作为一个Linux持续学习者&#xff0c;我们经常需要处理文本文件&#xff0c;例如提取特定内容、格式化数据或者进行文本分析等。在这篇文章中&#xff0c;我将介绍使用Python进行文本处理的一些实用技巧&#xff0c;帮助你更有效地处理文本数据。无需担心&#xff0c;你不…

pdf可以编辑修改内容吗?看看这几种编辑方法

pdf可以编辑修改内容吗&#xff1f;PDF文件是一种广泛使用的文件格式&#xff0c;但是有时候我们需要对PDF文件进行编辑和修改。那么&#xff0c;PDF文件可以编辑修改内容吗&#xff1f;答案是肯定的。下面介绍几种编辑PDF文件的方法。 第一种方法是使用【迅捷PDF编辑器】。 这…

java JUC并发编程 第六章 CAS

系列文章目录 第一章 java JUC并发编程 Future: link 第二章 java JUC并发编程 多线程锁: link 第三章 java JUC并发编程 中断机制: link 第四章 java JUC并发编程 java内存模型JMM: link 第五章 java JUC并发编程 volatile与JMM: link 第六章 java JUC并发编程 CAS: link 文章…

Springboot上传文件

上传文件示例代码&#xff1a; ApiOperation("上传文件") PostMapping(value "/uploadFile", consumes MediaType.MULTIPART_FORM_DATA_VALUE) public ApiResult<String> uploadFile(RequestPart("file") MultipartFile file) { //调用七…

从0开始 yolov5可以用灰度图像进行训练和检测吗

yolov5可以用灰度图像进行训练吗,从0开始yolov5灰度图训练和检测 文章目录 yolov5可以用灰度图像进行训练吗,从0开始yolov5灰度图训练和检测[toc]1 预演【表1-1 模型结构截取】 2 修改源码使可以灰度训练2.1 修改读取图片模式2.2 修改源码传参中的通道数2.3 运行train.py2.4 修…

java八股文面试[数据库]——BufferPool

Buffer Pool是MYSQL数据库中的一个重要的内存组件&#xff0c;介于外部系统和存储引擎之间的一个缓存区&#xff0c;针数据库的增删改查这些操作都是针对这个内存数据结构中的缓存数据执行的,在操作数据之前&#xff0c;都会将数据从磁盘加载到Buffer Pool中&#xff0c;操作完…

亚马逊店铺如何快速出单?

对于一个新开的亚马逊店铺而言&#xff0c;首先要做的就是想办法让自己的店铺快速出单&#xff0c;只有有订单了&#xff0c;才能够稳住局势&#xff0c;才能够让自己亚马逊店铺在市场上有立足的资本。 不过对于一个亚马逊店铺而言&#xff0c;要想做到快速出单是很难的&#…

一文读懂GPU显卡的10个重要参数

在当今的高性能计算机世界中&#xff0c;GPU显卡的性能至关重要。这一领域的快速发展&#xff0c;使得图形渲染、游戏体验、视频编辑等高性能计算任务变得更加高效和流畅。正因如此&#xff0c;选择一款合适的GPU显卡变得越来越重要。在挑选GPU显卡时&#xff0c;了解其关键参数…

uni-app:实现右侧弹窗

效果&#xff1a; 代码&#xff1a; <template><view class"container"><button click"showModal true">点击按钮</button><view class"modal-overlay" v-if"showModal" click"closeModal">…

没有软件怎么管理固定资产

在当今数字化的世界中&#xff0c;我们已经习惯了使用各种软件来管理我们的日常生活和工作。然而&#xff0c;当我们面临一个看似简单的问题——如何管理固定资产时&#xff0c;我们可能会感到困惑。那么&#xff0c;如果没有软件&#xff0c;我们该如何进行资产管理呢&#xf…

QT C++ 基于TCP通信的网络聊天室

一、基本原理及流程 1&#xff09;知识回顾&#xff08;C语言中的TCP流程&#xff09; 2&#xff09;QT中的服务器端/客户端的操作流程 二、代码实现 1&#xff09;服务器 .ui .pro 在pro文件中添加network库 .h #ifndef WIDGET_H #define WIDGET_H#include <QWidget>…

Netty—FuturePromise

Netty—Future&Promise 一、JDK原生 Future二、Netty包下的 Future三、Promise1、使用Promise同步获取结果2、使用Promise异步获取结果.3、使用Promise同步获取异常 - sync & get4、使用Promise同步获取异常 - await5、使用Promise异步获取异常 在异步处理时&#xff0…

uniapp - 倒计时组件-优化循环时间倒计时

使用定时器的规避方法 为了避免定时器误差导致倒计时计算错误&#xff0c;可以采用一些规避方法&#xff0c;比如将倒计时被中断时的剩余时间记录下来&#xff0c;重新开启定时器时再将这个剩余时间加到新的计算中。同时&#xff0c;为了避免定时器延迟&#xff0c;可以在每次执…