10.1.2 使用記憶化快取結果
記憶化(memoization),可以描述為快取函式呼叫的結果,聽起來可能有點複雜,但是,技術非常簡單。正如我們前面提到的那樣,在函式式程式設計中,大多數函式是沒有***的,因此,如果我們用相同的引數值,兩次呼叫同乙個函式,得到的結果相同。
如果我們要得到與上一次相同的結果,為什麼還要麻煩去再一次執行函式呢?相反,我們可以快取這個結果。如果我們把第一次呼叫的結果,以字典方式儲存起來,第二次呼叫時,就不必重新計算值了;可以從字典中讀取結果,馬上返回。清單 10.3 顯示了乙個函式,計算兩個整數的和。
計算 10.3 使用了記憶化的加法(f# interactive)
> open system.collections.generic;;
> let add******(a, b) = [1]
printfn"adding %d + %d" a b <--輸出除錯資訊
a + b
val add****** : int * int –> int
> let add = [2]
let cache = newdictionary<_, _>()
(fun x –> <-- 建立使用快取的函式
matchcache.trygetvalue(x) with |
|true, v –> v |從快取中讀取值
| _-> let v = add******(x) | 或者計算
cache.add(x, v) |
v) |
val add : (int * int -> int)
> add(2,3);; |
adding 2 + 3 |
val it : int = 5 | add****** 只執行一次
> add(2,3);; |
val it : int = 5 |
清單 10.3 的第一部分是通常的加法函式[1],稍許有一點變化,把執行過程記錄在控制台上;如果沒有這一點,我們就不知道在原始版本和記憶化版本之間,有什麼明顯的差異,因為從效率的變化上看,在這個示例中實在是太小了。
實現有快取功能的函式為 add[2];它使用 .net 中的字典(dictionary)型別來儲存快取的結果。快取被宣告為區域性值,在用 lambda 表示式中使用,指定給 add 值。我們使用的模式,與第八章討論使用閉包捕獲可變狀態的相類似。在這裡,cache 值也是可變的(因為,字典是可變的雜湊表),也由閉包捕獲。問題是,對於所有呼叫 add 函式,我們需要使用相同的 cache 值,所以,必須在函式之前宣告,但我們又不希望使它成為全域性值。
函式的最後一部分是 lambda 函式本身。當結果沒有快取時,它才使用 add****** 函式。從f# interactive 會話中可以看到,進行計算的函式,只在第一次呼叫時才執行。
這種方法比尾遞迴的應用更廣泛,沒有任何***1 的函式都適用。因此,在c# 3.0 中也可以成功使用。在下一小節中,我們將使用 c# 3.0,把這段**寫成更通用的版本。
1 這可能有些令人困惑,因為在前面清單中的函式,是有***(輸出到螢幕)的。這是一種「軟***」(soft side effect),可以放心地忽略。核心要求是,結果應該只依賴於傳遞給函式的引數值。
記憶化搜尋
演算法上依然是搜尋的流程,但是搜尋到的一些解用 動態規劃 的那種思想和模式作一些儲存。一般說來,動態規劃總要遍歷所有的狀態,而搜尋可以排除一些無效狀態。更重要的是搜尋還可以剪枝,可能剪去大量不必要的狀態,因此在空間開銷上往往比動態規劃要低很多。記憶化演算法在求解的時候還是按著自頂向下的順序,但是每求...
記憶化搜尋
記憶化搜尋 演算法上依然是搜尋的流程,但是搜尋到的一些解用動態規劃的那種思想和模式作一些儲存。記憶化演算法在求解的時候還是按著自頂向下的順序,但是每求解乙個狀態,就將它的解儲存下來,以後再次遇到這個狀態的時候,就不必重新求解了。例1.題目描述 給從左至右排好隊的小朋友們分糖果,要求 1.每個小朋友都...
記憶化搜尋
原文 感謝作者。一.動態規劃 動態規劃 dynamic programming 與 分治思想 有些相似,都是利用將問題分 為子問題,並通過合併子問題的解來獲得整個問題的解。於 分治 的不同之處在 於,對於乙個相同的子問題動態規劃演算法不會計算第二次,其實現原理是將每乙個計算過的子問題的值儲存在乙個表...