递归和尾递归的举例详细深度剖析和区别,栈帧的概念

我们先来了解下栈帧的概念,帮助更好的理解递归和尾递归的区别。

一、栈帧的概念

栈帧(Stack Frame),也被称为活动记录(Activation Record),是程序执行过程中,每次函数调用时创建的一个数据结构。栈帧用于存储函数调用期间所需的所有信息,包括但不限于:

  1. 局部变量:函数内部定义的变量。
  2. 参数:传递给函数的参数。
  3. 返回地址:函数完成执行后,程序应继续执行的代码位置。
  4. 临时变量:编译器为了优化或实现语言特性而产生的临时变量。
  5. 保存的寄存器值:某些情况下,需要保存当前CPU寄存器的状态,以便在函数返回时恢复。

每当一个函数被调用时,一个新的栈帧就会被压入调用栈(Call Stack)。调用栈是一个后进先出(LIFO, Last In First Out)的数据结构,意味着最近被调用的函数会最先从栈中弹出。

当函数执行完毕并返回时,对应的栈帧会被从调用栈中移除,同时控制权和相关状态会恢复到调用该函数的地方。

二、递归和尾递归的详细深度剖析

递归和尾递归都是调用函数本身来进行计算。

1、递归

递归是层层往下深入到最底层,再由最底层逐级往上返回值。

例如:求n的阶乘问题。

#include <stdio.h>
//核心代码
unsigned long factorial(unsigned int n) {
    if (n == 0) {
        return 1;
    } else {
        return n * factorial(n - 1);
    }
}

int main() {
    unsigned int number = 5;
    printf("Factorial of %u is %lu\n", number, factorial(number));
    return 0;
}

求5!的递归过程

5 * factorial ( 4 )
4 * factorial ( 3 )
3 * factorial ( 2 )
2 * factorial ( 1 )

我们知道,一个函数的完整结束,需要执行完成最后一条语句。

可以看到,要求5的阶乘,整个factorial函数的返回值是5 * factorial ( 4 )的值,也就是说只有求出5 * factorial ( 4 )的值整个程序才算结束。

而要求出5 * factorial ( 4 )的值,我们需要先知道factorial ( 4 )的值。于是,程序就会先调用factorial函数递归去求出factorial ( 4 )的值,这时,我们要知道5 * factorial ( 4 )还没有被完全执行完成,那么,我们就需要记录下当前执行到5 * factorial ( 4 )这个位置,以便后续完成factorial ( 4 )函数的调用,求出factorial ( 4 )的值时,程序可以重新从这个位置继续执行5 * factorial ( 4 )的值求得最终的结果并返回,结束整个函数。

栈帧就能存储函数完成执行后,程序应继续执行的代码位置。

