当前位置: 首页 > news >正文

力扣 30 天 JavaScript 挑战 第40天 (第十一题)对纯函数和记忆函数有了更深理解

在这里插入图片描述
力扣官方题解

开始答题

/*** @param {Function} fn* @return {Function}*/
function memoize(fn) {let cache = {}return function (...args) {const key = args.toString()if(cache[key] !== undefined) return cache[key]else {const result = fn(...args)cache[key] = resultreturn result}}
}/** * let callCount = 0;* const memoizedFn = memoize(function (a, b) {*	 callCount += 1;*   return a + b;* })* memoizedFn(2, 3) // 5* memoizedFn(2, 3) // 5* console.log(callCount) // 1 */

因为上一道题的官方题解里面有记忆函数的知识点,所以这道题写出来了。

学习官方题解

知识点

1. 记忆函数只对纯函数有效。

纯函数: 相同的输入,会产生相同的输出。不会函数外部世界产生了影响,或者依赖了外部世界的状态
举例子:
例子一:相同的输入,没有产生相同的输出的函数Date.now()

/*** @param {Function} fn* @return {Function}*/
function memoize(fn) {let cache = {}return function() {const key = JSON.stringify(arguments)if (cache[key]!== undefined){return cache[key]}else{const result = fn(...arguments)cache[key] = resultreturn result}}
}
const getCurrentTimeMemoized = memoize(Date.now);console.log("memoized-1:", getCurrentTimeMemoized()); // 真正调用 Date.now()
console.log("normal-1:", Date.now()); // 普通调用setTimeout(() => {console.log("memoized-2:", getCurrentTimeMemoized());console.log("normal-2:", Date.now());
}, 20); // 延时20毫秒

输出为
在这里插入图片描述
从输出可以看见,memoized-1,memoized-2应用了记忆函数,memoized-2的调用结果与memoized-1是相同的。但是Date.now()本来时得到时间戳的函数,不同的时间调用,输出还相同这就没有意义了。所以相同的输入,没有产生相同的输出的函数不能应用记忆函数。
例子二:对函数外部世界产生影响。上传数据(数据库多了数据)。

/*** @param {Function} fn* @return {Function}*/
function memoize(fn) {let cache = {}return function() {const key = JSON.stringify(arguments)if (cache[key]!== undefined){return cache[key]}else{const result = fn(...arguments)cache[key] = resultreturn result}}
}
function uploadRow(row) {// 上传逻辑
}const memoizedUpload = memoize(uploadRows);
memoizedUpload('Some Data'); // 成功上传
memoizedUpload('Some Data'); // 第二次调用,由于有相同的输入,所以什么都不会发生。

第一次调用memoizedUpload时,数据将正确上传到数据库,但每次后续调用都不会再上传数据了。这样写明显是不合理的。所以对函数外部世界产生了影响,或者依赖了外部世界的状态的函数不可以应用记忆函数。

2. 在Web开发中的记忆化用途
  1. 缓存网站文件。
    网站通常是由js文件组成的,访问网站的不同页面时候,会动态下载这些文件。有时会采用一种模式,其中文件名基于文件内容的哈希值。这样,当 Web 浏览器请求已经在之前请求过的文件名时,它可以从磁盘上本地加载文件,而不必重新下载它。
  2. React 组件
    React 是一个非常流行的用于构建用户界面的库,尤其适用于单页面应用程序。其核心原则之一是将应用程序分解为单独的 组件。每个组件负责渲染应用程序HTML的不同部分。
    例如,你可能有一个组件如下:
const TitleComponent = (props) => {return <h1>{props.title}</h1>;
};

上面的函数将在每次父组件渲染时调用,即使 title 没有更改。通过在其上调用 React.memo,可以提高性能,避免不必要的渲染。

const TitleComponent = React.memo((props) => {return <h1>{props.title}</h1>;
});

现在,TitleComponent 只有在 title 发生变化时才会重新渲染,从而提高了应用程序的性能。
3. 缓存 API 调用
假设你有一个函数,用于向API发送网络请求以访问数据库中的键值对。

async function getValue(key) {// 数据库请求逻辑
}
const getValueMemoized = memoize(getValue);

