這篇文章不是細緻的講述對各個問題怎麼進行遞歸的,我們只討論方法論。
首先提一下能使用遞歸的條件:
下兩點說法來源:https://leetcode-cn.com/explore/orignial/card/recursion-i/256/principle-of-recursion/1101/
- 一個簡單的
基本案例(basic case)
(或一些案例) —— 能夠不使用遞歸來產生答案的終止方案。 - 一組規則,也稱作
遞推關係(recurrence relation)
,可將所有其他情況拆分到基本案例。
即大問題能化解成多個和大問題僅規模不同的小問題,並且規模最小時能直接得出該極小問題的解。
然後提一下記憶化技術:
下一行說法來源:https://leetcode-cn.com/explore/orignial/card/recursion-i/258/memorization/1211/
記憶化 是一種優化技術,主要用於加快計算機程序的速度,方法是存儲昂貴的函數調用的結果,並在相同的輸入再次出現時返回緩存的結果。
由於遞歸方法經常會對子問題重複計算,所以經常和記憶化技術一起使用。
改進條件:當遞歸方法需要會對子問題進行重複計算時。
改進方法:
將計算過的子問題的解存儲起來,再次調用到這個子問題的函數時,直接在存儲結構中查找該解並返回。
一般記憶化技術用hash表存儲,C++中一般使用unordered_set或unordered_map,有時也可以用其它容器來做,看題目需求。
加了記憶化技術的遞歸方法是對普通遞歸方法的一種改進,所以遞歸方法能用記憶化技術時儘量用。
既然講到了遞歸基於記憶化技術的改進,那我們再提一下更進一步的改進“動態規劃”。
改進條件:能直接看出各個子問題的計算順序時使用該改進。
改進方法:動態規劃。動態規劃的內容太多了,這裏不具體細說,只寫一下自己概括的方法論,具體的之後應該會寫一篇動態規劃的。
別人常說的動態規劃三步驟:表示狀態、找出狀態轉移方程、邊界處理。
實際上還有重要的一步:找動態規劃的計算順序,如果很難看出來的,可以用拓撲排序。這也是判斷是否可以用動態規劃的標準,如果不能找到計算順序,那麼就不能用動態規劃,只能用深搜廣搜等遞歸或迭代方法。
對於狀態轉移,如果很難看出來的,可以使用狀態機,通過狀態機還能簡化狀態數:可以利用編譯原理的NFA->DFA的方法。
考慮算法佔用的空間,只要狀態轉移方程中新狀態只涉及固定的可統一表示的有限箇舊狀態的,可以通過循環賦值進行內存優化。
最後,也是寫這篇文章的動力,爲了記錄一下看到的新東西:尾遞歸。
說是新東西,其實以前上課時老師也講到過,只不過當時沒怎麼聽懂。
以下部分內容來自:https://leetcode-cn.com/explore/orignial/card/recursion-i/259/complexity-analysis/1224/
現在呢也沒怎麼搞懂:
只知道尾遞歸是一種優化遞歸調用棧空間的方法,使得可以避免遞歸調用期間棧空間開銷的累積,只使用固定的一小部分棧空間。
只搞懂了怎麼識別一個方法是不是尾遞歸:尾遞歸函數是遞歸函數的一種,其中遞歸調用是遞歸函數中的最後一條指令。並且在函數中應該只有一次遞歸調用。
卻不知道能用尾遞歸的條件是什麼?不知道怎麼把普通遞歸改成尾遞歸?
由於尾遞歸不太能搞懂,並且感覺能尾遞歸的應該能直接動態規劃,感覺尾遞歸用處不大。所以這裏先做一下記錄,以後有時間再弄明白。
以下是記錄的普通遞歸和尾遞歸的java方法。
public class Main {
private static int helper_non_tail_recursion(int start, int [] ls) {
if (start >= ls.length) {
return 0;
}
// not a tail recursion because it does some computation after the recursive call returned.
return ls[start] + helper_non_tail_recursion(start+1, ls);
}
public static int sum_non_tail_recursion(int [] ls) {
if (ls == null || ls.length == 0) {
return 0;
}
return helper_non_tail_recursion(0, ls);
}
//---------------------------------------------
private static int helper_tail_recursion(int start, int [] ls, int acc) {
if (start >= ls.length) {
return acc;
}
// this is a tail recursion because the final instruction is the recursive call.
return helper_tail_recursion(start+1, ls, acc+ls[start]);
}
public static int sum_tail_recursion(int [] ls) {
if (ls == null || ls.length == 0) {
return 0;
}
return helper_tail_recursion(0, ls, 0);
}
}