系统学习算法: 专题七 递归

递归算法简而言之就是当一个大问题拆分为多个子问题时,如果每个子问题的操作步骤都一样,就可以用递归,其中递归在递的时候要有结束条件,不能一直递下去,结束条件后就归

这里不建议学习递归的时候抠细节,还原每一步递归的结果,要从宏观上来理解递归,将递归函数视为一个小黑盒,绝对无条件相信这个小黑盒能够完成其使命,然后再分析传参哪些参数合适,结束条件是什么就可以写出递归了

递归也是为之后的搜索算法以及回溯算法打基础,要熟练掌握

题目一:

最经典的递归题目之一,其实不算作简单的递归题,对于刚学习递归还是相当有挑战性

这个挑战性就是让之前抠细节的递归思想转化为宏观递归,造成思想上的转变

图就不画了,一是不好画,递过去归回来的,且容易陷入抠细节流程图,不够宏观

但是会主要展示代码的书写顺序,通过书写顺序来理解递归算法,容易站在宏观视角

思路:

一开始盘子都在起始柱子A,因为大的再最底下,所以要让除了最底下的盘子都拿走放在中转柱子B,才能让最大的盘子移动到目标柱子C,然后再让其他盘子从中转柱子B移动到目标柱子C

那么又如何让其他盘子移动到C呢,那么就要让除了其他盘子中最大的盘子先拿到中转柱子A上,让其他盘子中最大的盘子移动到目标柱子C,再让其他盘子移动到目标柱子C

……

所以这个大问题就拆为上述的子问题,而每一个子问题的操作流程是一样的,根据归纳总结就是:

(此时操作对象为n个盘子)

{

让n-1个盘子先从起始柱子移动到中转柱子

再让最大盘子从起始柱子移动到目标盘子

再让n-1个盘子移动从中转柱子移动到目标柱子

}

由此可以知道我们定义递归的参数应该有四个:起始柱子,中转柱子,目标柱子,操作对象数量n

那么就开始写函数,不要想太多,找到每个子问题的相同的操作流程就写,再思考又代进去递归下一轮,就越来越绕了

第一步:

先确定参数个数(X为起始柱子,Y为中转柱子,Z为目标柱子,n为操作对象个数)

    public void bfs(List<Integer> X,List<Integer> Y,List<Integer> Z,int n){}

 第二步:

确定该函数的用途,即你想让这个函数能具有什么功能

我们想让n个盘子能够从起始X开始,借助中转Y,到达目标Z

完美解决题目,然后就绝对无条件信任这个函数,管它代码还没写,它就是能做到我的要求

第三步:

将上述子问题相同步骤的流程转化为代码

1.

让n-1个盘子先从起始柱子移动到中转柱子

怎么实现呢?我们创建的递归函数就能直接实现,其中原起始柱子为起始柱子,而原中转柱子为目标柱子了,那么原目标柱子就为中转柱子了,然后操作对象个数n-1,直接调用递归函数传参即可

(绝对无条件信任递归函数)

    public void bfs(List<Integer> X,List<Integer> Y,List<Integer> Z,int n){bfs(X,Z,Y,n-1);  //操作1}

2.

再让最大盘子从起始柱子移动到目标盘子

题目给的是List数据结构,那么就直接通过add函数和remove函数实现移动的步骤

    public void bfs(List<Integer> X,List<Integer> Y,List<Integer> Z,int n){bfs(X,Z,Y,n-1);  //操作1Z.add(X.remove(X.size()-1));  //操作2}

注意add是尾插,remove的返回值就是删除的那个值

最重要的是为什么是size()-1而不是X[0],很关键的一个混淆地方,那就是我们理解成了要移动的是最底下的盘子,其实是移动当前情况的最上面的盘子,因为此时A只有一个盘子,所以最底下的盘子是它,最上面的盘子也是它,所以会容易搞混

以3个盘子为例,我们默认1操作后,此时为这样的

但其实我们这个状况已经是接近大问题操作流程的尾声了,我们是从后往前递的,但实际上是从前往后归的,可以简单理解成“递”是在找一开始操作的位置(即结束条件),而“归”才是真正在做事的顺序

这里就明显可以看出来是size()-1而不是X[0],x[0]还是最底下那个大盘子,而实际我们要移动的最上面那个小盘子,即size()-1

如果没get到的话可以再思考一下,汉诺塔问题确实操作不算简单,只是太经典了而不是最容易的

3.

再让n-1个盘子移动从中转柱子移动到目标柱子

还是直接调用我们的递归函数,绝对无条件相信它

  public void bfs(List<Integer> X,List<Integer> Y,List<Integer> Z,int n){bfs(X,Z,Y,n-1);  //操作1Z.add(X.remove(X.size()-1));  //操作2bfs(Y,X,Z,n-1);  //操作3}

第四步:

找到结束条件,那就是当n==1时,那么我们就不用借助什么中转柱子了,直接让这个盘子从起始柱子移动到目标柱子,也是通过add和remove来实现移动的操作(和操作2一样的),并return

  public void bfs(List<Integer> X,List<Integer> Y,List<Integer> Z,int n){//结束条件if(n==1){Z.add(X.remove(X.size()-1));return;}bfs(X,Z,Y,n-1);  //操作1Z.add(X.remove(X.size()-1));  //操作2bfs(Y,X,Z,n-1);  //操作3}

第五步:

在main方法调用我们的递归函数

绝对无条件信任,我们递归函数的功能是:让n个盘子能够从起始X开始,借助中转Y,到达目标Z

所以最后主函数调用传参

class Solution {public void bfs(List<Integer> X,List<Integer> Y,List<Integer> Z,int n){//结束条件if(n==1){Z.add(X.remove(X.size()-1));return;}bfs(X,Z,Y,n-1);  //操作1Z.add(X.remove(X.size()-1));  //操作2bfs(Y,X,Z,n-1);  //操作3}public void hanota(List<Integer> A, List<Integer> B, List<Integer> C) {bfs(A,B,C,A.size());//主函数调用}
}

这道题就解决了,是不是感觉有些莫名其妙的,突然就解决了,所以就是要绝对无条件信任你的递归函数,它不会辜负你的

综上大致总结递归算法就是上述五步

1.确定参数

2.定义你的递归函数功能,并且相信它

3.将子问题的相同流程转化成代码

4.找到结束条件并return

5.主函数调用你的递归函数即可

没有抠所谓的细节递归流程图,一开始刚学可以抠一下,因为确实太莫名其妙了,还不够信任你的递归函数,但之后就不要抠了,要宏观的来写代码,不容易搞混且效率很高,对你的递归函数有足够底气去信任

题目二:

思路:

第一种思路就是用双指针,一个指向第一个链表,另一个指向第二个链表,然后循环比较大小,修改对应的next即可,之前学过就不多赘述了

class Solution {public ListNode mergeTwoLists(ListNode list1, ListNode list2) {//如果出现空链表的情况if(list1==null){return list2;}if(list2==null){return list1;}//cur1cur2为双指针,cur为当前结点,ret为头结点ListNode cur1=list1;ListNode cur2=list2;ListNode ret=null;ListNode cur=null;//确定头结点if(cur1.val<cur2.val){ret=cur1;cur=ret;cur1=cur1.next;}else{ret=cur2;cur=ret;cur2=cur2.next;}//遍历while(cur1!=null&&cur2!=null){if(cur1.val<cur2.val){cur.next=cur1;cur=cur1;cur1=cur1.next;}else{cur.next=cur2;cur=cur2;cur2=cur2.next;}}//如果其中一个链表遍历完了if(cur1!=null){cur.next=cur1;}if(cur2!=null){cur.next=cur2;}//返回头结点return ret;}
}

 上述这个方法也就是循环的方法

但其实循环和递归是可以相互转化的,因为每一次循环做的事情都是一样的,也就是说明子问题操作流程也是一样的,递归也是这个特性,那么就可以相互转化了

还是按照五步来走

第一步确定参数,我们子问题的相同操作流程为合并链表,比较两个链表的头结点,然后提取较小的出来,接下来让剩下的链表继续参与合并

所以我们的参数为两个链表的头结点

第二步定义功能,我们定义的递归函数的功能是给两个链表的头结点,使得两个链表能够合并,并返回头结点,还是绝对无条件信任

第三步操作流程转化为代码

无非是让当前结点比较一下头结点的大小,然后选择较小的那一个,让该结点的next指向后面的链表合并后的新结点

第四步找到结束条件

那就是当其中一个链表为空的时候,就返回null就行

第五步主函数调用递归函数

无条件信任就行

代码:

class Solution {public ListNode bfs(ListNode list1,ListNode list2){//结束条件if(list1==null){return list2;}if(list2==null){return list1;}//找到较小的结点if(list1.val<list2.val){//next指向后面合并链表返回较小的结点list1.next=bfs(list1.next,list2);//返回较小的结点return list1;}else{list2.next=bfs(list1,list2.next);return list2;            }}public ListNode mergeTwoLists(ListNode list1, ListNode list2) {//调用递归函数ListNode ret=bfs(list1,list2);return ret;}
}

总结:

既然循环和递归可以相互转化,那么什么时候用循环写着舒服,什么时候用递归写着舒服

 当像左边这样有很多分支的话,就用递归舒服,像右边这样单边树就用循环

比如遍历数组,它不会出现什么回溯,也没有其他多余的选择,就下标一直往后走就行了,就用循环很舒服

像汉诺塔那道题,因为有三个柱子,所以往那边移动就有多个选择,选择的地方有很多,分支就多,很复杂,所以递归写着就很舒服

题目三:

思路:

之前学链表的时候也做过, 那个时候用的是循环的方法来解决,通过记录前驱后继和当前结点,通过互相修改完成翻转,也不多赘述

代码(循环):

class Solution {public ListNode reverseList(ListNode head) {//如果是空链表if(head==null){return head;}//记录旧头结点的后继,并修改旧头结点的后继指向nullListNode cur=head.next;head.next=null;//往后遍历直到为空while(cur!=null){//记录当前位置的后继ListNode curN=cur.next;//让当前位置的后继指向前驱cur.next=head;//让当前位置成为前驱head=cur;//让后继成为当前位置cur=curN;}return head;}
}

稍微画画图还是很简单的

循环和递归可以相互转化,那么这道题递归其实没循环那么写着舒服,因为递归流程图是单边树情况,所以适合循环,但递归也可以来练一下,看着很简洁

第一步确定参数

大问题是要求给出头结点,将其翻转,并返回新的头结点,那么每个子问题也是需要给出头结点,将其翻转,再返回这个翻转后的头结点,所以需要头结点

第二步定义功能

这个递归函数能够实现链表翻转,并返回新头结点

第三步转化代码

我们让当前结点的next传入递归函数,就能拿到新头结点

那么此时只需要让当前结点的next指向当前结点,并修改当前结点的next指向null就行

然后返回新头结点

第四步结束条件

当结点为空时为结点的next为空就结束

第五步调用递归函数

因为这道题本身的函数参数和返回值与递归函数一样,就直接让当前函数成为递归函数

代码(递归):

class Solution {public ListNode reverseList(ListNode head) {//结束条件if(head==null||head.next==null){return head;}//将该结点之后的链表全部翻转并返回新头结点ListNode newHead=reverseList(head.next);//让当前的next的next指向当前结点head.next.next=head;//修改当前的next指向空head.next=null;//返回新头结点return newHead;}
}

虽然写着很简洁但还是会比较绕一点,其中newHead是不会变的,一直都是原链表的末尾结点,如果不理解画画图就明白了,不如循环写着舒服,但是很简洁

题目四:

思路:

因为大问题是将链表中两两结点进行交换,子问题是将剩下的链表两两结点进行交换,而每一个子问题的操作步骤都是一样的,所以可以用递归

假设函数的功能是交换链表的所有两两结点,并返回链表的新头结点

操作步骤:

通过递归函数得到剩下链表交换后的头结点tmp,备份当前大链表的头结点的next为ret,并且让ret的next指向当前大链表的头结点,让当前大链表的头结点指向tmp,最后返回ret就行

主要在顺序上要理清楚,大部分都是调用递归要放在前面,然后操作步骤在后面,因为要先往后递归,所以调用递归函数要放在代码块的前面

其中结束条件就是当只剩一个结点或者为null的情况就直接返回

代码:

class Solution {public ListNode swapPairs(ListNode head) {//结束条件if(head==null||head.next==null){return head;}//先调用ListNode tmp=swapPairs(head.next.next);//操作步骤ListNode ret=head.next;ret.next=head;head.next=tmp;//返回结果return ret;}
}

题目五:

思路:

按照题意来想非常简单,就是累乘n次的x就行,用循环和递归都可以

虽然结果一定是没问题的,但是这道题加了一些限制,其中n非常大,直接来到整型的最大值和最小值了 ,所以按照常规思路来写循环一定会超时,递归一定会栈溢出

那么接下来就要想优化

根据幂的运算法则我们可以进行下面的拆分

 比如3^16,原本就需要16次递归,而现在直接就只需4次递归(当n==0直接返回不再递归),相当于时间复杂度从O(N)来到了O(logN),大大提高了效率,这种方法也叫做快速幂

每个子问题是求得当前n的一半次幂,然后进行相乘,如果幂是奇数就再多乘一下自己本身

其中如果是负数就要转化为x^(-n)   ——>  1/x^n

代码1(正确但不完全正确):

class Solution {//递归函数public double pow(double x,int n){//结束条件if(n==0){return 1.0;}//求出n的一半次幂double ret=pow(x,n/2);//求当前的n次幂return n%2==0?ret*ret:ret*ret*x;}//主函数调用public double myPow(double x, int n) {return n<0?1.0/pow(x,-n):pow(x,n);}
}

如果这里直接提交到力扣,会发现通过,但其实有个小错误,但是误打误撞对了

那就是当n为-2^31时,这时负数变正为2^31,但是整型最大也就为2^31-1,会发生溢出,根据溢出的规则,-2^31取反溢出后还是-2^31,而-2^31经过多次的/2,最后会来到-1/2==0,刚好又符合结束条件,所以虽然结果是正确的,但是运行的逻辑与我们构想的其实是不一样的

解决整型溢出这个问题其实很简单,就是将会发生溢出的地方换成long就好

代码2(正确):

class Solution {//递归函数public double pow(double x,long n){//结束条件if(n==0){return 1.0;}//求出n的一半次幂double ret=pow(x,n/2);//求当前的n次幂return n%2==0?ret*ret:ret*ret*x;}//主函数调用public double myPow(double x, int n) {long N=n;return N<0?1.0/pow(x,-N):pow(x,N);}
}

这里求快速幂的版本是递归,还有一种求快速幂的方法是迭代,会在数学专题算法那里再学习

总结:

递归其实不难,只要发现子问题的操作都相同就可以使用,且无条件相信递归函数,其中调用递归函数往往在前面,后面才是操作步骤,然后找到结束条件就能快速解决,不要抠细节流程图,要宏观来看

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

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

相关文章

python leetcode 笔记

只为记录一些python相关的特殊写法 无穷大&#xff0c;无穷小&#xff0c;NAN float(inf), float(-inf), float(nan) 判断字符的类型 isdigit(x) isspace(x) 字符串拼接 /.join([a,b,c]) # a/b/c 格式转换&#xff0c;字符转整形 ord(a) # 97 chr(97) # a 进制转…

如何成为一名 Python 全栈工程师攻略

## 从零基础到全栈工程师&#xff1a;Python 学习路线&#xff08;细化版&#xff09; **目标&#xff1a;** 掌握 Python 编程&#xff0c;并能独立开发全栈应用。 **学习路线&#xff1a;** ### 第一阶段&#xff1a;Python 基础 (4-6 周) **目标&#xff1a;** 掌握 Pyt…

Windows系统中Docker可视化工具对比分析,Docker Desktop,Portainer,Rancher

Docker可视化工具对比分析&#xff0c;Docker Desktop&#xff0c;Portainer&#xff0c;Rancher Windows系统中Docker可视化工具对比分析1. 工具概览2. Docker Desktop官网链接&#xff1a;主要优点&#xff1a;主要缺点&#xff1a;版本更新频率&#xff1a; 3. Portainer官网…

C++中常用的十大排序方法之1——冒泡排序

成长路上不孤单&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a; 【&#x1f60a;///计算机爱好者&#x1f60a;///持续分享所学&#x1f60a;///如有需要欢迎收藏转发///&#x1f60a;】 今日分享关于C中常用的排序方法之——冒泡排序的相关…

远程连接-简化登录

vscode通过ssh连接远程服务器免密登录&#xff08;图文&#xff09;_vscode ssh-CSDN博客

OpenEuler学习笔记(十五):在OpenEuler上搭建Java运行环境

一、在OpenEuler上搭建Java运行环境 在OpenEuler上搭建Java运行环境可以通过以下几种常见方式&#xff0c;下面分别介绍基于包管理器安装OpenJDK和手动安装Oracle JDK的步骤。 使用包管理器安装OpenJDK OpenJDK是Java开发工具包的开源实现&#xff0c;在OpenEuler上可以方便…

[Java]继承

1. 什么是继承&#xff1f; 继承是面向对象编程的一种机制&#xff0c;允许一个类&#xff08;叫做子类&#xff09;继承另一个类&#xff08;叫做父类&#xff09;的属性和方法。也就是说&#xff0c;子类可以“继承”父类的行为&#xff08;方法&#xff09;和状态&#xff…

双指针c++

双指针&#xff08;Two Pointers&#xff09;是一种常用的算法技巧&#xff0c;通常用于解决数组或链表中的问题&#xff0c;如滑动窗口、区间合并、有序数组的两数之和等。双指针的核心思想是通过两个指针的移动来优化时间复杂度&#xff0c;通常可以将 (O(n^2)) 的暴力解法优…

第05章 16 Implicit Function应用举例

Implicit Function在VTK中有多种广泛的应用场合&#xff0c;以下是一些主要的应用场景及其详细说明&#xff1a; 1. 几何裁剪&#xff08;Clipping&#xff09; Implicit Function可以用于对几何体进行裁剪&#xff0c;生成新的几何形状。裁剪操作通常基于一个Implicit Funct…

【二叉搜索树】

二叉搜索树 一、认识二叉搜索树二、二叉搜索树实现2.1插入2.2查找2.3删除 总结 一、认识二叉搜索树 二叉搜索树&#xff08;Binary Search Tree&#xff0c;简称 BST&#xff09;是一种特殊的二叉树&#xff0c;它具有以下特征&#xff1a; 若它的左子树不为空&#xff0c;则…

洛谷P3372 【模板】线段树 1以及分块

【模板】线段树 1 题目描述 如题&#xff0c;已知一个数列&#xff0c;你需要进行下面两种操作&#xff1a; 将某区间每一个数加上 k k k。求出某区间每一个数的和。 输入格式 第一行包含两个整数 n , m n, m n,m&#xff0c;分别表示该数列数字的个数和操作的总个数。 …

Linux运维之Linux的安装和配置

目录 Linux的基本概念&#xff1a; 1.为什么要使用Linux&#xff1f; 2.什么是Linux&#xff1f; Linux的安装和配置&#xff1a; 1.下载Linux的虚拟机和镜像文件&#xff1a; 1.1下载虚拟机 1.2下载镜像文件 2.在虚拟机或者物理机中安装Linux操作系统 3.配置虚拟机的…

Google 和 Meta 携手 FHE 应对隐私挑战

1. 引言 为什么世界上最大的广告商&#xff0c;如谷歌和 Meta 这样的超大规模公司都选择全同态加密 (FHE)。 2. 定向广告 谷歌和 Meta 是搜索引擎和社交网络领域的两大巨头&#xff0c;它们本质上从事的是同一业务——广告。它们最近公布的年度广告收入数据显示&#xff0c;…

【ArcMap零基础训练营】01 ArcMap使用入门及绘图基础

ArcMap入门及使用技巧 230106直播录像 ArcMap使用技巧及制图入门 ArcGIS的安装 本次教学使用的ArcMap版本为10.7&#xff0c;建议各位安装ArcGIS10.0及其以上版本的英文版本。 下载及安装详细教程可参考ArcGIS 10.8 for Desktop 完整安装教程 麻辣GIS 改善使用体验的几个操作…

一个 windows 自动语音识别案列

一个 windows 自动语音识别案列 之前给写过一段很有意思的代码,今天分享给大家 ! 文章目录 一个 windows 自动语音识别案列前言一、需要安装一些python 库二、代码如下三,测试总结下前言 一、需要安装一些python 库 speech_recognition:这是一个用于语音识别的库。它可以…

程序员学英文之At the Airport Customs

Dialogue-1 Making Airline Reservation预定机票 My cousin works for Xiamen Airlines. 我表哥在厦航上班。I’d like to book an air ticket. 我想预定一张机票。Don’t judge a book by its cover. 不要以貌取人。I’d like to book / re-serve a table for 10. 我想预定一…

[250125] DeepSeek 发布开源大模型 R1,性能比肩 OpenAI o1 | 希捷推出高达 36TB 的硬盘

DeepSeek 发布开源大模型 R1&#xff0c;性能比肩 OpenAI o1 DeepSeek 正式发布了 DeepSeek-R1 大模型&#xff0c;并同步开源了模型权重&#xff0c;其性能对标 OpenAI o1 正式版。 &#x1f31f; 主要亮点&#xff1a; 开源模型&#xff0c;MIT 许可证&#xff1a; DeepSe…

Python 写的几个经典游戏 新年放烟花、 贪吃蛇、俄罗斯方块、超级玛丽、五子棋、蜘蛛纸牌

0、新年放烟花 import pygame import random import math# 初始化Pygame pygame.init()# 设置窗口 WIDTH 800 HEIGHT 600 screen pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption("新年放烟花")# 颜色定义 BLACK (0, 0, 0) WHITE (255, 2…

Python Typing: 实战应用指南

文章目录 1. 什么是 Python Typing&#xff1f;2. 实战案例&#xff1a;构建一个用户管理系统2.1 项目描述2.2 代码实现 3. 类型检查工具&#xff1a;MyPy4. 常见的 typing 用法5. 总结 在 Python 中&#xff0c;静态类型检查越来越受到开发者的重视。typing 模块提供了一种方式…

canvas的基本用法

canvas canvas元素简介 1.是个container元素<canvas width100 height100></canvas>&#xff0c;有开闭标签 2.有且只有width和height两个attribute&#xff0c;不需要写单位 canvas的基本使用 const canvasEl document.getElementById(canvas01) const ctx …