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

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

场景

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

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项,因此得以实现获取任意项

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

总结

  • 在处理具有重复计算问题的情况下,闭包和记忆函数是一种有效的优化方式,通过牺牲一定的空间复杂度来换取更优的时间复杂度。
  • 动态规划则是一种更为高效的解决方案,通过迭代的方式自底向上计算,避免了递归中的重复计算,降低了时间复杂度。
  • 在实际问题中,需要根据具体情况选择适合的算法思路,权衡空间与时间复杂度的关系,以达到最佳的性能表现。
相关推荐
დ旧言~17 分钟前
【高阶数据结构】图论
算法·深度优先·广度优先·宽度优先·推荐算法
张彦峰ZYF22 分钟前
投资策略规划最优决策分析
分布式·算法·金融
前端拾光者36 分钟前
利用D3.js实现数据可视化的简单示例
开发语言·javascript·信息可视化
The_Ticker37 分钟前
CFD平台如何接入实时行情源
java·大数据·数据库·人工智能·算法·区块链·软件工程
Json_181790144801 小时前
电商拍立淘按图搜索API接口系列,文档说明参考
前端·数据库
爪哇学长1 小时前
双指针算法详解:原理、应用场景及代码示例
java·数据结构·算法
风尚云网1 小时前
风尚云网前端学习:一个简易前端新手友好的HTML5页面布局与样式设计
前端·css·学习·html·html5·风尚云网
Dola_Pan1 小时前
C语言:数组转换指针的时机
c语言·开发语言·算法
木子02041 小时前
前端VUE项目启动方式
前端·javascript·vue.js
GISer_Jing1 小时前
React核心功能详解(一)
前端·react.js·前端框架