從本題中咱們能夠學到包含重複子問題,能夠採用記憶化的方式,複用計算後的值;並用動態規劃的思想,找到動態轉移方程,採用循環實現。javascript
題目描述:前端
題目:假設咱們須要爬一個樓梯,這個樓梯一共有 N 階,能夠一步跨越 1 個或者 2 個臺階,那麼爬完樓梯一共有多少種方式?java
示例:算法
輸入:2
輸出:2緩存
有2種方式能夠爬樓梯,跳1+1階,跳2階oop
示例:spa
輸入:3
輸出:33d
有3種方法爬樓梯,跳1+1+1階,1+2階,2+1階;code
推理:cdn
1階樓梯,跳1階,有1種方法;
2階樓梯,跳1+1階,跳2階,有2種方法;
3階樓梯,跳1+1+1階,1+2階,2+1階,有3種方法;
4階樓梯,跳1+1+1+1階,1+2+1階,1+1+2階,2+1+1階,2+2階,有5中方法;
若是要跳 n 階臺階,最後一步動做能夠是上1階,也能夠上2階,能夠轉化爲:
以上4階樓梯舉例,選擇最後上 1 階到達,則爲 1 + (1+1+1)階,1 + (2+1)階,1 + (1+2)階,括號中的方法,正好是上 3 階樓梯的方法;選擇最後上 2 階到達,則爲 2 + (1+1)階,2 + (2)階,括號中的方法,正好是上 2 階樓梯的方法。
因此最後上 n 階樓梯能夠得出:
fn(n) = fn(n - 1) + fn(n - 2)
相似是 斐波那契數列 的形式了,能夠用遞歸進行實現。
遞歸實現代碼:
var climbStairs = function(n) { if(n === 1) return 1 if(n === 2) return 2 return climbStairs(n - 1) + climbStairs(n - 2) }; console.log(climbStairs(4)); // 5 console.log(climbStairs(20)); // 10946
能夠畫個圖具象表示:
遞歸算法的時間複雜度怎麼計算?用子問題個數乘以解決一個子問題須要的時間。
首先計算子問題個數,即遞歸樹中節點的總數。顯然二叉樹節點總數爲指數級別,因此子問題個數爲 O(2^n)
而後計算解決一個子問題的時間,在本算法中,沒有循環,只有 f(n - 1) + f(n - 2) 一個加法操做,時間爲 O(1)。
因此,這個算法的時間複雜度爲兩者相乘,即 O(2^n),指數級別,隨着 n 越大,複雜度會愈來愈高。
在以上遞歸過程當中,會有不少重複的計算。例如計算上 5 階梯的方法,則須要計算上 4 階梯的方法,和上 3 階梯的方法;要計算上 4 階梯的方法,則須要計算上 3 階梯的方法,和上 2 階梯的方法。
計算上 3 階梯的方法在第一次計算後,以後又要從新計算,這樣會形成重複計算。
這是一個典型的重疊子問題,怎麼讓重複計算的結果更高效的利用呢?
能夠採用記憶化遞歸的方式,把已經計算好的結果緩存起來,以備遇到已經計算過的數字,能夠直接使用,再也不耗時計算。
記憶化遞歸代碼實現:
const climbStairs = function(n) { const cache = {} // 緩存計算過的值 const loop = (n) => { if(n === 1) return 1 if(n === 2) return 2 if(!cache[n]){ cache[n] = loop(n - 1) + loop(n - 2) } return cache[n] } return loop(n) }; console.log(climbStairs(4)); // 5 console.log(climbStairs(20)); // 10946
從上面看出,定義對象來存儲已計算好的結果,key 值爲上的階梯數,value 值爲階梯計算後的方法數, 每一個階梯只須要計算一次,能夠達到 O(n) 的時間複雜度。
這種方法是 自頂向下 計算,從一個規模較大的原問題 fn(20) 開始,一步步拆分爲愈來愈小的規模計算,直到最後不能拆分爲止的 fn(1),fn(2) 爲止,而後逐層返回結果。
還有一種方式是 自底向上 計算,先從問題最小規模的 fn(1),fn(2) 開始,不斷的擴大規模,直到推導出最終原問題 fn(20) 的值,便獲得最終的結果。
自底向上 屬於動態規劃的思路,可使用循環完成。
動態規劃代碼:
const climbStairs = function(n) { const result = [] result[0] = 0; // 0 階 佔位 result[1] = 1; result[2] = 2; for(let i = 3; i <= n; i++){ result[i] = result[i - 1] + result[i - 2] } return result[n] }; console.log(climbStairs(4)); // 5 console.log(climbStairs(20)); // 10946
在動態規劃中有一個 動態轉移方程 的概念,實際上就是描述問題結構的數學形式:
**
聽起來很高深,實際上能夠把 fn(n) 當作一個狀態,這個狀態是由狀態 fn(n-1) 和 fn(n-2) 相加轉移而來的,經過不斷的循環,轉態不斷的轉移到要求值的 n 上,僅此而已。
上面的代碼還能夠進一步簡化,當前的狀態只和兩個狀態相關,記錄兩個狀態便可以獲得另外一個狀態,在循環過程當中記錄兩個狀態便可。
簡化代碼:
const climbStairs = function(n) { if(n < 1) return 0 if(n === 1 ) return 1 if(n === 2 ) return 2 let current = 2; // 前一個 let prev = 1; // 前前一個 let sum = 0; for(let i = 3; i <= n; i++){ sum = current + prev prev = current current = sum } return current }; console.log(climbStairs(4)); // 5 console.log(climbStairs(20)); // 10946
總結:
以上是對這道題的解析發現,可使用斐波那契額的思路,採用遞歸的方式實現,時間複雜度在 O(2^n),成指數級上升;
又從中看出有重疊子問題,採起記憶化遞歸,將重複計算的值緩存起來,以避免屢次計算,時間複雜度降到了 O(n)。
後有採用了動態規劃的方式,獲得轉移方程式,採用循環的方式實現。
若是對你有幫助,請關注【前端技能解鎖】: