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

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

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

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

题目一:

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

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

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

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

思路:

一开始盘子都在起始柱子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,一经查实,立即删除!

相关文章

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

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

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

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

【二叉搜索树】

二叉搜索树 一、认识二叉搜索树二、二叉搜索树实现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.配置虚拟机的…

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

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

程序员学英文之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. 我想预定一…

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 模块提供了一种方式…

14-8C++STL的queue容器

一、queue容器 (1)queue容器的简介 queue为队列容器&#xff0c;“先进先出”的容器 (2)queue对象的构造 queue<T>q; queue<int>que Int;//存放一个int的queue容器 queue<string>queString;//存放一个string的queue容器 (3)queue容器的push()与pop()方…

计算机毕业设计Python+CNN卷积神经网络高考推荐系统 高考分数线预测 高考爬虫 协同过滤推荐算法 Vue.js Django Hadoop 大数据毕设

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…

PyCharm接入DeepSeek实现AI编程

目录 效果演示 创建API key 在PyCharm中下载CodeGPT插件 配置Continue DeepSeek 是一家专注于人工智能技术研发的公司&#xff0c;致力于开发高性能、低成本的 AI 模型。DeepSeek-V3 是 DeepSeek 公司推出的最新一代 AI 模型。其前身是 DeepSeek-V2.5&#xff0c;经过持续的…

C语言指针专题一 -- 指针基础原理

目录 1. 指针概念 地址和变量 指针 2. 指针的声明与初始化 3. 指针的使用 指针访问 指针的运算 指针与数组 指针与函数 4. 编程实例 5. 指针的常见陷阱与防御 6. 总结 1. 指针概念 地址和变量 在C语言中&#xff0c;地址和变量是两个基本但非常重要的概念。 1. 变…

【Python】已解决:ModuleNotFoundError: No module named ‘cv2’

个人简介&#xff1a;某不知名博主&#xff0c;致力于全栈领域的优质博客分享 | 用最优质的内容带来最舒适的阅读体验&#xff01;文末获取免费IT学习资料&#xff01; &#x1f345; 文末获取更多信息 &#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅收藏 &#x…

从0开始,来看看怎么去linux排查Java程序故障

一&#xff0c;前提准备 最基本前提&#xff1a;你需要有liunx环境&#xff0c;如果没有请参考其它文献在自己得到local建立一个虚拟机去进行测试。 有了虚拟机之后&#xff0c;你还需要安装jdk和配置环境变量 1. 安装JDK&#xff08;以OpenJDK 17为例&#xff09; 下载JDK…

设计模式-建造者模式、原型模式

目录 建造者模式 定义 类图 优缺点 角色 建造者模式和工厂模式比较 使用案例 原型模式 定义 类图 优缺点 应用场景 应用类型 浅克隆 深克隆 建造者模式 定义 将一个复杂的对象的构造与它的表示分离&#xff0c;使同样的构建过程可以创建不同的表示&#xff0c;…

1 HDFS

1 HDFS 1. HDFS概述2. HDFS架构3. HDFS的特性4. HDFS 的命令行使用5. hdfs的高级使用命令6. HDFS 的 block 块和副本机制6.1 抽象为block块的好处6.2 块缓存6.3 hdfs的文件权限验证6.4 hdfs的副本因子 7. HDFS 文件写入过程&#xff08;非常重要&#xff09;7.1 网络拓扑概念7.…

75-《倒提壶》

倒提壶 倒提壶&#xff08;学名&#xff1a;Cynoglossum amabile Stapf et Drumm.&#xff09;&#xff1a;紫草科&#xff0c;琉璃草属多年生草本植物&#xff0c;高可达60厘米。茎密生贴伏短柔毛。基生叶&#xff0c;长圆状披针形或披针形&#xff0c;茎生叶长圆形或披针形&a…

第一个3D程序!

运行效果 CPP #include <iostream> #include <fstream> #include <string> #include <cmath>#include <GL/glew.h> #include <GLFW/glfw3.h> #include <glm/glm.hpp> #include <glm/gtc/type_ptr.hpp> #include <glm/gtc/…

简要介绍C语言/C++的三目运算符

三元运算符是C语言和C中的一种简洁的条件运算符&#xff0c;它的形式为&#xff1a; 条件表达式 ? 表达式1 : 表达式2; 三元运算符的含义 条件表达式&#xff1a;这是一个布尔表达式&#xff0c;通常是一个比较操作&#xff08;如 >、<、 等&#xff09;。 表达式1&am…