前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >数据结构与算法 --- 递归(一)

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

作者头像
Niuery Diary
发布2023-10-22 16:56:37
3480
发布2023-10-22 16:56:37
举报
文章被收录于专栏:Niuery的技术日记

什么是递归?

「递归(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

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

代码语言:javascript
复制
public int Fibonaci(uint n)
{
    if (n > 0 && n <= 2) return 1;

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

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

递归的堆栈溢出问题

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

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

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

代码语言:javascript
复制
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) 这个值就被计算了三次。

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

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

代码语言:javascript
复制
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;
}

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

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

代码语言:javascript
复制
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 [2] 斐波那契数列:https://en.wikipedia.org/wiki/Fibonacci_number ❞

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-07-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Niuery Diary 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是递归?
  • 满足递归的条件
  • 如何编写递归代码
  • 递归的堆栈溢出问题
  • 递归的重复计算问题
  • 将递归代码改写为非递归代码
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档