递归之谜:解析无限嵌套的美

news2024/11/27 16:40:28

一、前言

嵌套是指在一个事物中包含另一个事物,而递归是一种特殊形式的嵌套,其中一个事物包含自身。

递归就是一种嵌套的形式,递归函数解决问题时嵌套调用自身。递归的核心思想是通过反复应用相同的过程来解决问题,每一次调用都在规模上比上一次调用更小,直到达到基本情况从而终止递归。
递归是一种强大而优雅的技术,它能够将复杂的问题分解成更小的子问题来解决,递归和数学归纳法有密切的关系,递归可以被看作是数学归纳法在编程中的一种具体应用。
在这里插入图片描述

二、递归初步理解

递归 = 递推 + 回归
递归是指函数直接或间接地调用自身的过程。递归函数通常在满足基本条件时停止调用自身,从而避免无限循环。递归的思想是将一个大问题分解成一个或多个相同的较小问题,直到问题简化到可以直接解决为止。
在这里插入图片描述

1、数学归纳法

数学归纳法的核心思想是:如果一个性质对于某个数成立,而且可以证明如果对于任何数n该性质成立就能推导出对于n+1也成立,那么这个性质就对所有正整数成立。

数学归纳法主要包括两个步骤:

  1. 基础步骤:首先要证明该性质对于初值(通常是1或0)成立。
  2. 归纳步骤:然后要假设性质对于某个数n成立(这被称为归纳假设),并证明这个假设就能推导出该性质对于n+1也成立。

我们可以看如下一个数学归纳法的例子,
用数学归纳法证明: ∑ i = 1 n i = n ∗ ( n + 1 ) 2 。 用数学归纳法证明:\sum_{i=1}^{n} i = \frac{n*(n + 1)}{2}。 用数学归纳法证明:i=1ni=2n(n+1)
用数学归纳法来证明:

  • 先证明对于N=1成立。
  • 再证明N>1时:假设对于N-1成立,那么对于N成立。

2、递归的设计

将上面这个数学归纳法的问题,设计为递归解决,应该怎么解决呢?

int sum(int n){
    if(n==1)
        return 1;	//边界条件:n==1
    else
        return sum(n-1)+n;	//利用sum(n-1)的值,计算sum(n)的值
}

在使用代码解决上面这个问题的时候,我们首先是实现了边界条件,这个对于的不就是数学归纳法中证明N=1的时候成立嘛?

其次, return sum(n-1)+n;这段代码的底层逻辑是假设该递归函数调用的返回值是正确的,而这个假设,就是对应了数学归纳法中的归纳步骤。

在数学归纳法中,我们证明了性质对于一个初始值(通常是1或0)成立。在递归中,我们则定义了一个或多个基本情况,这些情况可以直接解决,无需递归。数学归纳法的归纳步骤和递归的递归步骤都是解决过程的主体部分。在数学归纳法中,我们假设性质对于某个值n成立,然后证明该性质对于n+1也成立。在递归中,假设我们能够解决比当前问题更小的问题,然后通过这些更小的问题来解决当前的问题。

我们可以简单的总结一下递归函数的设计方法:

  1. 确定递归函数的参数和返回值:首先,你需要明确递归函数的输入(参数)和输出(返回值)是什么。
  2. 确定边界情况:边界情况是递归函数的终止条件。你需要明确当参数满足什么条件时,函数可以直接返回结果,而无需进一步递归。
  3. 确定递归情况:递归情况是函数如何调用自身的部分。在这一步,你需要考虑如何将大问题分解成小问题,然后用递归的方式解决这些小问题。

3、递归的练习

Ⅰ、路飞吃桃

题目链接:路飞吃桃

当我们使用递归解决这个问题时,我们可以从第n天开始反向计算。

  1. 边界情况:因为题目中已经说明了到第n天时只剩下一个桃子,所以在n=1时,桃子的数量为1。
  2. 递归情况:对于第n天,桃子的数量可以根据第n+1天的数量计算。在第n天,桃子的数量是(第n+1天的桃子数量 + 1) * 2。
