本文讲解函数记忆与菲波那切数列的实现,分享给大家,具体如下

定义
函数记忆是指将上次的计算结果缓存起来,当下次调用时,如果遇到相同的参数,就直接返回缓存中的数据。
举个例子:
function add(a, b) {
return a + b;
}
// 假设 memorize 可以实现函数记忆
var memoizedAdd = memorize(add);
memoizedAdd(1, 2) // 3
memoizedAdd(1, 2) // 相同的参数,第二次调用时,从缓存中取出数据,而非重新计算一次
原理
实现这样一个 memorize 函数很简单,原理上只用把参数和对应的结果数据存到一个对象中,调用时,判断参数对应的数据是否存在,存在就返回对应的结果数据。
第一版
我们来写一版:
// 第一版 (来自《JavaScript权威指南》)
function memoize(f) {
var cache = {};
return function(){
var key = arguments.length + Array.prototype.join.call(arguments, ",");
if (key in cache) {
return cache[key]
}
else return cache[key] = f.apply(this, arguments)
}
}
我们来测试一下:
var add = function(a, b, c) {
return a + b + c
}
var memoizedAdd = memorize(add)
console.time('use memorize')
for(var i = 0; i < 100000; i++) {
memoizedAdd(1, 2, 3)
}
console.timeEnd('use memorize')
console.time('not use memorize')
for(var i = 0; i < 100000; i++) {
add(1, 2, 3)
}
console.timeEnd('not use memorize')
在 Chrome 中,使用 memorize 大约耗时 60ms,如果我们不使用函数记忆,大约耗时 1.3 ms 左右。
注意
什么,我们使用了看似高大上的函数记忆,结果却更加耗时,这个例子近乎有 60 倍呢!
所以,函数记忆也并不是万能的,你看这个简单的场景,其实并不适合用函数记忆。
需要注意的是,函数记忆只是一种编程技巧,本质上是牺牲算法的空间复杂度以换取更优的时间复杂度,在客户端 JavaScript 中代码的执行时间复杂度往往成为瓶颈,因此在大多数场景下,这种牺牲空间换取时间的做法以提升程序执行效率的做法是非常可取的。
第二版
因为第一版使用了 join 方法,我们很容易想到当参数是对象的时候,就会自动调用 toString 方法转换成 [Object object],再拼接字符串作为 key 值。我们写个 demo 验证一下这个问题:
var propValue = function(obj){
return obj.value
}
var memoizedAdd = memorize(propValue)
console.log(memoizedAdd({value: 1})) // 1
console.log(memoizedAdd({value: 2})) // 1
两者都返回了 1,显然是有问题的,所以我们看看 underscore 的 memoize 函数是如何实现的:
// 第二版 (来自 underscore 的实现)
var memorize = function(func, hasher) {
var memoize = function(key) {
var cache = memoize.cache;
var address = '' + (hasher ? hasher.apply(this, arguments) : key);
if (!cache[address]) {
cache[address] = func.apply(this, arguments);
}
return cache[address];
};
memoize.cache = {};
return memoize;
};
从这个实现可以看出,underscore 默认使用 function 的第一个参数作为 key,所以如果直接使用
var add = function(a, b, c) {
return a + b + c
}
var memoizedAdd = memorize(add)
memoizedAdd(1, 2, 3) // 6
memoizedAdd(1, 2, 4) // 6
肯定是有问题的,如果要支持多参数,我们就需要传入 hasher 函数,自定义存储的 key 值。所以我们考虑使用 JSON.stringify:
var memoizedAdd = memorize(add, function(){
var args = Array.prototype.slice.call(arguments)
return JSON.stringify(args)
})
console.log(memoizedAdd(1, 2, 3)) // 6
console.log(memoizedAdd(1, 2, 4)) // 7
如果使用 JSON.stringify,参数是对象的问题也可以得到解决,因为存储的是对象序列化后的字符串。
适用场景
我们以斐波那契数列为例:
var count = 0;
var fibonacci = function(n){
count++;
return n < 2? n : fibonacci(n-1) + fibonacci(n-2);
};
for (var i = 0; i <= 10; i++){
fibonacci(i)
}
console.log(count) // 453
我们会发现最后的 count 数为 453,也就是说 fibonacci 函数被调用了 453 次!也许你会想,我只是循环到了 10,为什么就被调用了这么多次,所以我们来具体分析下:
当执行 fib(0) 时,调用 1 次
当执行 fib(1) 时,调用 1 次
当执行 fib(2) 时,相当于 fib(1) + fib(0) 加上 fib(2) 本身这一次,共 1 + 1 + 1 = 3 次
当执行 fib(3) 时,相当于 fib(2) + fib(1) 加上 fib(3) 本身这一次,共 3 + 1 + 1 = 5 次
当执行 fib(4) 时,相当于 fib(3) + fib(2) 加上 fib(4) 本身这一次,共 5 + 3 + 1 = 9 次
当执行 fib(5) 时,相当于 fib(4) + fib(3) 加上 fib(5) 本身这一次,共 9 + 5 + 1 = 15 次
当执行 fib(6) 时,相当于 fib(5) + fib(4) 加上 fib(6) 本身这一次,共 15 + 9 + 1 = 25 次
当执行 fib(7) 时,相当于 fib(6) + fib(5) 加上 fib(7) 本身这一次,共 25 + 15 + 1 = 41 次
当执行 fib(8) 时,相当于 fib(7) + fib(6) 加上 fib(8) 本身这一次,共 41 + 25 + 1 = 67 次
当执行 fib(9) 时,相当于 fib(8) + fib(7) 加上 fib(9) 本身这一次,共 67 + 41 + 1 = 109 次
当执行 fib(10) 时,相当于 fib(9) + fib(8) 加上 fib(10) 本身这一次,共 109 + 67 + 1 = 177 次
所以执行的总次数为:177 + 109 + 67 + 41 + 25 + 15 + 9 + 5 + 3 + 1 + 1 = 453 次!
如果我们使用函数记忆呢?
var count = 0;
var fibonacci = function(n) {
count++;
return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
};
fibonacci = memorize(fibonacci)
for (var i = 0; i <= 10; i++) {
fibonacci(i)
}
console.log(count) // 12
我们会发现最后的总次数为 12 次,因为使用了函数记忆,调用次数从 453 次降低为了 12 次!
兴奋的同时不要忘记思考:为什么会是 12 次呢?
从 0 到 10 的结果各储存一遍,应该是 11 次呐?咦,那多出来的一次是从哪里来的?
所以我们还需要认真看下我们的写法,在我们的写法中,其实我们用生成的 fibonacci 函数覆盖了原本了 fibonacci 函数,当我们执行 fibonacci(0) 时,执行一次函数,cache 为 {0: 0},但是当我们执行 fibonacci(2) 的时候,执行 fibonacci(1) + fibonacci(0),因为 fibonacci(0) 的值为 0, !cache[address] 的结果为 true,又会执行一次 fibonacci 函数。原来,多出来的那一次是在这里!
多说一句
也许你会觉得在日常开发中又用不到 fibonacci,这个例子感觉实用价值不高呐,其实,这个例子是用来表明一种使用的场景,也就是如果需要大量重复的计算,或者大量计算又依赖于之前的结果,便可以考虑使用函数记忆。而这种场景,当你遇到的时候,你就会知道的。
# JavaScript
# 函数记忆
# JavaScript Memoization 让函数也有记忆功能
# javascript 用记忆函数快速计算递归函数
# 的是
# 是有
# 数为
# 你会
# 使用了
# 当我们
# 就会
# 是在
# 第一个
# 一句
# 是指
# 当你
# 你看
# 执行时间
# 这个问题
# 是从
# 很容易
# 一遍
# 给大家
# 很简单
相关文章:
安徽网站建设与外贸建站服务专业定制方案
宝塔面板如何快速创建新站点?
惠州网站建设制作推广,惠州市华视达文化传媒有限公司怎么样?
非常酷的网站设计制作软件,酷培ai教育官方网站?
,购物网站怎么盈利呢?
如何在建站之星网店版论坛获取技术支持?
网站插件制作软件免费下载,网页视频怎么下到本地插件?
购物网站制作公司有哪些,哪个购物网站比较好?
制作假网页,招聘网的薪资待遇,会有靠谱的吗?一面试又各种折扣?
视频网站制作教程,怎么样制作优酷网的小视频?
c# 服务器GC和工作站GC的区别和设置
如何快速生成橙子建站落地页链接?
如何在Golang中使用encoding/gob序列化对象_存储和传输数据
Swift中switch语句区间和元组模式匹配
建站之星会员如何解锁更多建站功能?
网站网页制作专业公司,怎样制作自己的网页?
北京网站制作公司哪家好一点,北京租房网站有哪些?
已有域名和空间如何快速搭建网站?
南京网站制作费用,南京远驱官方网站?
详解免费开源的.NET多类型文件解压缩组件SharpZipLib(.NET组件介绍之七)
常州自助建站费用包含哪些项目?
北京专业网站制作设计师招聘,北京白云观官方网站?
代购小票制作网站有哪些,购物小票的简要说明?
Python文件管理规范_工程实践说明【指导】
番禺网站制作公司哪家值得合作,番禺图书馆新馆开放了吗?
建站之星好吗?新手能否轻松上手建站?
建站之星代理如何获取技术支持?
如何通过万网虚拟主机快速搭建网站?
建站之星展会模板:智能建站与自助搭建高效解决方案
中山网站制作网页,中山新生登记系统登记流程?
头像制作网站在线制作软件,dw网页背景图像怎么设置?
临沂网站制作公司有哪些,临沂第四中学官网?
济南企业网站制作公司,济南社保单位网上缴费步骤?
导航网站建站方案与优化指南:一站式高效搭建技巧解析
如何做网站制作流程,*游戏网站怎么搭建?
重庆网站制作公司哪家好,重庆中考招生办官方网站?
小程序网站制作需要准备什么资料,如何制作小程序?
如何在阿里云香港服务器快速搭建网站?
网站好制作吗知乎,网站开发好学吗?有什么技巧?
浙江网站制作公司有哪些,浙江栢塑信息技术有限公司定制网站做的怎么样?
如何访问已购建站主机并解决登录问题?
建站中国官网:模板定制+SEO优化+建站流程一站式指南
免费制作统计图的网站有哪些,如何看待现如今年轻人买房难的情况?
七夕网站制作视频,七夕大促活动怎么报名?
网站制作多少钱一个,建一个论坛网站大约需要多少钱?
网页制作模板网站推荐,网页设计海报之类的素材哪里好?
黑客入侵网站服务器的常见手法有哪些?
如何用花生壳三步快速搭建专属网站?
建站之星客服服务时间及联系方式如何?
如何快速打造个性化非模板自助建站?
*请认真填写需求信息,我们会在24小时内与您取得联系。