zoukankan      html  css  js  c++  java
  • 尾递归与 memorize 优化

    尾递归与 memorize 优化

    本文写于 2020 年 12 月 10 日

    递归

    递归是一种非常常见的算法思维,在大家刚开始学编程的时候应该就会接触到。

    我们可以这么理解递归:

    function 讲故事() {
      从前有座山,山里有座庙;
    
      庙里有个老和尚给小和尚讲故事;
    
      讲的什么故事呢;
    
      讲故事()
    }
    

    递归就是“我用我自己”。

    递归的次数缺陷

    但是众所周知,递归是会进行「压栈」和「弹栈」的。

    因为递归是在自己里面调用自己,所以上一个函数根本没有结束的时候,我们就要再一次调用新的函数,这样在调用栈里面的函数根本没有机会出来——直到爆栈。

    function foo() {
      try {
        return 1 + foo();
      } catch(e) {
        console.error(e);
        return 1;
      }
    }
    

    可以尝试在浏览器中运行这段代码,他会告诉你该浏览器的调用栈长度是多少,并且报错:Maximum call stack size exceeded.,超过调用栈的最大长度。

    一般这个值会在一万左右浮动,根据不同的电脑、系统、浏览器呈现出不一样的结果。

    可以看到这个值可能一般够用,但还是容易爆栈。这就是递归的第一个缺陷:次数有限

    递归的速度缺陷

    斐波那契第 n 项的值计算应该都大家都会:

    const fib = (n) => (n === 0 || n === 1 ? n : fib(n - 1) + fib(n - 2));
    

    这时候我们给他加一个计时功能(代码我就不写了),看看求每一项的值需要多少时间。

    项数 时间(ms)
    20 1
    24 4
    32 69
    38 1193
    40 3065
    64 太慢了,出不来结果了

    可以看到这个数字上涨的非常的离谱,这就是递归的第二个缺陷:速度太慢

    尾递归

    尾递归比递归快的条件,是在编译阶段进行了优化(Safari 实现了),如果编译器根本没有优化尾递归,那么速度和递归就没有区别了。

    首先理解什么递归需要压栈和弹栈。

    因为函数执行到一半,又去执行了函数,而这个新函数结束之后我们还需要用到当前函数里参数、环境……等,所以我们得记住他们,并且回来。

    尾递归就是让我们不需要当前函数的环境了,直接 return 我们的答案,自然也就不需要将大量的信息进行压栈、弹栈了。

    const fib = (n) => (n === 0 || n === 1 ? n : fib(n - 1) + fib(n - 2));
    

    这段代码里面,我们需要回来之后进行相加,所以需要进行压栈弹栈。

    Memorize 优化

    关于递归速度太慢,我们的解决方案可以是:Memorize 优化

    我们在计算第 n 项的时候,本质上是从第 0 项开始算起:

    1. fib(4) = fib(3) + fib(2)
    2. fib(3) = fib(2) + fib(1)
    3. fib(2) = fib(1) + fib(0)
    4. fib(1) = 1
    5. fib(0) = 0

    我们是在弹栈的时候发生计算,那也就是倒着来:

    1. 先算 fib(0)fib(1)
    2. 再算 fib(2)
    3. 再算 fib(3)
    4. 再算 fib(4)——此时我们已经忘记了 fib(2) 的值,只知道 fib(3),所以还要再算一遍 fib(2)

    由此我们可以知道在计算过程中发生了太多重复的计算

    完全可以用一个哈希表存起来这些数据,第二次、第三次使用的时候直接获取结果就可以了,没有必要像第一次一样重新计算。

    const memorize = (fn) => {
      const cache = {};
      return (n) => {
        if (!cache[n]) {
          cache[n] = fn(n);
        }
        return cache[n];
      };
    };
    
    const fib = memorize((n) => (n === 0 || n === 1 ? n : fib(n - 1) + fib(n - 2)));
    

    我们发现 fib(1024), fib(2048) 这种很大很大的数字我们都可以秒出答案

    (完)

  • 相关阅读:
    字符串切片
    格式化输出
    原生链+对象冒充组合继承模式
    对象冒充继承和原生链实现继承的方法和问题
    xlwt使用
    xlrd使用
    pip安装插件库
    第一天入驻博客园
    2-2ARP概念
    1-14常用的应用层协议及应用
  • 原文地址:https://www.cnblogs.com/xhyccc/p/14113245.html
Copyright © 2011-2022 走看看