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

news/2024/11/30 0:33:44/

一、前言

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

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

二、递归初步理解

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

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==1elsereturn 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.ppmy.cn/news/95921.html

相关文章

【python】pytorch包(第四章)手写数字图像识别

问题描述&#xff1a; 给定手写字体的图片&#xff0c;人工智能自动判断这是数字几 数据来源&#xff1a; MNIST数据集 代码实战&#xff1a; Part 1. 准备数据集 该模块内容完成的功能&#xff1a; 下载MNIST数据集&#xff1b;转换数据格式&#xff0c;使适用于pytorch&…

算法6.堆结构、堆排序、加强堆

算法|6.堆结构、堆排序、加强堆 1.比较器的定义 题意&#xff1a;定义一个学生类&#xff0c;分别实现对学生对象数组按年龄升序、按id降序、按名字的字典序、按id排序且id相同的年龄大的排在前边。 解题思路&#xff1a; 定义一个学生类定义一个实现了Comparator接口的类A…

C++ 学习 ::【基础篇:06】:C++ (inline)内联函数的介绍及其出现的意义【对比于 C语言宏函数】

本系列 C 相关文章 仅为笔者学习笔记记录&#xff0c;用自己的理解记录学习&#xff01;C 学习系列将分为三个阶段&#xff1a;基础篇、STL 篇、高阶数据结构与算法篇&#xff0c;相关重点内容如下&#xff1a; 基础篇&#xff1a;类与对象&#xff08;涉及C的三大特性等&#…

Java中的String数据类型,String类(字符串)详解

目录 第一章、String概述1&#xff09;String是什么2&#xff09;String长什么样3&#xff09;String的构造方法(声明方式) 第二章、String类的详解1&#xff09;String底层是什么2&#xff09;字符串存储的内存原理/字符串常量池(String Constant Pool&#xff09;3&#xff0…

1. 从JDK源码级别彻底刨析JVM类加载机制

JVM性能调优 1. 类加载的运行全过程1.1 加载1.2 验证1.3 准备1.4 解析 本文是按照自己的理解进行笔记总结&#xff0c;如有不正确的地方&#xff0c;还望大佬多多指点纠正&#xff0c;勿喷。 课程内容: 1、从java.exe开始讲透Java类加载运行全过程 2、从JDK源码级别剖析JVM核…

科技的力量:致敬全国科技工作者

在这个日新月异的时代&#xff0c;科技的力量正在改变着我们的生活。为了庆祝5月30日的全国科技者工作日&#xff0c;我们特地上线本次创作活动&#xff0c;向所有为科技进步付出辛勤努力的科技工作者们致敬。在这篇博文中&#xff0c;我们将通过讲述科技发展的故事、分享技术成…

洛谷01背包变形(P1858多人背包)

多人背包 文章目录 一、问题简述二、问题分析三、代码参考 一、问题简述 DD 和好朋友们要去爬山啦&#xff01; 他们一共有 K 个人&#xff0c;每个人都会背一个包。这些包 的容量是相同的&#xff0c;都是 V。可以装进背包里的一共有 N 种物品&#xff0c;每种物品都有 给定…

【地铁上的面试题】--基础部分--数据结构与算法--数组和链表

零、章节简介 《数据结构与算法》是《地铁上的面试题》专栏的第一章&#xff0c;重点介绍了技术面试中不可或缺的数据结构和算法知识。数据结构是组织和存储数据的方式&#xff0c;而算法是解决问题的步骤和规则。 这一章的内容涵盖了常见的数据结构和算法&#xff0c;包括数组…