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

【力扣】2623. 记忆函数——函数转换

【力扣】2623. 记忆函数——函数转换

文章目录

  • 【力扣】2623. 记忆函数——函数转换
    • 一、题目
    • 二、解决方案
      • 1、概述
        • 1.1纯函数
      • 2、在Web开发中的记忆化用途
        • 2.1缓存网站文件
          • (1)React 组件
          • (2)缓存 API 调用
      • 3、算法中的记忆化
      • 4、专业实现的考虑
        • 4.1处理任意输入
        • 4.2内存管理
      • 方法 1:使用 Rest/Spread 语法 + JSON.stringify()
      • 方法 2:使用参数语法
      • 方法 3:基于数字约束进行优化 + Function.apply
      • 方法4:一行代码
      • 5、复杂度分析

一、题目

请你编写一个函数 fn,它接收另一个函数作为输入,并返回该函数的 记忆化 后的结果。

记忆函数 是一个对于相同的输入永远不会被调用两次的函数。相反,它将返回一个缓存值。

你可以假设有 3 个可能的输入函数:sumfibfactorial

  • sum 接收两个整型参数 ab ,并返回 a + b 。假设如果参数 (b, a) 已经缓存了值,其中 a != b,它不能用于参数 (a, b)。例如,如果参数是 (3, 2)(2, 3),则应进行两个单独的调用。
  • fib 接收一个整型参数 n ,如果 n <= 1 则返回 1,否则返回 fib (n - 1) + fib (n - 2)
  • factorial 接收一个整型参数 n ,如果 n <= 1 则返回 1 ,否则返回 factorial(n - 1) * n

示例 1:

输入:
fnName = "sum"
actions = ["call","call","getCallCount","call","getCallCount"]
values = [[2,2],[2,2],[],[1,2],[]]
输出:[4,4,1,3,2]
解释:
const sum = (a, b) => a + b;
const memoizedSum = memoize(sum);
memoizedSum (2, 2);// "call" - 返回 4。sum() 被调用,因为之前没有使用参数 (2, 2) 调用过。
memoizedSum (2, 2);// "call" - 返回 4。没有调用 sum(),因为前面有相同的输入。
// "getCallCount" - 总调用数: 1
memoizedSum(1, 2);// "call" - 返回 3。sum() 被调用,因为之前没有使用参数 (1, 2) 调用过。
// "getCallCount" - 总调用数: 2

示例 2:

输入:
fnName = "factorial"
actions = ["call","call","call","getCallCount","call","getCallCount"]
values = [[2],[3],[2],[],[3],[]]
输出:[2,6,2,2,6,2]
解释:
const factorial = (n) => (n <= 1) ? 1 : (n * factorial(n - 1));
const memoFactorial = memoize(factorial);
memoFactorial(2); // "call" - 返回 2。
memoFactorial(3); // "call" - 返回 6。
memoFactorial(2); // "call" - 返回 2。 没有调用 factorial(),因为前面有相同的输入。
// "getCallCount" -  总调用数:2
memoFactorial(3); // "call" - 返回 6。 没有调用 factorial(),因为前面有相同的输入。
// "getCallCount" -  总调用数:2

示例 3:

输入:
fnName = "fib"
actions = ["call","getCallCount"]
values = [[5],[]]
输出:[8,1]
解释:
fib(5) = 8 // "call"
// "getCallCount" - 总调用数:1

提示:

  • 0 <= a, b <= 105
  • 1 <= n <= 10
  • 1 <= actions.length <= 105
  • actions.length === values.length
  • actions[i] 为 “call” 和 “getCallCount” 中的一个
  • fnName 为 “sum”, “factorial” 和 “fib” 中的一个

二、解决方案

1、概述

此问题要求你编写一个修改所提供函数的函数,使所提供的函数只有在没有传递参数的情况下才会被调用。如果之前已传递过这些参数,它应返回之前的输出而且无需调用所提供的函数。这种类型的优化称为 记忆化,是 高阶函数 的一个极其重要的示例。

为了具体说明记忆化,以下是没有记忆化的一些代码示例。

let callCount = 0;
const add = (a, b) => {callCount += 1;return a + b;
}add(2, 2); // 4
console.log(callCount); // 1
add(2, 2); // 4
console.log(callCount); // 2
add(2, 2); // 4
console.log(callCount); // 3

