【LeetCode】70. 爬樓梯

從本題中咱們能夠學到包含重複子問題,能夠採用記憶化的方式,複用計算後的值;並用動態規劃的思想,找到動態轉移方程,採用循環實現。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階,能夠轉化爲:

  • 若是選擇最後上 1 階到達,則求出上 n - 1 個臺階有多少種方法
  • 若是選擇最後上 2 階到達,則求出上 n - 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

能夠畫個圖具象表示:

image.png

遞歸算法的時間複雜度怎麼計算?用子問題個數乘以解決一個子問題須要的時間。

首先計算子問題個數,即遞歸樹中節點的總數。顯然二叉樹節點總數爲指數級別,因此子問題個數爲 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

在動態規劃中有一個 動態轉移方程 的概念,實際上就是描述問題結構的數學形式:
**
image.png

聽起來很高深,實際上能夠把 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)

後有採用了動態規劃的方式,獲得轉移方程式,採用循環的方式實現。

若是對你有幫助,請關注【前端技能解鎖】:

相關文章
相關標籤/搜索