递归算法(动图演示)

前言

在学习递归算法的时候一定会遇到这样一道题目:用递归算法求阶乘

其实用递归算法来解决求阶乘的问题是不明智的,之后再做讨论。这个例子用递归解法的目的是为了帮助理解递归,可以说是递归算法中一道非常经典的例题。

例如:计算 5 的阶乘可以有两种方法。

方法1:循环(迭代)

// JavaScript 实现
let fac = 1;
for (let i=1; i<=5; i++) {
    fac = fac * i;
}
console.log(fac);

方法2:递归

// JavaScript 实现
function factorial(n) {
    if (n == 1) {
        return n;
    }
    return n * factorial(n-1);
}
let fac = factorial(5)
console.log(fac);

采用循环的方式解决问题是很容易理解的。如果你能用循环解决这个问题,就说明你已经理解了如何用程序来计算阶乘,那么接下来你就可以尝试理解阶乘的递归解法:

本例题的动图演示:

 

  • 给 factorial 函数传入参数 5,if 判断条件不成立,执行最后的 return 语句。

  • return 语句中调用该函数自身,并且传入的参数值 -1。

  • 系统会为该函数开辟新的空间,并传入参数反复执行第一步和第二步(传入的参数递减)。

  • 当最后传入的参数正好使 if 判断成立,结束递归,将当前步骤所得值逐级返回。

所以本题最终所得结果就是 5 * 24 = 120 。

相信本例题已经让你对递归算法的实现方式有所理解了,接下来开始详细介绍递归算法!

什么是递归算法?

引用百度百科的介绍:

递归算法在计算机科学中是指一种通过重复将问题分解为同类的子问题而解决问题的方法。

顾名思义,递归算法就是 “递去” 和 “归来” 的组合。只不过,在这两个组合中间,还有一个判断条件,用来区分什么时候 “递去” 以及什么时候 “归来”。这个判断条件就是:终止条件(任何一个正常运行的递归程序都必须有一个终止条件!!!)

回顾本文开头的例题:

  • return 语句反复调用函数自身就是 “递去” 的过程。

    这是一种自调用的过程,递归算法是一种直接或者间接调用自身函数或者方法的算法。

  • 在递去的过程中,给函数传入的值逐级递减,但运算的方式却相同。

    将原问题不断分解为规模缩小的子问题,并使用相同的方式解决子问题,这就是递归算法的本质

  • 当传入的 n 值等于 1 的时候, if 判断条件成立,即到达终止条件。

    终止条件就是递归算法的临界点,到达临界点时,说明问题不需要再被分解,便开始逐级返回计算结果。

  • 最后将每一阶段所得值逐级返回就是 “归来” 的过程。

递归算法可以取代循环(迭代)吗?

引用百度百科的介绍:

计算理论可以证明递归的作用可以完全取代循环,因此在很多函数编程语言(如Scheme)中习惯用递归来实现循环。

但很多情况下,用递归来取代循环是不明智的!从性能方面考虑,递归每次调用自身时都会重新开辟空间,例如用栈机制实现递归的 c++,每次调用函数自身都会去开辟一次栈空间,如果递归的深度过深的话就会导致栈溢出而发生异常。

所以对于一个需要大量重复执行的简单问题,采用循环才是最明智的选择。而对于一个复杂的问题,很难用循环的方式去解决,即便是可以解决的,其循环结构也会相对复杂,难于理解。

我看见很多人在对比递归和循环的时候,都会认为递归和循环的区别之一是:递归代码简洁,容易理解。但对于一个刚开始接触递归的人来说,似乎是递归更难以理解吧!但是请相信我,当你掌握递归之后,你就会发现递归的魅力。

来对比一个递归解法和循环解法的例子:

爬楼梯:有 n 级楼梯,一次只能爬一步或两步,问 n 级楼梯有多少种爬法?

递归解法

// JavaScript 实现
function step(n) {
    if (n == 1 || n == 2) {
        return n;
    }
    return step(n-1) + step(n-2);
}
let ways = step(4);
console.log(ways);  // 输出:5

解题思路:这一题其实就是考虑每走一步后下一步该怎么走的问题,可以看出这是一个可以划分成多个子问题的问题。而递归的终止条件就是最后只剩下一步或者两步,至于为什么是一步或两步?因为如果只剩下一步,那么就只有一种走法,如果只剩下两步,那么就有两种走法,即 “1+1” 或者 “2” 的两种走法。

动图演示:

如图所示,return 语句中共调用了函数自身两次,这两次其实就是考虑下一步该走一步还是两步,一直走到终点,然后返回所有的走法。其实不难理解的,多看看动图联想联想就明白了。

循环解法

// JavaScript 实现
function step(n) {
    if(n == 1 || n == 2) { 
        return n; 
    }
    let temp1 =1;
    let temp2 =2;
    let res = 0;
    for(let i=3; i<=n; i++) {
        res = temp1 + temp2;
        temp1 = temp2;
        temp2 = res;
    }
    return res;
}
let ways = step(4);
console.log(ways);  // 输出:5

当你理解了上面的递归解法后再来看这里的循环解法,你就会深刻的认识到,原来递归真的很容易理解!而且代码更简洁。

所以递归算法可以取代循环吗?这还是要根据问题的本身来判断!如果只有 10 步台阶,递归显然是最优解,因为它易于理解,代码简洁;如果是 1000 步或者是 10000 步台阶呢,从性能上考虑,循环似乎才是最优解,尽管它难以理解!

这里对比一下 40 步台阶情况下递归和循环的效率:

let start = new Date();
// 省略递归部分代码
let end = new Date();
console.log((end-start)+"ms");  // 耗时:624ms
let start = new Date();
// 省略循环部分代码
let end = new Date();
console.log((end-start)+"ms");  // 耗时:4ms

台阶的阶数越多,递归耗费的时间就越长。

总结

递归算法的三个步骤:递去,到达终止条件,归来

什么样的问题可以用递归来解

  • 问题可以被分解为相同解法的子问题

  • 有明确的终止条件(递归太深影响效率)

递归和循环的区别

  • 递归代码简洁,处理复杂问题时易于理解,但效率低。

  • 循环效率高,处理复杂问题时代码结构复杂且不易于理解。

  • 7
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@cooltea

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值