而根据上面求5!的递归过程,可以知道,我们需要记录4个程序当前执行到的位置,也就是有4个栈帧要被压入调用栈。(因为factorial ( 4 ) = 4 * factorial ( 3 ),需要知道factorial ( 3 )的值,先用一个新的栈帧记录下当前执行的位置,再去调用factorial ( 3 ),后面以此向下类推

直到最后调用factorial ( 1 ),return 1,factorial ( 1 )的函数调用完整结束。于是,回到factorial ( 1 )对应的栈帧记录的位置继续执行程序,同时把factorial ( 1 )对应的栈帧从调用栈中移除,以此往上类推回到5 * factorial ( 4 ),完整结束5的阶乘的计算。

通过整个递归过程,我们可以知道,每一次递归都有一个新的栈帧被压入调用栈。可想而知,当递归深度较大时,可能会导致栈溢出错误。

2、尾递归

尾递归是一种特殊的递归形式。尾递归是层层往下深入到最底层,最底层的结果即为最终结果。不再需要由最底层逐级往上返回值。

而尾递归与递归最大的区别在于:只会在首次调用尾递归函数时创建一个新的栈帧。

当函数再调用自己时,不会创建新的栈帧。相反,它会更新当前栈帧中的参数和其他相关信息,然后跳转到函数的开头继续执行。

为什么呢?我们还是用求n的阶乘问题来举例。

#include <stdio.h>
//核心代码
unsigned long factorial_tail_recursive(unsigned int n, unsigned long accumulator) {
    if (n == 0) {
        return accumulator;
    } else {
        return factorial_tail_recursive(n - 1, n * accumulator);
    }
}

unsigned long factorial(unsigned int n) {
    return factorial_tail_recursive(n, 1);// 初始调用时,累加器设为1
}

int main() {
    unsigned int number = 5;
    printf("Factorial of %u is %lu\n", number, factorial(number));
    return 0;
}

求5!的递归过程

factorial_tail_recursive( 4 , 5 * 1)
factorial_tail_recursive( 3 , 4 * 5)
factorial_tail_recursive( 2 , 3 * 20)
factorial_tail_recursive( 1 , 2 * 60)

根据下面这段代码,

return factorial_tail_recursive(n - 1, n * accumulator);

我们可以知道,递归调用是函数执行的最后一个操作,并且其返回值直接作为整个函数的返回值。

也就是说当我们调用factorial_tail_recursive( 5 )时,首次调用factorial_tail_recursive函数,创建了一个新的栈帧,然后进入到factorial_tail_recursive函数中执行,直到执行到调用factorial_tail_recursive( 4 , 5 * 1),直接return,factorial_tail_recursive( 4 , 5 * 1)的结果即为最终结果(也就是最深层factorial_tail_recursive( 1 , 2 * 60)递归调用的结果),这时factorial_tail_recursive( 5 ,  1)整个函数调用完整执行完成。不像递归那样,每层递归还需要再乘以n(尾递归是把乘以n这一步作为递归参数直接传给下一次递归调用),再把值往上返回。也正因为不需要往上返回值,我们才不需要保存当前的栈帧来记录程序执行到的位置

于是执行调用的factorial_tail_recursive( 4 , 5 * 1)、factorial_tail_recursive( 3 , 4 * 5)、factorial_tail_recursive( 2 , 3 * 20)、factorial_tail_recursive( 1 , 2 * 60)时,我们只需要重用首次创建的栈帧并更新栈帧的信息即可,不需要再创建新的栈帧。

避免了随着递归深度增加而不断增长的调用栈,从而节省了内存空间,并允许处理更深的递归层级而不导致栈溢出。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Python 3 与 Python 2 的主要区别

文章目录 1. 语法与关键字print 函数整数除法 2. 字符串处理默认字符串类型字符串格式化 3. 输入函数4. 迭代器和生成器range 函数map, filter, zip 5. 标准库变化urllib 模块configparser 模块 6. 异常处理7. 移除的功能8. 其他重要改进数据库操作多线程与并发类型注解 9. 总结…

word中编号统一格式

不要手敲编号&#xff0c;要利用工具来。要善于利用多级编号和编号&#xff0c;分别对标题和段落进行组织 尤其是段落和标题特别多的时候&#xff0c;像毕设、标书这些 为什么呢&#xff1f;因为这样更方便修改&#xff0c;后续的增加和删除段落&#xff0c;编号会自动排列&am…

MySQL8安装与卸载

1.下载mysql MySQL :: Download MySQL Community Serverhttps://dev.mysql.com/downloads/mysql/ 2.解压mysql安装包 解压到自己定义的目录&#xff0c;这里解压就是安装&#xff0c;解压后的路径不要有空格和中文。 3.配置环境变量 配置环境变量可以方便电脑在任何的路径…

牛客网刷题 ——C语言初阶——JZ15 二进制中1的个数

1.题目描述 题目OJ链接 描述 输入一个整数 n &#xff0c;输出该数32位二进制表示中1的个数。其中负数用补码表示。 2.思路 求2进制中1的个数&#xff0c;可以转换为求每一位&#xff0c;1的个数&#xff0c;1&1还是1 所以判断如果该数值&1为真&#xff0c;我们就co…

SAP物料主数据界面增加客制化字段、客制化页签的方式

文章目录 前言一、不增加页签&#xff0c;只增加客制化字段二、增加物料主数据页签 前言 【SAP系统MM模块研究】 #SAP #MM #物料 #客制化 #物料主数据 项目上难免会遇到客户要在物料主数据的界面上&#xff0c;增加新字段的需求。 实现方式有&#xff1a; &#xff08;1&…

十一、Vue 自定义指令详解

文章目录 一、自定义指令概念二、自定义指令的基本语法全局自定义指令局部自定义指令三、指令钩子函数bindinsertedupdatecomponentUpdatedunbind四、指令的参数传递简单参数传递动态参数传递五、自定义指令的应用场景权限控制动画效果第三方库集成六、自定义指令的双向绑定双向…

《Spring Framework实战》1:Spring简介

欢迎观看《Spring Framework实战》视频教程 Spring简介 目录 1. Spring简介 2. Spring项目 3. Spring 能做什么&#xff1f; Spring 使 Java 简单化。 Spring 使 Java 现代化。 Spring 使 Java 富有成效。 Spring 使 Java 反应性。 Spring 使 Java 轻松上云。 Sprin…

利用KPaaS平台提升企业审批流程的透明度

企业的审批流程不仅影响决策效率&#xff0c;还直接关联到组织的透明度和运营效果。传统的审批流程通常由多个环节和系统构成&#xff0c;每一个环节都可能存在信息不对称的现象。例如&#xff0c;某一审批节点的负责人可能并不清楚当前的审批状态&#xff0c;而在其他环节&…

重塑信任与价值:MHX如何定义数字资产新规则

在全球经济逐步数字化的浪潮中&#xff0c;数字资产交易正以惊人的速度成为主流投资方式。然而&#xff0c;这个市场充满机遇的同时&#xff0c;也因规则不透明、风险过高等问题让许多投资者望而却步。在这样的背景下&#xff0c;MHX曼哈顿数字资产交易所以全新的思维和创新的交…

《向量数据库指南》——应对ElasticSearch挑战,拥抱Mlivus Cloud的新时代

在当今数据驱动的商业环境中,向量数据库的应用正变得愈加重要。随着人工智能和机器学习的快速发展,尤其是在自然语言处理、图像识别及推荐系统等领域,向量数据库以其强大的存储和检索能力,迎来了广泛的应用机会。然而,在实际应用中,企业在选择和实施向量数据库方案时,常…

基于SpringBoot的网上订餐系统(源码+数据库+文档)

亲测完美运行带论文&#xff1a;文末获取源码 文章目录 项目简介&#xff08;论文摘要&#xff09;运行视频包含的文件列表&#xff08;含论文&#xff09;前台运行截图后台运行截图 项目简介&#xff08;论文摘要&#xff09; 随着我国经济的飞速发展&#xff0c;人们的生活速…

【保姆级】sql注入之堆叠注入

一、堆叠注入的原理 mysql数据库sql语句的默认结束符是以";"号结尾&#xff0c;在执行多条sql语句时就要使用结束符隔 开,而堆叠注入其实就是通过结束符来执行多条sql语句 比如我们在mysql的命令行界面执行一条查询语句,这时语句的结尾必须加上分号结束 select * fr…

Linux(centos)安装 MySQL 8 数据库(图文详细教程)

前言 前几天写了个window系统下安装Mysql的博客&#xff0c;收到很多小伙伴私信需要Linux下安装Mysql的教程&#xff0c;今天这边和大家分享一下&#xff0c;话不多说&#xff0c;看教程。 一、删除以前安装的MySQL服务 一般安装程序第一步都需要清除之前的安装痕迹&#xff…

Segment Anything论文详细翻译【Part2:引言Introduction】

目录 写在前面 Introduction 第1段 第2段 第3段 第4段 第5段 第6段 第7段 第8段 第9段 第10段 第11段 第12段 Figure2 关键特点 图中具体内容 图例说明 写在前面 为啥要写这篇文章&#xff1f;因为找不到一篇写的特别好的【翻译并仔细解释】文章。网上大多千…

整数拼接(哈希表 枚举)

2068. 整数拼接 - AcWing题库 #include <bits/stdc.h> using namespace std;const int N 1e5 10;int n,k; int a[N]; int s[11][N]; //因为Ai < 10^9 10^9 是一个10位数&#xff0c;所以要*10^10 才能拼接int main() {cin >> n >> k;for (int i 1;i &…

使用爬虫技术获取网页中的半结构化数据

目录 前言1. 半结构化数据与爬虫技术简介1.1 半结构化数据的定义与特性1.2 爬虫技术的基本原理 2. 爬取半结构化数据的实现过程2.1 明确目标与准备2.2 发送HTTP请求2.3 解析网页内容2.4 动态内容的处理2.5 数据存储与清洗 3. 技术挑战与应对策略3.1 处理反爬机制3.2 提高爬取效…

Linux(Centos 7.6)命令详解:ls

1.命令作用 列出目录内容(list directory contents) 2.命令语法 Usage: ls [OPTION]... [FILE]... 3.参数详解 OPTION: -l&#xff0c;long list 使用长列表格式-a&#xff0c;all 不忽略.开头的条目&#xff08;打印所有条目&#xff0c;包括.开头的隐藏条目&#xff09…

一文读懂主成分分析法(PCA)

主成分分析法&#xff08;PCA&#xff09; 主成分分析法&#xff08;PCA&#xff09;主成分分析的基本思想主成分的计算主成分分析的原理主成分分析的特点主成分分析的应用 主成分分析法&#xff08;PCA&#xff09; 主成分分析的基本思想 PCA是1901 年Pearson在研究回归分析…

LLVM防忘录

目录 Windows中源码编译LLVMWindows下编译LLVM Pass DLL Windows中源码编译LLVM 直接从llvm-project下载源码, 然后解压后用VS2022打开该目录, 然后利用VS的开发终端执行: cmake -S llvm -B build -G "Visual Studio 17 2022" -DLLVM_ENABLE_PROJECTSclang -DLLVM_…

adb 不是内部或外部命令,也不是可运行的程序或批处理文件。

1、问题概述&#xff1f; 本文讲述的是在window系统中安装了Android SDK之后&#xff0c;adb无法使用的情况。 在cmd中执行adb devices提示如下问题&#xff1a; adb 不是内部或外部命令&#xff0c;也不是可运行的程序或批处理文件。 问题&#xff1a;没有配置android sdk环…