【力扣】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
英文地址,請注明出處:http://en.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;科技行業…