不出所料,每次调用add时都会增加 callCount

然而,如果我们应用 记忆化

let callCount = 0;
const add = (a, b) => {callCount += 1;return a + b;
};
const memoizedAdd = memoize(add);memoizedAdd(2, 2); // 4
console.log(callCount); // 1
memoizedAdd(2, 2); // 4
console.log(callCount); // 1
memoizedAdd(2, 2); // 4
console.log(callCount); // 1

就可以看到callCount仅在第一次调用memoizedAdd时增加。每次后续传递 (2, 2) 时,记忆化逻辑会检测到这些参数以前已传递,并立即返回缓存的值 4,而不调用 add

避免添加两个数字显然算不上什么巨大的优化,但可以想象如果是对一个复杂的多的函数进行记忆化,会给性能带来多大的提升。

1.1纯函数

值得注意的是,记忆化 仅对 纯函数 有效。纯函数的定义为:给定相同的输入,始终返回相同的输出,并且没有任何副作用的函数。

例如,假设你尝试记忆化不纯的函数 Date.now,它返回自Unix时间戳以来的当前时间(以毫秒为单位)。

const getCurrentTimeMemoized = memoize(Date.now);getCurrentTimeMemoized(); // 1683784131157
getCurrentTimeMemoized(); // 1683784131157
getCurrentTimeMemoized(); // 1683784131157

getCurrentTimeMemoized 第一次调用时会正确返回当前时间。但每次后续调用时,它都会错误地返回和第一次相同的值。

类似的,假设你有一个具有副作用的函数,如将数据上传到数据库。

function uploadRow(row) {// 上传逻辑
}const memoizedUpload = memoize(uploadRows);
memoizedUpload('Some Data'); // 成功上传
memoizedUpload('Some Data'); // 什么都不会发生

第一次调用memoizedUpload时,数据将正确上传到数据库,但每次后续调用都不会再得到新的结果。

事实上,你只能在纯函数上应用此优化,这也是尽可能使函数纯粹的一个很好的理由。

2、在Web开发中的记忆化用途

记忆化有无数的用途,在这里我们讨论其中一些典型案例。

2.1缓存网站文件

大型网站通常由许多 JavaScript 文件组成,在用户访问不同页面时会动态下载这些文件。有时会采用一种模式,其中文件名基于文件内容的哈希值。这样,当 Web 浏览器请求已经在之前请求过的文件名时,它可以从磁盘上本地加载文件,而不必重新下载它。

(1)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发生变化时才会重新渲染,从而提高了应用程序的性能。

(2)缓存 API 调用

假设你有一个函数,用于向API发送网络请求以访问数据库中的键值对。

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

现在,getValueMemoized 将仅为每个键进行一次网络请求,可能大大提高性能。需要注意的是,由于getValue是异步的,它将返回一个 Promise 而不是实际值。对于这种用例,这实际上是最理想的,因为即使在第一次请求返回值之前调用两次,它仍然只会进行一次网络请求。

记忆化网络请求的一个潜在缺点是数据陈旧的风险。如果数据库中与特定键关联的值发生更改,记忆化函数可能仍然返回旧的缓存结果,使用户无法看到更新。

处理这种情况的几种方法:

  1. 始终向API发送请求,询问值是否已更改。
  2. 使用WebSocket订阅数据库中值的更改。
  3. 为值提供 过期时间,以使用户至少不会看到太过时的数据。

你可以在 此处 了解有关 HTTP 缓存的更多信息。

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) 的时间内计算斐波那契数。

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); // 几乎立即解决

我们是否可以只是调用了fib的第一个实现,然后在其上写了memoizedFib = memoize(fib);以获得相同的性能优化?不幸的是,不能。fib 的原始实现引用了自身(未记忆化版本)。因此,如果调用 memoizedFib(100),缓存只会添加一个键(100),仍然需要数年时间才能计算。这是 JavaScript 的一个基本限制(Python 没有此问题)。

4、专业实现的考虑

4.1处理任意输入

之所以仅假设将 3 个具体的函数作为参数传递,都具有数值输入,是有原因的。这是因为数字具有唯一的字符串表示,使缓存逻辑更简单。如果函数可以传递任意输入,你将需要比仅将输入直接转换为字符串更复杂的方法。考虑解决 2630. 记忆函数 II,它需要更通用的方法。

