尾递归与 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 项开始算起:
fib(4) = fib(3) + fib(2)
fib(3) = fib(2) + fib(1)
fib(2) = fib(1) + fib(0)
fib(1) = 1
fib(0) = 0
我们是在弹栈的时候发生计算,那也就是倒着来:
- 先算
fib(0)
和fib(1)
; - 再算
fib(2)
; - 再算
fib(3)
; - 再算
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 优化的更多相关文章
- 详解JavaScript调用栈、尾递归和手动优化
调用栈(Call Stack) 调用栈(Call Stack)是一个基本的计算机概念,这里引入一个概念:栈帧. 栈帧是指为一个函数调用单独分配的那部分栈空间. 当运行的程序从当前函数调用另外一个函数时 ...
- Scala Tail Recursion (尾递归)
Scala对尾递归进行了优化,甚至提供了专门的标注告诉编译器需要进行尾递归优化.不过这种优化仅限于严格的尾递归,间接递归等情况,不会被优化. 尾递归的概念 递归,大家都不陌生,一个函数直接或间接的调用 ...
- 尾递归与Continuation
怎样在不消除递归的情况下防止栈溢出?(无论如何都要使用递归) 这几天恰好和朋友谈起了递归,忽然发现不少朋友对于“尾递归”的概念比较模糊,网上搜索一番也没有发现讲解地完整详细的资料,于是写了这么一篇文章 ...
- Scala尾递归
递归函数应用 首先,我们来对比两个递归方法的求值步骤. 假设有方法gcd,用来计算两个数的最大公约数.下面是欧几里得算法的实现: def gcp(a: Int, b: Int): Int = if ( ...
- JavaScript 中的尾调用
尾调用(Tail Call) 尾调用是函数式编程里比较重要的一个概念,它的意思是在函数的执行过程中,如果最后一个动作是一个函数的调用,即这个调用的返回值被当前函数直接返回,则称为尾调用,如下所示: f ...
- 第四章 函数(JavaScript:语言精粹)
函数包含一组语句,用来指定对象的行为,其代码可以用来重复使用. 一般来说,编程就是将一组需求分解成一组函数和数据结构的技能. 概览:函数对象 | 函数字面量 | 调用 | 方法调用模式 | 函 ...
- ES6 入门系列 - 函数的扩展
1函数参数的默认值 基本用法 在ES6之前,不能直接为函数的参数指定默认值,只能采用变通的方法. function log(x, y) { y = y || 'World'; console.log( ...
- Android ViewTreeObserver简介
Android ViewTreeObserver简介 一.结构 public final class ViewTreeObserver extends Object java.lang.Objec ...
- SCIP读书笔记(1)
这书也算是必修吧,尤其是我这种非科班人员,还是应该抽时间尽量学习一下.大致翻过一遍,习题非常多,尽力吧. ##构造过程抽象 * 为了表述认知,每种语言都提供了三种机制:基本元素:组合方式:抽象方法. ...
随机推荐
- 学习Kvm(六)
五,管理虚拟存储 5.1 虚拟磁盘概述 5.1.1 虚拟化项目中存储的注意事项 [x] 存储的性能几乎总是虚拟化的瓶颈 [x] 通过多个硬盘驱动以分布磁盘I/O来实现存储解决方案 [x] 考虑部署集中 ...
- C语言之标识符(知识点3)
条件:用户表示符仅由大小写英文字母,数字和下划线组成,且第一个字符不能是数字 注意: 不能和关键字或函数库相同名字 但关键字的大写就可以用了,因为关键字都是小写的,而C语言区分大小写 案例
- 阿里云场景化阿里云企业数字化转型售前方法PSA
阿里云场景化阿里云企业数字化转型售前方法PSA 目录 01 课程收获 理解企业数字化转型的概念.内涵.本质 了解企业数字化转型的要点.目标和切入点 掌握数字化转型项目售前阶段实践方法 场景化方案 阿里 ...
- 【分享】WeX5的正确打开方式(6)——数据组件初探
本文是[WeX5的正确打开方式]系列的第6篇文章,简单介绍一下WeX5中数据组件的特性和结构形式. 数据组件的由来 上一篇 WeX5绑定机制我们实现了一个简单的记账本应用,当时所有数据都用 JSON ...
- 给新手的最简单electron使用教程
我花了两个月闲暇翻译完了文档,大概是目前最完整最实时的中文文档了,有需要可以去看看学学:github传送门,大多数的需求阅读文档即可解决,实际上,翻译文档正是我入门一项未知事物时的最简单常用的法子. ...
- 【Android Studio】Gradle统一管理版本号引用配置
1.在根目录下的build.gradle文件下添加 ext{ .... } 中的内容 ...... // Define versions in a single place ext { // SDK ...
- 【Android开发】Android6.0请求权限方式
记录一下最普通的动态请求权限的方法: private int requestCode == 123; //判断当前系统的版本 if(Build.VERSION.SDK_INT >= 23){ i ...
- jboss7学习3-jboss安装 访问(外网)添加用户
一.下载安装 1.下载地址: http://www.jboss.org/jbossas/downloads ,下载Certified Java EE 6 Full Profile版本. 2.解压 jb ...
- BlockingCollection实现生产者消费者
很简单的代码,看注释 class Program { private static ManualResetEvent _manualResetEvent = new ManualResetEvent( ...
- Python入门-多进程
1.获取本机CPU # 早期的CPU是单核:实现多个程序并行,在某一时间点,其实只有一个进程 # 后来硬件多核CPU:多个进程是并行执行. from multiprocessing import cp ...