#include<stdio.h>
int f (int n )  // 确定递归函数的参数和返回值
{
	if(n==1)	//确定边界情况
		return 1; 
	else		//确定递归情况
		return (f(n-1)+1)*2;
}
int main ()
{
	int n = 0;
	scanf("%d",&n);
	printf("%d",f(n));
	return 0;
}

Ⅱ、弹簧版

题目链接:弹簧板

  1. 确定递归函数的参数和返回值:递归函数需要接受弹簧板的跳跃能力列表、当前弹簧板的索引和弹簧板的总数作为参数,并返回小球弹跳出去的总次数。
  2. 确定基本情况:我们首先需要确定基本情况,即递归函数的终止条件。在这个问题中,如果当前弹簧板超出了弹簧板范围(即当前弹簧板的索引大于等于弹簧板的总数),那么小球无法再继续弹跳,返回0次弹跳。
  3. 确定递归情况:在递归情况中,我们将计算从当前弹簧板弹跳出去的总次数。根据题目的描述,我们可以通过当前弹簧板的索引和弹簧板的跳跃能力列表来计算下一次弹跳的弹簧板索引。然后,递归调用函数,并返回1加上从下一个弹簧板开始弹跳出去的总次数。
#include <stdio.h>
int countBounces(int springs[], int currentSpring, int numSprings) {
    // 边界情况:如果当前弹簧板超出范围,返回0次弹跳
    if (currentSpring >= numSprings) {
        return 0;
    }
    // 递归情况:计算从当前弹簧板弹跳出去的次数
    int nextSpring = currentSpring + springs[currentSpring];
    return 1 + countBounces(springs, nextSpring, numSprings);
}
int main() {
    int numSprings;
    scanf("%d", &numSprings);
    int springs[numSprings];
    for (int i = 0; i < numSprings; i++) {
        scanf("%d", &springs[i]);
    }   
    int startSpring = 0;  // 从1号弹簧板开始
    int totalBounces = countBounces(springs, startSpring, numSprings);
    printf("%d\n", totalBounces);
    return 0;
}

Ⅲ、反转链表

题目链接:反转链表

【常规解法】

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
    ListNode new_head,*p = head,*q;
	new_head.next = NULL;
	while(p){
			q = p->next;
			p->next = new_head.next;
            new_head.next = p;
			p=q;
	}
	return new_head.next;
    }
};

【递归解法】

边界情况:如果 head == NULL 或者 head->next == NULL,这表示链表为空或者链表只有一个节点,无需反转,直接返回 head

递归步骤:如果链表有多个节点,那么首先通过调用 reverseList(head->next) 进行递归,这个调用会返回反转后的链表头节点(记作 ret)。需要注意的是,此时 head->next 还是指向原链表的下一个节点,这个节点在反转后的链表中是最后一个节点。然后执行 head->next->next = head,这会让 head 的下一个节点的 next 指针指向 head,即实现了局部的反转。最后执行 head->next = NULL,断开原 headhead->next 之间的链接,这是因为 head 在反转后的链表中应当是最后一个节点,所以 head->next 应当指向 NULL。最后返回 ret,即反转后的链表的头节点。

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        if (head == NULL || head->next == NULL) {
            return head;
        }
        ListNode* ret = reverseList(head->next);
        head->next->next = head;
        head->next = NULL;
        return ret;
    }
};

三、递归的深层理解

1、栈帧结构

递归允许一个函数直接或间接地调用自己,这种调用方式表现为一种层次结构:每次递归调用都会创建一个新的函数实例,这个新的函数实例处理问题的一部分,并可能进一步调用自身。从底层来看,当一个函数调用自身时,操作系统会为这次函数调用分配一段内存,这段内存称为栈帧。

帧是指为一个函数调用单独分配的那部分栈空间。

比如,当运行中的程序调用另一个函数时,就要进入一个新的栈帧,原来函数的栈帧称为调用者的帧,新的栈帧称为当前帧。被调用的函数运行结束后当前帧全部收缩,回到调用者的帧。

栈帧的创建和销毁是通过栈指针来实现的。栈指针指向当前栈帧的顶部,当一个新的函数调用发生时,栈指针会向下移动,为新的栈帧腾出空间。而当函数执行完毕后,栈指针会向上移动,销毁当前的栈帧。栈帧结构的使用使得函数调用和返回过程可以按照嵌套的方式进行,每个函数都有自己的独立空间来保存参数、局部变量等信息,从而实现了程序的模块化和递归调用。

