遞歸是一種應用非常廣泛的算法
遞歸需要滿足的三個條件
1. 一個問題的解可以分解爲幾個子問題的解
2. 這個問題與分解之後的子問題,除了數據規模不同,求解思路完全一樣
3. 存在遞歸終止條件
寫遞歸代碼最關鍵的是寫出遞推公式,找到終止條件
示例:
有 n 個臺階,每次你可以跨 1 個臺階或者 2 個臺階,請問走這 n 個臺階有多少種走法?
分析:
n=1時,1種;
n=2時,2種;
n>2時,第一步走1個臺階,則有f(n-1)種;第一步走2個臺階,則有f(n-2)種;
所以 f(n) = f(n-1) + f(n-2)
func f(n int) int {
if n == 1 {
return 1
}
if n == 2 {
return 2
}
return f(n-1) + f(n-2)
}
寫遞歸代碼的關鍵就是找到如何將大問題分解爲小問題的規律,並且基於此寫出遞推公式,然後再推敲終止條件,最後將遞推公式和終止條件翻譯成代碼。
注意點:
1.要警惕堆棧溢出,限制遞歸調用的最大深度.
2.要警惕重複計算。通過一個數據結構(比如散列表)來保存已經求解過的 f(k)。當遞歸調用到 f(k) 時,先看下是否已經求解過了。
例題:
https://leetcode-cn.com/problems/climbing-stairs/
func climbStairs(n int) int {
//方法2
// var tempMap = make(map[int]int)
// return climb2(0, n, tempMap)
//方法1,3,4
return climb4(n)
}
//dp原理:dp[i] = dp[i-1] + dp[i-2]
//方法1 暴力法
func climb1(i, n int) int {
if i == n {
return 1
}
if i > n {
return 0
}
return climb1(i+1, n) + climb1(i+2, n)
}
//方法2 記憶
func climb2(i, n int, tempMap map[int]int) int {
if i == n {
return 1
}
if i > n {
return 0
}
if tempMap[i] == 0 {
tempMap[i] = climb2(i + 1, n, tempMap) + climb2(i + 2, n, tempMap)
}
return tempMap[i]
}
//方法3 動態規劃
func climb3(n int) int {
var tempMap = make(map[int]int)
tempMap[1] = 1
tempMap[2] = 2
for i := 3; i <= n; i++ {
tempMap[i] = tempMap[i-1] + tempMap[i-2]
}
return tempMap[n]
}
//方法4 動態規劃 + 優化
func climb4(n int) int {
if n == 1 {
return 1
}
one, two := 1, 2
for i := 3; i <= n; i++ {
one, two = two, one + two
}
return two
}
因爲遞歸調用一次就會在內存棧中保存一次現場數據,所以在分析遞歸代碼空間複雜度時,需要額外考慮這部分的開銷.