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

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

时间:2023-08-13 18:34:08浏览次数:43  
标签:问题 调用 Fibonaci 递归 代码 --- 算法 数据结构

什么是递归?

递归(Recursion) 是一种解决问题的方法,它将问题分解为更小的子问题,并逐层解决这些子问题。递归算法的核心思想是:一个函数可以直接或间接地调用自身。通过这种自我调用,我们可以用简洁的代码来解决复杂问题。

满足递归的条件

一般来说,满足下面三个条件就可以使用递归:

  • 待求解问题的解可以分解为多个子问题的答案。子问题就是数据规模更小的问题。
  • 待求解问题与分解之后的问题,只有数据规模不同,求解思路完全相同。
  • 存在递归终止的条件。递归问题必须得有终止条件,否则将会无限循环。

如何编写递归代码

编写递归代码的关键是将符合递归条件的问题公式化,将问题变成递推公式,寻找终止条件,然后根据公式“翻译”为代码。

例如斐波那契数列的问题:数列的前两项为1,从第三项开始,每一项都等于前两项之和,那么求解斐波那契数列的第\(n\)项则有:

  • \(n\)为正整数 \(n\)∈N
  • 当\(n=1\)或\(n=2\),值为1
  • 当\(n>2\)时,则\(f(n)=f(n-1)+f(n-2)\)

则有

\[ f(x) = \begin{cases} 1 & 0< x\leq 2 \\ f(x-1)+f(x-2) & x>2 \end{cases}\qquad x∈N \]

然后将上述公式“翻译”为代码,如下所示:

public int Fibonaci(uint n)
{
    if (n > 0 && n <= 2) return 1;

    return Fibonaci(n - 1) + Fibonaci(n - 2);
}

所以,编写递归代码的关键就是找到将大问题分解为小问题的规律,并且基于此写出递推公式,然后找出终止条件,最后将公式"翻译"成代码。

递归的堆栈溢出问题

在函数调用会使用栈来保存临时变量,每调用一个新的函数,都会将临时变量封装为栈帧,压入内存栈,等函数执行完成后,再将栈帧出栈,所以,如果递归求解的数据规模很大,调用层次很深,一直往函数栈里添加数据,就会塞满函数栈,导致堆栈溢出。

如何避免出现堆栈溢出呢?可以通过在代码中限制递归调用的最大深度

假设限制递归深度为1000,则修改后代码:

static int depth = 0;

public static int Fibonaci(uint n)
{
    depth++;

    if (depth > 1000) throw new Exception("超出递归范围!");

    if (n > 0 && n <= 2) return 1;

    return Fibonaci(n - 1) + Fibonaci(n - 2);
}

递归的重复计算问题

除堆栈溢出问题外,编写递归还会出现重复计算的问题,例如上述斐波那契数列的递归,在执行时就有重复计算的问题。

例如,当计算 Fibonaci(5) 的时候,需要计算 Fibonaci(4)Fibonaci(3),而计算 Fibonaci(4) 又需要计算 Fibonaci(3)Fibonaci(2),以此类推。因此,Fibonaci(3) 这个值就被计算了两次,Fibonaci(2) 这个值就被计算了三次。

为了避免重复,可以使用字典将计算过的值存储下来,当递归调用到已经计算过的值时,直接从字典中取值并返回,这样就省掉了重复计算。

将上文中的代码再进行优化:

static int depth = 0;

static Dictionary<uint, int> ValuePairs = new Dictionary<uint, int>();

public static int Fibonaci(uint n)
{
    depth++;

    if (depth > 1000) throw new Exception("超出递归范围!");

    if (n > 0 && n <= 2) return 1;

    if (ValuePairs.ContainsKey(n))
    {
        return ValuePairs[n];
    }

    var res = Fibonaci(n - 1) + Fibonaci(n - 2);

    ValuePairs.Add(n, res);

    return res;
}

将递归代码改写为非递归代码

使用递归编程有利有弊,递归编程的好处是使用递归编写的代码的表达能力强,写起来简洁,而递归编程的劣势是空间复杂度高,且存在堆栈溢出和重复计算的问题,因此,在实际开发过程中,可以根据实际情况来决定是是否使用递归实现,例如可以将上述的斐波那契数列的代码改为非递归代码,如下所示:

public static int Fibonaci(uint n)
{

    if (n > 0 && n <= 2) return 1;

    int prev = 1;

    int prev_prev = 1;

    int result = 0;
    
    for (int i = 2; i < n; i++)
    {
        result = prev + prev_prev;

        prev_prev = prev;

        prev = result;
    }

    return result;
}

所有递归算法都可以改写为迭代循环的非递归写法吗?

是,理论上所有递归算法都可以改写为迭代循环的非递归写法。这是因为递归算法本质上是一个函数在自己内部不断调用自己,而迭代循环可以通过变量的更新来达到相同的效果。

具体来说,可以通过使用一个栈或队列等数据结构来模拟递归函数的调用过程。每当递归函数需要调用自身时,将当前的参数值和程序计数器等信息保存到栈或队列中,然后继续执行下一个语句。当递归函数返回时,从栈或队列中弹出保存的信息,恢复之前的状态,并继续执行之前被中断的语句。

虽然理论上可以将所有递归算法改写为迭代循环的非递归写法,但实际上有些算法可能更适合使用递归实现,而另一些算法则更适合使用迭代循环实现。例如,递归算法通常在树形结构的遍历和图形搜索等算法中使用,而迭代循环则更适合处理数值计算等需要大量循环迭代的算法。

总结

递归式一种高效,简洁的编码模式,只要满足递归的3个条件,就可以使用递归算法去实现。不过,递归代码比较难写,难理解。编写递归代码的关键是不要试图去模拟计算机递归调用的过程,正确的编写方式是写出递推公式,找出终止条件,然后"翻译"为代码。

递归也有它自己的弊端,比如堆栈溢出,重复计算,函数调用耗时多和空间复杂度高,所以在编写递归算法代码时,要避免出现这些问题。

参考资料

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

标签:问题,调用,Fibonaci,递归,代码,---,算法,数据结构
From: https://www.cnblogs.com/pandefu/p/17536264.html

相关文章

  • 数据结构与算法 --- 排序算法(二)
    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)来简化问题,这种旅程即......
  • t113-c-触摸篇
    学一下如何添加触摸先在menuconfig里面寻找是否有GT911但是结果并没有找得到那么在kernel_menuconfig中是否有呢也没见有,但是我找到了gt9xx这个选项估计就是这个了,那就不用添加驱动了把它选上board.dts设备树中也应该看一看,这中驱动硬是在iic也就是twi总线下的,果然在twi......