【力扣】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.pswp.cn/pingmian/94235.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/94235.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据结构 -- 队列

队列的核心定义队列是受限线性表&#xff0c;仅允许在一端&#xff08;队尾&#xff09;插入元素、另一端&#xff08;队头&#xff09;删除元素&#xff0c;遵循 “先进先出&#xff08;FIFO&#xff0c;First In First Out&#xff09;” 原则。队列的结构与操作端队尾&#…

为什么hive在处理数据时,有的累加是半累加数据

在 Hive 处理数据时&#xff0c;“半累加数据” 指的是部分字段保留历史状态、部分字段随业务变化累加或更新的场景&#xff0c;这种模式广泛存在于需要兼顾 “历史追溯” 和 “增量更新” 的业务中。以下是具体例子&#xff0c;帮助理解其本质&#xff1a;例子 1&#xff1a;用…

【贪心算法】day2

&#x1f4dd;前言说明&#xff1a; 本专栏主要记录本人的贪心算法学习以及LeetCode刷题记录&#xff0c;按专题划分每题主要记录&#xff1a;&#xff08;1&#xff09;本人解法 本人屎山代码&#xff1b;&#xff08;2&#xff09;优质解法 优质代码&#xff1b;&#xff…

Spring Boot整合RabbitMQ进阶实战:TTL、死信队列与延迟队列深度解析

Spring Boot整合RabbitMQ进阶实战&#xff1a;TTL、死信队列与延迟队列深度解析 一、TTL机制深度解析&#xff1a;从原理到落地 在RabbitMQ的消息生命周期管理中&#xff0c;TTL&#xff08;Time-To-Live&#xff09; 是核心机制之一——它通过设置消息的"存活时长"&…

最新react,vue 解决无法使用js触发点击,解决方案