%ebp是帧指针,它总是指向当前帧的底部;

%esp是栈指针,它总是指向当前帧的顶部。

img

2、递归与栈

递归的底层机制是栈。

这是因为栈在计算机中用于管理函数调用和返回的过程。当一个函数被调用时,会创建一个新的栈帧,栈帧用于存储函数的局部变量、参数和其他相关信息。这个新的栈帧用于存储递归调用的函数的局部变量和参数。每个递归调用都会在调用栈中创建一个新的栈帧,形成一种嵌套的结构。这样,调用栈就能够记录递归调用的顺序和相关的上下文信息。递归的终止条件决定了递归的结束点。

image-20230527213624249

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

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

相关文章

容器化:MongoDB

1 缘起 开启容器化之路。 2 容器化MongDB 2.1 查看镜像 docker search mongodb2.2 安装 前台安装 sudo docker run \ --name mongodb \ -p 27017:27017 \ -v /home/xindaqi/mongodb/conf:/data/configdb \ -v /home/xindaqi/data/mongodb-data:/data/db \ -v /home/xind…

99年表示真干不过,部门新来的00后测试员已把我卷崩溃,想离职了...

在程序员职场上&#xff0c;什么样的人最让人反感呢? 是技术不好的人吗?并不是。技术不好的同事&#xff0c;我们可以帮他。 是技术太强的人吗?也不是。技术很强的同事&#xff0c;可遇不可求&#xff0c;向他学习还来不及呢。 真正让人反感的&#xff0c;是技术平平&#x…

常见的GPIO口框架分析

目录 1、单片机平台 2、嵌入式 Linux 平台 GPIO 八种工作模式详解 接着上一篇的讲&#xff0c;我们上一篇研究了 GPIO 的硬件结构&#xff0c;其来源于 STM32 官方手册&#xff0c;研究了 GPIO 的八种工作模式和推挽输出及开漏输出原理&#xff0c;接下来我们研究 GPIO 的软件…

孙燕姿谈“AI孙燕姿”:她的反应让人意外,深入体验揭示其背后的真相与潜力!

目录 前言AI歌手简介AI歌手的技术原理孙燕姿对“AI孙燕姿”的看法结论个人感受一、你听过AI歌手的音乐呈现吗&#xff1f;作为听众你的感受如何&#xff1f;二、你认为这种新型演艺模式能否获得广泛的市场认可&#xff1f;原因是什么&#xff1f;三、你认为AI歌手会取代流行歌手…

SQL查询语言(3) 嵌套查询

如果不进行去重可能会出现一个情况 嵌套查询根据子查询的结果是否依赖于外层循环,分成相关子查询和不相关子查询 分类 IN 笔者总结&#xff1a;一般这种方法适用于查找有共性的元组&#xff0c;同一类事物比如查找和elsa选修相同科目的学生/选修相同科目的女同学。在后面我…

【随手查】数据手册研读笔记

一个付费课程的学习之旅&#xff0c;将课程中所学到的东西以及实践中学到的悟到的记录下来&#xff0c;方便日后查看&#xff0c;持续更。。。 笔记目录 一、电阻1、贴片电阻表面的阻值标记2、额定功率下降曲线3、贴片电阻的温度系数 二、电容1、电容值的计算公式2、ESR曲线3、…

JVM Sandbox入门详解

一. 概述 在日常开发中&#xff0c;经常会接触到面向AOP编程的思想&#xff0c;我们通常会使用Spring AOP来做统一的权限认证、异常捕获返回、日志记录等工作。之所以使用Spring AOP来实现上述功能&#xff0c;是因为这些场景本质上来说都是与业务场景挂钩的&#xff0c;但是具…

http请求和响应(包含状态码)+过滤器

目录 一、http协议概述 二、http请求 三、http响应 四、过滤器 一、http协议概述 1.http&#xff1a;超文本传输协议&#xff0c;是用于在网络上传输数据的应用层协议。是互联网上应用最为流行的一种网络协议,用于定义客户端浏览器和服务器之间交换数据的过程&#xff0c;基…

