数据结构杂谈番外篇——搞懂递归的小文章

文章目录

    • 1 难题
    • 2 递归
      • 2.1 n的阶层
      • 2.2 斐波那契数列的第n项
      • 2.3 逆序打印数组
    • 3 反转链表
    • 4 回顾递归

1 难题

如果不想听我谈学习的过程而注重怎么学习,可以直接跳到第二小节

这个递归的问题是在我刷题的时候遇到的。事实上,我对递归是一窍不通的,第一次学递归是在大二上学期学的数据分析和可视化中遇到了,但是那时候老师叫我们背,所以没怎么注意这个问题。

没注意的问题在后面就开始暴露出来了。在Leetcode刷题的时候,第一次遇见递归是在反转单链表的时候。反转单链表一文可以在每日一题——剑指 Offer24反转链表_尘鱼好美的小屋-CSDN博客中查看。在当时,我用的仅仅是新手都能接受的迭代法。而无法接受思维混乱的递归,但是在解决Leetcode上的另外一道题的时候就开始出问题了,这道题必须用到递归或者栈。

我们学过栈的都知道,栈的本质是递归,这就意味着这个知识点是一个跨不过的坎,我知道我必须面对了。

对于解决这个问题,我首先是看了一下大佬的递归解法剑指 Offer 24. 反转链表(迭代 / 递归,清晰图解) - 反转链表 - 力扣(LeetCode) (leetcode-cn.com)。但是我发现其对于递归的本质没有详细的阐述,反而是只提解法,这对于新手显然十分不友好。我又在看不懂递归的看过来,希望能帮到你! - 反转链表 - 力扣(LeetCode) (leetcode-cn.com)上面看到了另外一个大佬的解法,虽然讲的挺好,但是在单链表反转中又是让人无法接受了,但是至此,我突然脑路一开,发现了一种新思路,我十分愿意和你分享我思考的思路,希望你耐心看完我的文章。

2 递归

大多数讲述递归都是先引出斐波那契数列。实际上,我们无需畏惧递归这个名词,我们先用另外一个词来体会递归,即递推公式,这在我们高中数学中几乎人人学过。在讲述斐波那契数列前,我们来解决一个问题。如何解决用递归实现n的阶层计算?

2.1 n的阶层

完成递归实际上就是三部曲:

  • 明确函数目的
  • 寻找递归结束条件
  • 找出函数的等价关系式

这么说好像太空了,我们来给出一个图,实际上这个图就是递归。

image-20220221225838716

没错,我们可以把俄罗斯套娃看成是递归,也就是说,每一层的娃都是在解决问题,递归的过程是把解决的问题都留在最后,先从外到里一步一步取娃,然后在最里面的娃从里到外解决问题。

现在我们看往例子:如果我们要解决阶层问题,那么结束递归的条件就是一个你知道的数,比如你从5的阶层,那么自然1的阶层是你知道的,那你就可以把1作为结束条件。当然,2你也知道是多少,甚至于更高。我们先用1来作为结束条件:

//结束条件
if(n == 1)
{return 1;
}

那我们接下来就是要写函数等价式了,这实际上是一个创造套娃的过程,从最小的娃开始,和相邻的娃建立联系。也就是说,我们只关注第n个娃和n-1个娃之间的关系,在这个例子中,它们的关系就是f(n) = n*f(n-1)。

等价关系式的寻找在这里看起来十分简单,可实际上,递归最难的就是此步。

接下来我们把上述写成代码,如下所示:

int func(int n)
{if(n == 1)return 1;return n*func(n-1);
}

也可以用2为结束递归条件,如下所示:

int func(int n)
{if(n <= 2)return 2; //2的阶层return n*func(n-1);
}

综上所述,这就是一个最简单的递归了。在下面,我们层层递进,来解决一些实际的问题。

2.2 斐波那契数列的第n项

我们来解决这么一个问题:

斐波那契数列的是这样一个数列:1、1、2、3、5、8、13、21、34…,即第一项 f(1) = 1,第二项 f(2) = 1…,第 n 项目为 f(n) = f(n-1) + f(n-2)。求第 n 项的值是多少。