现在,getValueMemoized 将仅为每个键进行一次网络请求,可能大大提高性能。可是我们之前说了,依赖与外部环境的函数不可以用记忆函数,原因是,当数据库发生变化后去请求数据的时候,拿到的还是原来的数据,所以要有额外的操作来确保得到最新的数据。
通常有三种方法。

  • 方法一:始终发出api请求,询问是否值已经发生了变化。
    这里指的是 做一个“条件请求”。
    比如 HTTP 协议里有 ETag 或 Last-Modified 机制:
    请求时带上 “上次拿到的版本号” 或 “修改时间”。
    服务器会返回:
    如果值没变:返回 304 Not Modified,告诉你可以继续用缓存。
    如果值变了:返回新的数据。
    👉 优点:数据总是最新的。
    👉 缺点:每次还是要发请求,只是有时能节省响应体流量。
  • 方法二:使用 WebSocket 订阅数据库中值的更改
    思路:让服务器主动“推送变化”。
    例如:
    前端通过 WebSocket 订阅 “user:123” 的值。
    数据库一旦更新了 “user:123”,服务器就立刻通过 WebSocket 通知前端。
    前端收到消息后,更新缓存里的数据。
    👉 优点:数据实时、同步更新。
    👉 缺点:要额外维护 WebSocket 连接,系统复杂度增加。
  • 方法三:为值提供过期时间
    即缓存里加一个“有效期”。
    例如:
    第一次请求 “user:123” 时缓存下来,并记录“5分钟后过期”。
    5分钟内重复请求,直接用缓存。
    超过 5分钟,再请求时就重新向 API 获取新值,并刷新缓存。
    👉 优点:实现简单,减少频繁请求。
    👉 缺点:在过期时间内,数据可能仍然是旧的。
3. 算法中的记忆化

记忆化的一个经典应用是动态规划,动态规划是将一个问题分解成若干个小问题,这些小问题可以为函数的调用,当一个函数被多次调用且有相同输入的时候,使用记忆函数可以提高性能。斐波那契函数是一个很好的例子。

function fib(n) {if (n <= 1) return n;return fib(n - 1) + fib(n - 2);
}
fib(100); // 耗时多年

上面的代码非常低效,时间复杂度为 O(1.6 的n次方)(1.6是黄金比率)。时间复杂度为啥是它我是真的没有搞明白,太难了。

但是,通过不再使用相同的输入两次调用 fib,我们可以在 O(n) 的时间内计算斐波那契数。这个原因我知道,通过记忆化,每个 fib(k)(0≤k≤n)只会被实际计算一次,总计算量是 O (n) 级别的,因此时间复杂度优化到了 O (n)。

const cache = new Map();
function fib(n) {if (n <= 1) return n;if (cache.has(n)) {return cache.get(n);}const result = fib(n - 1) + fib(n - 2);cache.set(n, result);return result;
}
fib(100); // 几乎立即解决

这里还会有一个问题,为什么这个斐波那契记忆化的写法不行上述写个memorize()函数,调用memorize再返回一个函数来实现,就像下面的写法。

