首页 > 编程语言 >数据结构与算法 --- 递归(二)

数据结构与算法 --- 递归(二)

时间:2023-08-13 18:34:19浏览次数:42  
标签:返回 递归 Factorial 局部变量 函数调用 --- 堆栈 数据结构

引言

上文数据结构与算法 --- 递归(一) 讲述了什么是递归算法,如何编写递归算法及如何写好递归算法,本文着重讲述一下如何避免递归过深导致的堆栈溢出问题。

探究产生堆栈溢出的原因

函数调用采用函数调用栈来保存当前“快照”(局部变量,返回地址等)。函数调用栈是内存中开辟的一块存储空间,它被组织成“栈”这种数据结构,数据先进后出。

递归的过程包含大量的函数调用,如果递归求解的数据规模很大,函数调用层次很深,那么函数调用栈中的数据(栈帧)会越来越多,而函数调用栈空间一般不大,堆栈空间不足以存储所有的调用信息,从而导致堆栈溢出。

以计算阶乘举例,代码如下:

public int Factorial(uint n)
{
    if (n <= 1) return 1;

    return n * Factorial(n - 1);
}

Factorial(n) 执行到 Factorial(n - 1) 是,编译器将局部变量,nFactorial(n - 1) 的返回地址封装为一个栈帧,保存在函数调用栈内,然后再跳转到 Factorial(n - 1) 函数体内。

Factorial(n - 1) 执行完成之后,返回结果(假设是 result ),编译器就从函数调用栈中取出之前保存的栈帧(局部变量 nFactorial(n - 1) 的返回地址)。通过返回地址,编译器就知道之前执行到了哪条语句(即 return n * Factorial(n - 1) 这条语句),就可以接着从这条语句继续往下执行:将栈帧中保存的 n 的值,与 Factorial(n - 1) 的结果(即 result )相乘后将结果返回。

那如果不在函数调用栈中存储局部变量,返回地址等信息,是否可行呢?

答案肯定是不行。

  • 若不存储返回地址,那么 Factorial(n - 1) 执行完之后,编译器就不知道该从哪里继续执行。
  • 若不存储局部变量,那么 Factorial(n - 1) 执行完之后,编译器即使知道该从哪里执行,但不知道 n 的值,也就无法计算 n * Factorial(n - 1) 并返回了。

讨论尾递归避免堆栈溢出

什么是尾递归?

尾递归是指一个递归函数的最后一个操作是递归调用自身,并且该调用的返回值直接返回给函数的调用者,而不进行任何其他的计算或处理。这种形式的递归称为尾递归

在尾递归中,递归调用是函数的最后一步操作,因此不需要再次回到递归调用之前的位置来执行其他操作。这意味着尾递归可以被优化为循环,从而避免了递归调用带来的栈空间开销和性能问题。

例如将上述阶乘代码转化为尾递归代码如下:

public int Factorial(uint n, int res)
{
    if (n <= 1) return res;
    return Factorial(n - 1, (int)(n * res));
}

从理论上来说,尾递归是又可能解决堆栈溢出的问题的。

上文说到,函数调用栈中保存局部变量和返回地址,而对于尾递归代码,递归代码出现在最后一行中,返回之后不需要继续往下执行,因此,返回地址可以不用保存;而局部变量 n 也被移动到新的函数 Factorial(n - 1, n * res) 中,也不需要保存到栈中。所以对于尾递归代码,不需要想栈里压入数据,也就不存在堆栈溢出的问题。

但是在实际开发过程中,尾递归其实并没有太大作用,不能期望它来规避递归导致的堆栈溢出问题,主要表现在:

  • 并不是所有编程语言都支持尾递归优化
  • 并不是所有的递归都可以改成尾递归
  • 能改成尾递归的代码也就都可以改成迭代方式
  • 尾递归代码的可读性差

参考资料

[1] 数据结构与算法之美 / 王争 著. --北京:人民邮电出版社,2021.6

标签:返回,递归,Factorial,局部变量,函数调用,---,堆栈,数据结构
From: https://www.cnblogs.com/pandefu/p/17536265.html