按照上面的套路,函数实际上是要返回一个第n项的值,所以我们可以这么定义:

int func(int n)
{}

接下来我们要寻找递归结束的条件,根据题意,我们知道f(1) = 1,f(2) = 1… 那么根据我们在最开始讲到的,我们实际上是寻求函数关系等价式,在本题中,如果你采用n = 1作为递归结束条件,那么在函数等价式中(本题已给出)有一个f(n) = f(n-1)+f(n-2),你把2填进去,会出现一个f(0),这样的话越过了f(0)越过了递归结束条件n = 1,会无限死循环下去。

这显然是我们不希望的,所以我们可以用n<=2来作为循环结束条件,这样,f(n)中的n只能填3以上的数字才会出现循环。

综上所述,代码如下所示:

int func(int n){if(n <= 2){return 1;}return func(n-1) + func(n-2)
}

2.3 逆序打印数组

上面的斐波那契数列问题实际上很容易看出函数等价关系式,让我们来一个不那么明显地例子。

我们需要逆序打印一个长度为n的数组。请问如何解决?

也就是说,我们要的是打印一个数组?我们可以这么做:

int func(int arrs[])
{}

接下来我们需要寻找递归结束条件,这个结束的条件就是数组为空,即n = 0就结束。

int func(int arrs[])
{if(n == 0)return false;
}

现在让我们来找函数等价关系,在这里,我们明显要倒序打印,所以首要任务是先打印再倒推,倒推的过程实际上是一个类似于指针移动的过程:n = n-1,所以我们可以写出如下代码:

void func(int arrs[], int n)
{if (n <= 0)return;cout << arrs[n - 1] << endl;return func(arrs, n - 1);
}

3 反转链表

回到我们的主题,我们要解决的最终问题是,如何解决反转链表,乃至解决更多问题,既然要使用递归这个工具乘风破浪,那就先拿这道破题开刀吧。

定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。

示例:

输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL

限制:

0 <= 节点个数 <= 5000

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/fan-zhuan-lian-biao-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

采用递归,首先要明白函数要干嘛,函数要反转链表并且返回头结点。

//逆置链表函数
ListNode* reverseList(ListNode * head)
{}

但是实际上,我们要实现递归的地方不是在这个逆置函数内,所以我们可以另外写一个递归函数。我们的思路是,分别指定两根指针,一根为pre,一根为cur,反转链表后cur.next = pre。最开始pre一定是空,cur一定是处于head的位置。

//逆置链表函数
ListNode* reverseList(ListNode * head)
{}//递归函数
ListNode* recur(ListNode* cur, ListNode* pre)
{}

接下来找结束条件。最开始pre是空,cur处于head的位置。当递归执行时,最内层的循环是cur快跑到null了。所以结束递归的条件一定是cur->next = NULL。image-20220221235557584

在最内层循环中,我们做的是:改变指针指向,即cur.next = pre。并且在最内层循环是,cur所处位置恰好是逆置后链表头结点所处位置。当递归函数执行完成,返回逆置链表头结点位置。而在逆置链表函数中,仅仅需要调用递归函数并且返回逆置链表头结点位置即可。

