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

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

总结

  • 在处理具有重复计算问题的情况下,闭包和记忆函数是一种有效的优化方式,通过牺牲一定的空间复杂度来换取更优的时间复杂度。
  • 动态规划则是一种更为高效的解决方案,通过迭代的方式自底向上计算,避免了递归中的重复计算,降低了时间复杂度。
  • 在实际问题中,需要根据具体情况选择适合的算法思路,权衡空间与时间复杂度的关系,以达到最佳的性能表现。
相关推荐
闻缺陷则喜何志丹2 分钟前
【C++动态规划 图论】3243. 新增道路查询后的最短距离 I|1567
c++·算法·动态规划·力扣·图论·最短路·路径
还是大剑师兰特19 分钟前
什么是尾调用,使用尾调用有什么好处?
javascript·大剑师·尾调用
Lenyiin21 分钟前
01.02、判定是否互为字符重排
算法·leetcode
m0_7482361127 分钟前
Calcite Web 项目常见问题解决方案
开发语言·前端·rust
鸽鸽程序猿36 分钟前
【算法】【优选算法】宽搜(BFS)中队列的使用
算法·宽度优先·队列
Jackey_Song_Odd36 分钟前
C语言 单向链表反转问题
c语言·数据结构·算法·链表
Watermelo61740 分钟前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
m0_7482489442 分钟前
HTML5系列(11)-- Web 无障碍开发指南
前端·html·html5
乐之者v1 小时前
leetCode43.字符串相乘
java·数据结构·算法
m0_748235611 小时前
从零开始学前端之HTML(三)
前端·html