JS闭包应用:通过记忆函数优化递归

对于闭包,上下文执行对象,还是不了解的小伙伴可以去看我之前关于闭包的文章: js中闭包是个啥?带你揭开闭包神秘面纱, 小白请签收:6分钟搞懂js预编译机制指南

场景

LeetCode509题:斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:

scss 复制代码
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1

给定 n ,请计算 F(n) 从第三项开始每一项都依赖于前两项的结果,我们只要知道前面项的值,也就能知道后面项的值,所以我们将从上往下拆分使用递归来解决

js 复制代码
var count = 0
var fibonacci = function(n){
    count++
    return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
}
// 计算执行时间和执行次数
console.time('fibonacci');
fibonacci(31);  
console.log('函数一共执行次数',count);
console.timeEnd('fibonacci');

但是递归能够将庞大的计算分解为一个一个的简单计算但是同样也带来了问题 执行结果:

我们需要的只是该数列的第31项就需要计算435万多次,这么多的函数执行上下文对象不断的进栈,直到最后一个函数的执行结果获取之后才出栈,占用大量的内存空间,很容易造成堆栈溢出(stack overflow)问题。

优化

其实在递归斐波那契数列的过程中获取某一项时,一直都存在重复计算前面的元素值现象,重复调用重复计算,这样就浪费了很多的空间时间。如果我们使用一个哈希表将函数的运行结果存起来,下次再遇到我们就直接读取值而不是再用递归去计算,就节省了时间,也免去了多余的函数入栈挤占空间。

以第6项为例子,我们可以看到后面很多项重复的计算,如果我们将每一项的值都使用哈希表存起来,之后重复出现就直接读取,而不需要再次递归,这样就达到优化效果,我们利用js中闭包特性构建一个记忆函数来优化

记忆函数

js 复制代码
    function memorize(f) {
        var cache = {} // 定义一个哈希表存储执行结果,空间换时间,读取值时间复杂度为o(1)   
        //闭包
        return function() {
            var key = arguments.length + 
                Array.prototype.join.call(arguments, ",")
        //判断该键是否存在于cache中
            if (key in cache) {
            //存在则返回该值
                return cache[key]
            } else {
            //不存在则执行该函数并将结果存入哈希表中
                return cache[key] = f.apply(this, arguments)
                 
            }
        }
    }

我们通过闭包的方法来实现,使用闭包的好处

  1. 保护内部状态: cache变量是memorize函数内部的局部变量,不会被外部访问到,从而有效地隐藏了内部状态。这有助于防止外部代码直接修改或访问cache,确保其状态的封装性。
  2. 维持状态持久性: 闭包使得内部状态(如cache)在多次调用之间得以保留。这意味着,即使memorize函数执行完毕后,其内部状态仍然存在,供后续调用使用。这对于记忆化函数非常有用,因为它可以在不同调用之间共享已经计算的结果,避免重复计算。
  3. 动态创建函数: 闭包允许在运行时动态创建函数,并且这些函数可以捕获和保留其所在作用域的状态。这种动态性对于实现记忆化函数非常方便,因为它可以根据需要创建具有不同缓存状态的记忆化函数。

在这里,memorize返回的闭包函数具有访问并更新cache的权限,从而实现了对斐波那契函数调用结果的记忆和复用。

以下这段代码通过,传入参数的长度,和传入参数,拼接形成的字符串完成唯一的键

js 复制代码
  var key = arguments.length + 
                Array.prototype.join.call(arguments, ",")

实现

js 复制代码
var fib = function(n) {

    var fibonacci = function(n) {
       
        return n < 2? n : fibonacci(n - 1) + fibonacci(n - 2); 
    }
    
    function memorize(f) {
        // if (typeof f !== 'function') return;
        var cache = {} // 空间换时间 自由变量   
        return function() {
            var key = arguments.length + 
                Array.prototype.join.call(arguments, ",")
            // add 
            if (key in cache) {
                return cache[key]
            } else {
                return cache[key] = f.apply(this, arguments)
                 
            }
        }
    }
    fibonacci = memorize(fibonacci)
    return  fibonacci(n)

    };

优化前后对比

递归 :代码只有一行好优雅,但是耗时长 记忆函数优化

其它解法动态规划

js 复制代码
function fibonacci(n){
let a = 1
let b = 1
for(let i=2;i<n;i++){
    // let temp
    // temp =b
    // b =a+b
    // a = temp
    //es6解构写法省去temp
    [a,b] = [b,a+b]
}
return b
}

因为从第三项开始每项都是前面两项之和,我们定义两个变量a,b,表示数列当中的一前一后的元素,每次循环,a+b的和为下一个元素,把b的值赋值给a把a+b的值赋值给b;而循环是从i=3开始,保证a+b的和为数列第i项,因此得以实现获取任意项

自下而上不断累加得到结果,也没有重复计算,所以运行时间上也很快

总结

  • 在处理具有重复计算问题的情况下,闭包和记忆函数是一种有效的优化方式,通过牺牲一定的空间复杂度来换取更优的时间复杂度。
  • 动态规划则是一种更为高效的解决方案,通过迭代的方式自底向上计算,避免了递归中的重复计算,降低了时间复杂度。
  • 在实际问题中,需要根据具体情况选择适合的算法思路,权衡空间与时间复杂度的关系,以达到最佳的性能表现。
相关推荐
小码哥_常3 小时前
一套自己部署的 IM,客户端是同一份 Flutter
前端·后端
蜗牛攻城狮6 小时前
微前端四大方案选型对比与最佳实践:qiankun、micro-app、wujie、Module Federation
前端·微前端·qiankun·前端架构·micro-app·wujie
她说可以呀7 小时前
Vue插槽
前端·javascript·vue.js
qq_448011167 小时前
Vue + Jinja2两套模版与法冲突造成undefinedError
前端·javascript·vue.js
aixingpan8 小时前
aixingpan.cn API开发文档:前端查询夏令时和时区接口指南
前端·php
2601_958352908 小时前
23.5×19mm 换掉一整条音频链?|A-68 集成了什么:ADC / DAC / 24.576MHz 时钟 / AEC / 波束降噪,主控仅需 6 根线
前端·音视频·ai降噪·回声消除·语音模块
UIU1148 小时前
P1233 [ICPC 2001 Taejon R] 木棍加工
数据结构·c++·算法·动态规划
论文复现现场9 小时前
WRF、FEA 能用 L40S 加速吗?求解器检查、断点恢复与长期运行
java·数据库·算法·wrf·有限元分析·gpu计算·断点续算
ZhengEnCi9 小时前
用 AI 写代码为什么拼到最后还是拼人:算力瓶颈与领域专家提示词的本质差异
算法·架构
志尊宝9 小时前
AI原生前端开发(039):AI辅助的Nuxt 4全栈开发
前端·vue.js·ai-native