相关文章

  • 数据结构与算法 --- 递归(一)
    什么是递归?递归(Recursion)是一种解决问题的方法,它将问题分解为更小的子问题,并逐层解决这些子问题。递归算法的核心思想是:一个函数可以直接或间接地调用自身。通过这种自我调用,我们可以用简洁的代码来解决复杂问题。满足递归的条件一般来说,满足下面三个条件就可以使用递归:待......
  • 数据结构与算法 --- 排序算法(二)
    title:数据结构与算法---排序算法(二)category:数据结构与算法tags:算法updatedAt:2023-05-18T15:29:17.847ZcreatedAt:2023-05-13T14:43:31.656Z引言上一篇数据结构与算法---排序算法(一)中,学习了冒泡排序,插入排序,选择排序这三种时间复杂度为\(O(n^2)\)的算法。实......
  • 无涯教程-Perl - ref函数
    描述如果EXPR为引用,则此函数返回真值;如果未提供EXPR,则为$_。返回的实际值还定义了引用所引用的实体的类型。内置类型为-REFSCALARARRAYHASHCODEGLOBLVALUEIO::Handle如果使用bless()函数为变量设置了祝福,则将返回新的数据类型。新的数据类型通常将是一个类名。语......
  • WPF 入门笔记 - 07 - MVVM示例
    滴咚,大家好久不见......
  • 定时任务查询通道狂暴超时,原因竟然是取数据不当----清扫100年前纽约街头马粪的不是清
    本文首发于我的公众号[发现问题就解决,是低效的方式,得探究根源]、【100年前的纽约街头,市民以马车为出行工具,问题来了】 我们支付系统有个定时任务,就是将系统里所有付款中的交易,调用第三方银行查单接口,然后持久化更新付款状态。 许多同学都做过类似的定时调度程序吧。 近......
  • 暑假牛客多校第八场 2023-8-11(H、K)
    H.Insert1,Insert2,Insert3,...算法:栈做法:   我们分析题目发现每个区间的左端点一定是\(1\),而且每个新加入的数\(x\)一定是匹配最靠近它的且未经匹配的\(x-1\)。举个例子,在[1,1,2,2,3]中我们加入一个数\(3\)时由于从左到右的第二个\(2\)是已经和第一个......
  • 【8月摸鱼计划】Air780E|物联网模组|AT命令|MQTT接入|云平台(1)-MQTT基本原理及AT步骤
    基础资料基于Air780E开发板:Air780E文档中心简介:AT开发探讨重点AT固件是通信模组或者单片机(MCU)+网络模块标准固件的基本配置,该模式定制化程序较高,简单易上手,但缺点也较为明显,仅用于快速基本功能验证。本系列主要探讨MQTT方式手动接入、信息订阅及发布的基本原理,后续详细介绍接入多......
  • 【HIVE系列】01-HIVE 常用操作
    title:【HIVE系列】01-HIVE常用操作date:2018-11-1320:20:31update:2018-11-1517:10:43categories:-大数据技术-hivetags:[hive]参考资料:https://blog.csdn.net/wisgood/article/details/17376393http://ju.outofmemory.cn/entry/1764081.数据库操作(增删......
  • 牛客sql-计算用户的平均次日留存率
    参考大佬题解做一下记录:https://blog.nowcoder.net/n/fe24f96a26f1437da19e91ab1d035b03?f=commenthttps://blog.nowcoder.net/n/dd3d75ce08e3485c95bafe3c23668fc2?f=comment https://www.runoob.com/sql/sql-dates.htmlDATE_ADD(date,intervalexprtype) date参数是合......
  • dp-双调欧几里德旅行商问题
    双调欧几里德旅行商问题目录双调欧几里德旅行商问题问题描述分析递推关系程序算法导论3rd-15.3问题描述平面上n个点,确定一条连接各点的最短闭合旅程。这个解的一般形式为NP的(在多项式时间内可以求出)J.L.Bentley建议通过只考虑双调旅程(bitonictour)来简化问题,这种旅程即......