const elements document.getElementsByClassName(remove-btn-eIaRy9 select-none semi-dropdown-item);if (elements.length > 0) {const element elements[0];const rect element.getBoundingClientRect();// 模拟鼠标移动到元素上const mouseOverEvent document.crea…

一键部署开源 Coze Studio

文章目录一、简介1、什么是 Coze Studio2、参考地址二、安装部署1、安装docker2、安装git3、下载core4、配置公网可用5、登录成功一、简介 1、什么是 Coze Studio Coze Studio 是一站式 AI Agent 开发工具。提供各类最新大模型和工具、多种开发模式和框架&#xff0c;从开发到…

Python Excel 通用筛选函数

案例目的 第一个函数从指定文件路径读取CSV数据并转换为DataFrame&#xff0c;第二个函数使用灵活的条件筛选DataFrame。 示例数据!&idxMarketCURRPMTERMANT……*1JPUSD10…*1CHINAEUR00…*1USAUSD10…*2JPJPY10…*3USACNY11…*4CHINACNY00…*5JPUSD11…*6JPJPY00…假定数据…

鸿蒙中内存泄漏分析

引言&#xff1a;什么是内存泄漏&#xff1f; 想象一下你的手机是一个酒店&#xff0c;每个应用程序都是酒店的客人。当客人&#xff08;应用程序&#xff09;使用房间&#xff08;内存&#xff09;时&#xff0c;酒店经理&#xff08;系统&#xff09;会分配房间给他们使用。…

将windows 的路径挂载到Ubuntu上进行直接访问

1、下载hane NFS Server安装2、安装后打开3、在电脑上创建个共享文件夹&#xff0c;我这里选择D:\share4、在hane win nfs server 软件上选择Edit\preferences5、选择exports6、选择Edit exports file, 在最后添加D:\share -name:nfs&#xff0c;然后点击Save如果添加root权限使…

开源 python 应用 开发(十一)短语音转文本

最近有个项目需要做视觉自动化处理的工具&#xff0c;最后选用的软件为python&#xff0c;刚好这个机会进行系统学习。短时间学习&#xff0c;需要快速开发&#xff0c;所以记录要点步骤&#xff0c;防止忘记。 链接&#xff1a; 开源 python 应用 开发&#xff08;一&#xf…

【C++闯关笔记】封装②:友元与模板

系列文章目录 第零篇&#xff1a;从C到C入门&#xff1a;C有而C语言没有的基础知识总结-CSDN博客 第一篇&#xff1a;【C闯关笔记】封装①&#xff1a;类与对象-CSDN博客 第二篇&#xff1a;【C闯关笔记】封装②&#xff1a;友元与模板-CSDN博客 第三篇&#xff1a;【C闯关笔…

Python 爬虫教程 | 豆瓣 TOP250 数据抓取与分析实战

一、项目背景与数据价值豆瓣TOP250是影视行业的重要榜单&#xff0c;具有以下数据价值&#xff1a;评分与评价人数&#xff1a;衡量电影市场热度&#xff1b;导演与演员信息&#xff1a;分析人才价值与影视趋势&#xff1b;类型 / 地区 / 年份&#xff1a;洞察电影类型与年代变…

第04章 SPSS简介与数据库构建

参考&#xff1a;SPSS实战与统计思维 - 武松编著 - 微信读书 4.1 SPSS简介 发展历史 全称Statistical Product and Service Solutions&#xff0c;由美国斯坦福大学三位研究生于1968年开发。 对比其他软件成立时间&#xff1a;SAS&#xff08;1976年&#xff09;、Stata&…

【ABAP4】数据字典

ABAP数据字典ABAP数据字典概述数据字典的基本对象域数据元素表类型系统创建自定义透明表创建自定义结构锁对象ABAP数据字典概述 ABAP数据字典是SAP定义和管理数据的工具&#xff0c;包含了程序使用的所有对象&#xff0c;数据字典中包括数据库表、视图、数据类型、域、搜索帮助…

不知道Pycharm怎么安装?Pycharm安装教程(附安装包)

Pycharm安装教程&#xff08;附安装包&#xff09;获取方式&#xff1a;python开发工具包丨夸克网盘-资源免费下载 有位朋友刚开始学习python&#xff0c;不知道Pycharm要怎么安装&#xff0c;于是问我要一个安装教程。 先介绍一下Pycharm吧&#xff0c;PyCharm是一款python开…

在 Docker 容器中查看 Python 版本

博客目录前言方法一&#xff1a;交互式进入容器查看方法二&#xff1a;启动时直接执行命令方法三&#xff1a;启动后使用 exec 执行命令方法四&#xff1a;直接运行并查看版本&#xff08;容器退出&#xff09;方法比较与选择指南实际应用中的注意事项进阶技巧批量检查多个镜像…

React:Umi + React + Ant Design Pro的基础上接入Mock数据

为什么需要Mock数据 前端开发依赖后端接口时的阻塞问题 独立开发和测试的需求 快速迭代和原型验证的重要性 当前版本及框架 React18 Umi 4.0 Ant Design Ant Design Pro 其实这些都不重要&#xff0c;主要是有Umijs&#xff0c;因为Umijs具有开箱即用Mock功能的能力&#…

VMware centos磁盘容量扩容教程

目录前言相关概念磁盘磁盘分区文件系统挂载点物理卷、VG&#xff08;卷组&#xff09;、LV&#xff08;逻辑卷&#xff09;、LVM&#xff08;逻辑卷管理&#xff09;解决方案前言 这篇博客主要分享我在VM中通过docker搭建dify大模型应用平台时&#xff0c;遇到了分配的磁盘容量…

kubernetes中的认证和授权

一 kubernetes API 访问控制Authentication&#xff08;认证&#xff09;认证方式现共有8种&#xff0c;可以启用一种或多种认证方式&#xff0c;只要有一种认证方式通过&#xff0c;就不再进行其它方式的认证。通常启用X509 Client Certs和Service Accout Tokens两种认证方式。…

雅菲奥朗SRE知识墙分享(四):『AI已开始重塑劳动力市场,美国年轻科技从业者首当其冲』

近日&#xff0c;据《商业内幕》报道&#xff0c;AI正在重塑美国就业市场&#xff0c;年轻的科技从业者正首当其冲地感受到冲击。高盛首席经济学家Jan Hatzius在本周一撰文指出&#xff1a;“AI 确实开始在各类数据中显现出更加明显的迹象。”据高盛的分析&#xff0c;科技行业…