class Solution {
public:ListNode* reverseList(ListNode* head) {return recur(head, nullptr);           // 调用递归并返回}
private:ListNode* recur(ListNode* cur, ListNode* pre) {if (cur == nullptr) return pre;        // 终止条件ListNode* res = recur(cur->next, cur); // 递归后继节点cur->next = pre;                       // 修改节点引用指向return res;                            // 返回反转链表的头节点}
};

从套娃的角度来看,我们可以这样做:

image-20220222001018699

4 回顾递归

递归实际上是一个解决子问题的过程。我们要解决f(n),实际上首先解决f(n-1),要解决f(n-1),实际上要先解决f(n-2),以此类推直至先解决最根本的问题,再回溯整个过程。

递归实际上也是需要优化的,比如f(n) = f(n-1)+f(n-2),如果n = 5,那么n-1 = 4,n-2 = 3,后续在递归的过程中会出现多次f(4)、f(3)等,如果每次都计算,开销挺大,一般可以用某个值来保存,但是这篇文章是针对像我一样的初学者的,我们就偷个懒,放自己一马吧。

好了,彦祖,别太累了,好好消化一下就休息吧。

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

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

相关文章

副本机制

1.首先说一下函数的副本机制 看一段简单的代码 1 # include<stdio.h>2 # include<stdlib.h>3 4 int add(int n, int m)5 {6 int z n m;7 return z;8 }9 10 int main() 11 { 12 printf("%d\n", add(1, 2)); 13 } View Code当函数返回后&am…

Dubbo亮点总结

Dubbo是阿里巴巴的一个开源RPC项目&#xff0c;可在http://dubbo.io进行訪问 类似的产品有Hessian、spring httpinvoke 等。 Dubbo的亮点总结例如以下&#xff1a; 1、服务注冊中心 相比Hessian类RPC框架&#xff0c;Dubbo有自己的服务中心。 写好的服务能够注冊到服务中心。…

cmake 教程

https://www.jianshu.com/p/bbf68f9ddffa 转载于:https://www.cnblogs.com/icmzn/p/8399250.html

王道操作系统考研笔记——2.1.7 进程调度的时机、切换与过程、方式

文章目录2.1.7 进程调度的时机、切换与过程、方式2.1.7.1 进程调度的时机2.1.7.2 进程调度的方式2.1.7.3 进程的切换和过程2.1.7.4 小结2.1.7 进程调度的时机、切换与过程、方式 知识总览 2.1.7.1 进程调度的时机 进程调度&#xff08;低级调度&#xff09;&#xff0c;就是按…

AS(Autonomous System)

在互联网中&#xff0c;一个自治系统&#xff08;英文&#xff1a;Autonomous system, AS&#xff09;是指在一个&#xff08;有时是多个&#xff09;实体管辖下的所有IP网络和路由器的 全体&#xff0c;它们对互联网执行共同的路由策略。 自治系统&#xff08;Autonomous Syst…

HTTP和HTTPS的区别(转)

原文链接&#xff1a;HTTP和HTTPS的区别 HTTPS&#xff08;Secure Hypertext Transfer Protocol&#xff09;安全超文本传输协议 它是一个安全通信通道&#xff0c;它基于HTTP开发&#xff0c;用于在客户计算机和服务器之间交换信息。它使用安全套接字层(SSL)进行信息交换&…

记录一次nginx配置vhost的小bug

话说这篇博客是在是为了保持自己记录生活的习惯而写的&#xff0c;没有什么阅读的价值&#xff0c;各位读者可以直接忽略了。今天在配置一个域名的时候&#xff0c;写了new_example.com&#xff08;举例而已&#xff09; 因为是内测&#xff0c;所以并未想象到深层次的问题&…

王道操作系统考研笔记——2.1.8 调度算法的评价指标

文章目录2.1.8 调度算法的评价指标2.1.8.1 CPU利用率2.1.8.2 系统吞吐量2.1.8.3 周转时间2.1.8.4 等待时间2.1.8.5 响应时间2.1.8.6 小结2.1.8 调度算法的评价指标 知识总览 需要注意的是&#xff0c;这一部分的知识要学会计算 2.1.8.1 CPU利用率 在早期&#xff0c;由于CPU造…

jenkins自动化构建iOS应用配置过程中遇到的问题

最近配置jenkins来自动构建iOS应用&#xff0c;期间遇上不少问题。在这里分享给大家,也给自己留个底&#xff0c;方便下次解决问题。 首先说明下基本情况&#xff0c;我们因为部署jenkins的机器不是Mac,所以不能安装Xcode插件&#xff0c;我不知道这个插件能给我们带来多少便捷…

用户体验设计的五个原则(转)

【编者按】本文作者&#xff0c;现Aura Marker Studio创始人路意Louis&#xff0c;曾带过五年多的用户体验设计团队。这篇文章是他带团队这几年沉淀下来的一些经验和思考。 “设计&#xff0c;必须是充满诗意的。” 带过五年多的用户体验设计团队&#xff0c;从几个人到二十几个…

js遍历 for-of

for-of遍历 entries() 返回一个遍历器对象&#xff0c;用来遍历[键名, 键值]组成的数组。对于数组&#xff0c;键名就是索引值&#xff1b;对于 Set&#xff0c;键名与键值相同。Map 结构的 Iterator 接口&#xff0c;默认就是调用entries方法。keys() 返回一个遍历器对象&…

王道操作系统考研笔记——2.1.9 调度算法

2.1.9 调度算法 知识总览 学习各种调度算法的思路 算法思想算法规则这种调度算法是用于作业调度还是进程调度&#xff1f;抢占式或是非抢占式优点和缺点是否会导致饥饿&#xff08;某进程/作业长期得不到服务&#xff09; 2.1.9.1 先来先服务 知识点说明英文名FCFS&#xff0…

程序员的春天来了,赏花去!说走就走

2019独角兽企业重金招聘Python工程师标准>>> 经历了寒冷的隆冬&#xff0c;阳春三月终于到来&#xff0c;在温暖如棉的风里&#xff0c;赶紧去户外走走吧&#xff0c;享受大自然赐予我们的无限美……春意盎然&#xff0c;正是赏花好时节&#xff0c;每天久坐电脑前的…

关于个人防火墙的真相

原文作者&#xff1a;MaD 原文标题&#xff1a;The truth aboutpersonal firewalls电子邮件&#xff1a;mad-factormail.ru作者国籍&#xff1a;俄罗斯声明&#xff1a;1、本人翻译水平有限&#xff0c;有不当之请大家理解。如部分看不懂可以和原文对照。 2、欢迎转…

NOI2015 程序自动分析

Luogu 写个并查集来维护就行了。先合并所有相等的变量&#xff0c;如果有两个不相等的变量相等&#xff0c;那么就输出NO。注意得先合并所有相等的变量&#xff0c;再来判断。因为如果两个操作一起搞的话&#xff0c;可能会有两个变量在某次查询的时候不相等&#xff0c;但后面…

王道操作系统考研笔记——2.2.0 交互式系统调度算法

2.2.0 交互式系统调度算法 知识总览 2.2.0.1 时间片轮转 知识点说明英文名RR&#xff0c;Round-Robin算法思想公平地、轮流地为各个进程服务&#xff0c;让每个进程在一定时间间隔内都可以得到响应算法规则按照各进程到达就绪队列的顺序&#xff0c;轮流让各个进程执行一个时…

Linux(Ubuntu 16) 下Java开发环境的配置(二)------Tomcat的配置及常见问题

前言 相比于java JDK的配置,Tomcat的配置简单的多,简直就相当于直接运行了,本文以Tomcat8.0为例进行配置1.Tomcat的下载 地址:https://tomcat.apache.org/download-80.cgi下载上图标示文件即可.然后解压到一个方便的文件夹即可.2.Tomcat的启动 如果你的Tomcat的JDK配置好的话,理…

王道操作系统考研笔记——2.3.1 进程同步和进程互斥

文章目录2.3.1 进程同步和进程互斥2.3.1.1 什么是进程同步&#xff1f;2.3.1.2 什么是进程互斥2.3.1.3 小结2.3.1 进程同步和进程互斥 知识总览 2.3.1.1 什么是进程同步&#xff1f; 在前面&#xff0c;我们说过进程具有异步性的特征。 而我们想让进程执行的顺序是可预知的&…

王道操作系统考研笔记——2.3.2 进程互斥的软件实现方法

文章目录2.3.2 进程互斥的软件实现方法2.3.2.1 单标志法2.3.2.2 双标志先检查法2.3.2.3 双标志后检查法2.3.2.4 Peterson算法2.3.2.5 小结2.3.2 进程互斥的软件实现方法 知识总览 2.3.2.1 单标志法 单标志法的算法思想是&#xff1a;两个进程在访问完临界区后会把使用临界区的…

js处理上下文代码的2个阶段

1、进入执行上下文 当进入执行上下文(代码执行之前)时&#xff0c;VO里已经包含了下列属性(前面已经说了)&#xff1a; 函数的所有形参(如果我们是在函数执行上下文中) — 由名称和对应值组成的一个变量对象的属性被创建&#xff1b;没有传递对应参数的话&#xff0c;那么由名称…