尾递归与 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) 这种很大很大的数字我们都可以秒出答案

(完)

尾递归与 memorize 优化的更多相关文章

  1. 详解JavaScript调用栈、尾递归和手动优化

    调用栈(Call Stack) 调用栈(Call Stack)是一个基本的计算机概念,这里引入一个概念:栈帧. 栈帧是指为一个函数调用单独分配的那部分栈空间. 当运行的程序从当前函数调用另外一个函数时 ...

  2. Scala Tail Recursion (尾递归)

    Scala对尾递归进行了优化,甚至提供了专门的标注告诉编译器需要进行尾递归优化.不过这种优化仅限于严格的尾递归,间接递归等情况,不会被优化. 尾递归的概念 递归,大家都不陌生,一个函数直接或间接的调用 ...

  3. 尾递归与Continuation

    怎样在不消除递归的情况下防止栈溢出?(无论如何都要使用递归) 这几天恰好和朋友谈起了递归,忽然发现不少朋友对于“尾递归”的概念比较模糊,网上搜索一番也没有发现讲解地完整详细的资料,于是写了这么一篇文章 ...

  4. Scala尾递归

    递归函数应用 首先,我们来对比两个递归方法的求值步骤. 假设有方法gcd,用来计算两个数的最大公约数.下面是欧几里得算法的实现: def gcp(a: Int, b: Int): Int = if ( ...

  5. JavaScript 中的尾调用

    尾调用(Tail Call) 尾调用是函数式编程里比较重要的一个概念,它的意思是在函数的执行过程中,如果最后一个动作是一个函数的调用,即这个调用的返回值被当前函数直接返回,则称为尾调用,如下所示: f ...

  6. 第四章 函数(JavaScript:语言精粹)

    函数包含一组语句,用来指定对象的行为,其代码可以用来重复使用.   一般来说,编程就是将一组需求分解成一组函数和数据结构的技能.   概览:函数对象 | 函数字面量 | 调用 | 方法调用模式 | 函 ...

  7. ES6 入门系列 - 函数的扩展

    1函数参数的默认值 基本用法 在ES6之前,不能直接为函数的参数指定默认值,只能采用变通的方法. function log(x, y) { y = y || 'World'; console.log( ...

  8. Android ViewTreeObserver简介

    Android ViewTreeObserver简介   一.结构 public final class ViewTreeObserver extends Object java.lang.Objec ...

  9. SCIP读书笔记(1)

    这书也算是必修吧,尤其是我这种非科班人员,还是应该抽时间尽量学习一下.大致翻过一遍,习题非常多,尽力吧. ##构造过程抽象 * 为了表述认知,每种语言都提供了三种机制:基本元素:组合方式:抽象方法. ...

随机推荐

  1. Saltstack自动化扩容

    一. etcd服务的安装和使用 1.安装etcd应用: wget https://github.com/coreos/etcd/releases/download/v2.2.5/etcd-v2.2.5 ...

  2. 攻防世界upload1

    upload1 进入环境就一个上传,我们先上传一个普通的木马文件看看 木马内容 <?php @eval($_POST["cmd"]); ?> 估计是前端校验我们查看源码 ...

  3. 10本 Linux PDF 书籍免费分享

    本篇文章主要分享以下Linux开发PDF书籍 一.Linux程序设计二.Unix环境高级编程三.Unix_Linux编程实践教程四.鸟哥的私房菜五.深入理解Linux内核六.Linux命令行与shel ...

  4. Altium design使用日常故障总结

    1.altiumdesigner09如何将不同的板子拼在一起发给工厂?打开这两个图,其中一个图ctrl+a,ctrl+c,打开另一个图pastespecial.放置时选取一边对齐.制版时告诉厂家做个V ...

  5. 移动端比1px还小的border

    巧用border 在移动端 经常出现border,细边框但有的时候 产品大大1px甚至乎会觉得不够细那么要如何写出比1px还要小的border下面是代码 希望对大家有所帮助 .thinner-bord ...

  6. canvas 1px 出现模糊解决方法及原理

    关于canvas绘制1像素出现模糊的原因及解决方法 canvas是html5中非常强大的功能,但是在绘制的时候如果出现1像素,例如画一条1像素的线可能出现模糊情况. 一.解决方法 网上比较常见的解决方 ...

  7. canvas系列教程07-canvas动画基础1

    上面我们玩了一个图表,大家学好结构,然后在那个基础上去扩展各种图表,慢慢就可以形成自己的图表库了.也可以多看看一些国外的图表库简单的版本,分析分析,读代码对提高用处很大.我说了canvas两大主流用途 ...

  8. ES6-11学习笔记--扩展运算符与rest参数

    1.符号都是使用:... 2.扩展运算符:把数组或者类数组展开成用逗号隔开的值 3.rest参数:把逗号隔开的值组合成一个数组   扩展运算符: function foo(a, b, c) { con ...

  9. 抽象方法不能为private,final或者static,为什么?

    4)抽象方法不能为private,final或者static, native, synchrozied为什么?[新手可忽略不影响继续学习]马克-to-win:抽象方法的最实质的意义在于被未来的子类覆盖 ...

  10. maven项目中各文件都没有报错,但是项目名称有红叉

             项目报错可以看到Problems(可以在Window--Show View--other--输入Problems找到) 一.可能原因     1.项目中各文件没有报错,但是项目名称中 ...