// 通用记忆化工具:包装一个函数,让其结果被缓存
function memoize(fn) {const cache = new Map();return function(n) {if (cache.has(n)) return cache.get(n);const result = fn(n); // 调用原始函数cache.set(n, result);return result;};
}// 原始的、不带缓存的fib
function fib(n) {if (n <= 1) return n;// 关键:这里调用的fib是"原始的、不带缓存的fib"return fib(n - 1) + fib(n - 2); 
}// 用memoize包装原始fib,得到"记忆化版本"
const memoizedFib = memoize(fib);

此时如果调用memoizedFib(100),会发生什么?

  1. memoizedFib(100)会先查自己的缓存(空),然后调用fib(100)(原始函数)。
  2. 原始fib(100)会递归调用fib(99)和fib(98)—— 但注意:这里的fib是原始的、不带缓存的 fib(不是memoizedFib)。
  3. 因此,fib(99)和fib(98)的计算完全没有缓存,会继续递归调用更小编号的fib(都是原始版本),导致所有子问题重复计算。
  4. 最终,memoizedFib的缓存里只会存一个键(100),但所有子问题(99、98、…、2)都没有被缓存,效率和原始版本一样低(O (1.6ⁿ))。
    核心原因:函数的 “自我引用” 无法被外部包装修改
    原始fib函数内部的递归调用写的是fib(n-1),这个fib指向的是定义时的原始函数,以为没有给他套一层memorizedFib函数。
    就像:你给一个人(原始fib)穿了件外套(memoizedFib),但这个人做事时(递归调用),还是用自己的手(原始fib),而不是外套的手(memoizedFib)。外套只能管最外层的调用,管不了里面的动作。

学习官方的解题方法

方法 1:使用 Rest/Spread 语法 + JSON.stringify()

function memoize(fn) {const cache = {};return function(...args) {const key = JSON.stringify(args);if (key in cache) {return cache[key];}const functionOutput = fn(...args);cache[key] = functionOutput;return functionOutput;}
}

方法 2:使用参数语法

function memoize(fn) {const cache = {};return function() {// 将参数转换为字符串let key = '';for (const arg of arguments) {key += ',' + arg;}if (key in cache) {return cache[key];}const functionOutput = fn(...arguments);cache[key] = functionOutput;return functionOutput;}
}

方法 3:基于数字约束进行优化 + Function.apply

假设你有两个数字 a 和 b,a,b不会大于 100,000,并且希望将它们转换为一个唯一的数字,使得没有其他值的a和b映射到相同的数字。你可以使用公式 key = a + (b * 100001)。

function memoize(fn) {const cache = new Map();return function() {let key = arguments[0];if (arguments[1]) {key += arguments[1] * 100001;}const result = cache.get(key);if (result !== undefined) {return result;}const functionOutput = fn.apply(null, arguments);cache.set(key, functionOutput);return functionOutput;}
}

这里的fn.apply也可以不同,因为fn函数并不需要使用this,用了也不会出错。

方法四:一行代码

为了展示 JavaScript 提供的一些语法,以下是一种一行代码的解决方案。让我们看看代码的不同部分,以了解它是如何工作的。

  1. var memoize = (fn, cache = {}) => (…args) => 定义了 memoize 函数,它接受两个参数:一个函数 fn 和一个可选的缓存对象 cache。由于永远不会传递第二个参数,cache将始终设置为一个空对象 {}。
  2. memoize 函数继续返回另一个函数,它接受任意数量的参数。
  3. ?? 这是 Nullish 合并运算符。仅当左侧的第一个操作数不为 null 或 undefined 时,它才会返回左侧的第一个操作数。否则,它将返回右侧的第二个操作数。
  4. cache[args.join()] 将参数转换为逗号分隔的字符串,并返回与该键关联的值。如果值不存在,则返回 undefined(导致函数返回右侧的值)。
  5. (cache[args.join()] = fn(…args)) 将缓存中的键设置为提供的函数的输出。然后返回该值。如果存在缓存未命中,将执行此代码。
var memoize = (fn, cache = {}) => (...args) => cache[args.join()] ?? (cache[args.join()] = fn(...args))

作者:力扣官方题解
链接:https://leetcode.cn/problems/memoize/solutions/2505885/ji-yi-han-shu-by-leetcode-solution-jtop/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
作者:力扣官方题解
链接:https://leetcode.cn/problems/memoize/solutions/2505885/ji-yi-han-shu-by-leetcode-solution-jtop/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

http://www.dtcms.com/a/352353.html

相关文章:

  • ABC420A-E题解
  • Zynq开发实践(FPGA之verilog仿真)
  • leetcode算法刷题的第十八天
  • 【世纪龙科技】职业院校汽车专业职业体验中心建设方案
  • 面试题随笔
  • 微服务-25.网关登录校验-网关传递用户到微服务
  • 微服务的编程测评系统16-用户答题
  • 【typenum】30 类型级别的取负(Neg)
  • `mmap` 系统调用详解
  • 设备驱动程序 day62
  • 变压器副边电流计算
  • es-toolkit 是一个现代的 JavaScript 实用库
  • 15公里图传模组:为远程飞行赋能,突破极限的无线连接新选择
  • 微服务-28.配置管理-共享配置
  • 微服务-26.网关登录校验-OpenFeign传递用户信息
  • 前端RSA加密库优缺点总结
  • 42_基于深度学习的非机动车头盔佩戴检测系统(yolo11、yolov8、yolov5+UI界面+Python项目源码+模型+标注好的数据集)
  • Python内存模型与对象系统深度解析
  • 使用Kiro智能开发PYTHON应用程序
  • 25072班8.26日数据结构作业
  • 【CFA三级笔记】资产配置:第一章 资本市场预期(宏观分析)
  • ansible的一些重要配置文件
  • 基于 LQG 控制的轨迹跟踪 —— 从原理到实践
  • 游隼可视化项目
  • python删除执行目录
  • 服装行业/服饰品牌OMS订单管理系统:全渠道零售时代的数字化中枢|商派
  • Chrome您的连接不是私密连接怎么办?试下手敲 thisisunsafe
  • Kafka 生态选型地图、最佳实践与落地清单
  • SELinux相关介绍
  • Android 属性 property 系统