软考A计划-试题模拟含答案解析-卷二

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例 &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff0c;以及各种资源分享&am…

当我按下开关,震惊了一众答辩老师,乍一看,啊,就这?

基于机器视觉的爬行机器人&#xff08;毕业设计&#xff09; 零、实现功能一、关于本想法二、理论分析三、3D结构设计四、硬件设计主控板驱动板 五、软件设计控制程序机器视觉APP设计 六、结束语 零、实现功能 实现了爬行机器人的移动控制功能。采用三角步态控制机器人移动&am…

【技术分享】万字长文图文并茂读懂高性能无锁 “B-Tree 改”:Bw-Tree

【技术分享】万字长文图文并茂读懂高性能无锁 “B-Tree 改”&#xff1a;Bw-Tree 原文链接&#xff1a; https://mp.weixin.qq.com/s/I5TphQP__tHn6JoPcP–_w 参考文献可能需要科学上网才能下载。如果你获取不到这几篇论文&#xff0c;可以关注公众号 IT技术小密圈 回复 bw-tre…

类和对象初阶

目录 一、再谈构造函数 1.1 构造函数体赋值 1.2 初始化列表 1.3 注意 1.4 总结 二、拷贝对象时的一些编译器优化 三、static成员 3.1 静态成员变量 3.1.1 引入 3.1.2 特点 3.1.3 区别 3.2 静态成员函数 3.2.1 引入 3.2.2 特点 3.2.3 例题 四、友元 4.1 友元函…

数据结构与算法·第2章【线性表】

线性结构具有以下基本特征&#xff1a; 有唯一的一个被称为首元素&#xff08;或头元素&#xff09;的元素&#xff0c;没有直接前驱&#xff1b;有唯一的一个被称为尾元素&#xff08;或尾节点&#xff09;的元素&#xff0c;没有直接后继。 数据元素之间存在一对一的线性关…

python 实现单链表

链表 链表是一种在存储单元上非连续、非顺序的存储结构。数据元素的逻辑顺序是通过链表中的指针链接次序实现。 链表是由一系列的结点组成&#xff0c;结点可以在运行时动态生成。每个结点包含两部分&#xff1a;数据域与指针域。数据域存储数据元素&#xff0c;指针域存储下一…

Yapi内网部署[CentOS7]

mongo安装 # 下载MongoDB https://www.mongodb.com/try/download/community4.2.24 RedHat/CentOS7.0 tgz# 安装MongoDB mkdir -p /home/jpge/devp-tools/tools cd /home/jpge/devp-tools/tools wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-rhel70-4.2.24.tgz…

线性代数:线性方程求解、矩阵的逆、线性组合、线性独立

本文参考www.deeplearningbook.org一书第二章2.3 Identity and Inverse Matrices 2.4 Linear Dependence and Span 本文围绕线性方程求解依次介绍矩阵的逆、线性组合、线性独立等线性代数的基础知识点。 一、线性方程 本文主要围绕求解线性方程展开&#xff0c;我们先把线性…

揭秘Redis持久化原理,探索fork与Copy-on-Write的魔法!

大家好&#xff0c;我是小米&#xff0c;今天我将和大家一起探索Redis持久化原理中的两个关键概念&#xff1a;fork和Copy-on-Write。这两个概念对于理解Redis的数据持久化机制至关重要。让我们一起来揭开这些技术的神秘面纱吧&#xff01; Redis持久化简介 在开始之前&#…

组合数学第四讲

Generating Function&#xff08;生成函数&#xff09; 这里是一个普通生成函数例子&#xff0c;生成函数一般适用于根据递推关系式求出比较复杂的通项公式的 关键点&#xff1a; 1.项可转换成G(x)-,因为生成函数规定是从0到∞的 2.,当|x|<1时&#xff0c;最终可收敛为。这里…

基础算法(六):回溯算法

前言 Hello大家好&#xff0c;停了半个多月算法学习的荔枝又变菜了&#xff0c;最近决定认认真真地重新学习回溯&#xff0c;无意间看到Carl哥的代码随想录&#xff0c;感动之余也是跟着一步步走&#xff0c;后悔上车晚了呜呜呜~~~。之前自己摸索确实有点难受&#xff0c;在这篇…