4.2内存管理

由于你可以无限次地调用函数并传递不同的输入,因此可能会耗尽内存。实施一些机制来限制缓存大小非常重要。一种方法是使用Least Recently Used(LRU)缓存。你可以在 2630. 记忆函数 II 的底部相关信息。

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

在 JavaScript 中,你可以使用rest语法访问所有参数作为数组。然后,可以将参数数组展开以将其传递回函数。

由于参数是数字数组(即有效的 JSON),将它们转换为字符串键的便捷方式是使用 JSON.stringify()

  1. 初始化一个用于新的记忆化函数的缓存对象。
  2. 每次调用记忆化函数时,将传递的参数转换为字符串。
  3. 检查键是否已存在于缓存中。如果是,则立即返回关联的值。
  4. 否则,调用提供的函数,将输出放入缓存,并返回输出。
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:使用参数语法

JavaScript 还允许你使用特殊的arguments变量访问传递的参数。

使用arguments变量时需要注意以下几点:

  1. 它不能与箭头函数一起使用,而是引用任何包含非箭头函数的封闭函数。
  2. 虽然arguments类似于数组,但实际上是一个类似 数组的可迭代 对象。像循环遍历它和访问索引一样的操作会按预期工作。但是,调用pushjoin等方法不会起作用。
  3. 在处理可变参数时,通常最佳实践是使用rest语法,而arguments主要用于较旧的代码库。
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

将参数转换为字符串是一个相对昂贵的操作。因为根据问题的约束,永远不会有超过两个参数,参数永远不会大于 100,000,所以我们可以避免将它们转换为字符串。

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

我们还可以使用Function.apply方法调用提供的函数。它的第一个参数是this上下文,我们可以将其设置为 null,因为提供的函数不引用 this。第二个参数是要传递给函数的参数。

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;}
}

方法4:一行代码

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

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

5、复杂度分析

以下分析适用于所有方法。

设 N 为以前调用函数的次数。

  • 时间复杂度:O(1)。每次调用记忆化函数时,只执行一次字典查找。
  • 空间复杂度:O(N)。在最坏的情况下,需要存储之前的所有参数。
http://www.dtcms.com/a/348186.html

相关文章:

  • 硬件抽象层 (HAL, Hardware Abstraction Layer)的简单使用示例
  • 邮箱创建时间打标与自动删除功能设计思路
  • UML时序图中opt,alt,switch-case的问题
  • 用户和组笔记
  • pion/webrtc v4.1.4 版本发布:关键特性与性能优化全面解析
  • 网络协议UDP、TCP
  • maven私服架构
  • Axure RP 9 交互原型设计(Mac 中文)
  • 【实习总结】快速上手Git:关键命令整理
  • 目标检测数据集 第007期-基于yolo标注格式的茶叶病害检测数据集(含免费分享)
  • 深度剖析Spring AI源码(一):蓝图初探,项目结构与设计哲学
  • 【嵌入式开发 Linux 常用命令系列 8 -- git checkout 解冲突详细介绍】
  • 【从零开始学习Redis】如何设计一个秒杀业务
  • [身份验证脚手架] 认证路由 | 认证后端控制器与请求
  • Zabbix 7.0中文乱码矫正
  • 网络协议---TCP
  • 论文阅读:VACE: All-in-One Video Creation and Editing
  • 机器学习算法-朴素贝叶斯
  • k8sday16调度器
  • Java全栈工程师面试实战:从基础到微服务的深度解析
  • 【运维进阶】高可用和负载均衡技术
  • 港口集装箱编号识别误识率↓79%!陌讯多模态融合算法落地优化
  • 静电服漏检率↓79%!陌讯多模态识别算法在智慧安检的实战解析
  • 下料口堵塞误报率↓79%!陌讯多模态融合算法在工业物料输送的实战解析
  • 电子厂静电释放检测误报率↓81%!陌讯多模态融合算法在安全生产监控的落地实践
  • 【Linux】Java线上问题,一分钟日志定位
  • 【C语言强化训练16天】--从基础到进阶的蜕变之旅:Day12
  • lanczos算法的核心——Ritz向量的计算(主要思想为反向映射)
  • 《一次高并发场景下疑难Bug的深度排查与复盘》
  • 基于Langchain框架的DeepSeek-v3+Faiss实现RAG知识问答系统(含完整代码)