題目描述
假設你正在爬樓梯。需要 n 階你才能到達樓頂。
每次你可以爬 1 或 2 個臺階。你有多少種不同的方法可以爬到樓頂呢?
注意: 給定 n 是一個正整數。
示例1:
輸入: 2
輸出: 2
解釋: 有兩種方法可以爬到樓頂。
- 1 階 + 1 階
- 2 階
示例2:
輸入: 3
輸出: 3
解釋: 有三種方法可以爬到樓頂。
- 1 階 + 1 階 + 1 階
- 1 階 + 2 階
- 2 階 + 1 階
解題思路
我們先舉例n=5
,當我們邁出第一步的時候,有兩種解法,走1個臺階就剩下了4個臺階;走2個臺階就剩下了3個臺階。那麼我們就可以將剩下的4個臺階繼續按照上述的方法分解。
所以n=5
時,有兩種情況,分解成子問題,走n=4
和n=3
的兩種情況之和。所以可以畫出圖解爲:
但是如果按照上述的想法,計算上述題的時候,就是出現好多重疊的子問題。但是這樣的話會出現時間複雜度過大,不是一個好的算法。
解法一:暴力解法
class Solution {
public int climbStairs(int n) {
// 1. 當n=1時,有一種解法:爬1個臺階
if(n == 1)
return 1;
if(n == 2)
return 2;
// 當n=5時,有兩種解法。
// 先爬1個,計算climb(4)
// 先爬2個,計算climb(3)
return climbStairs(n-1) + climbStairs(n-2);
}
}
這個當我們運行的時候,就會發現當n=44時,該代碼就超出的時間限制,因爲這樣的遞歸做法計算了好多的重疊子問題(當然不同的電腦可能n的最大取值不同)。
解法二:自頂向下備忘錄法(記憶記錄法)
就是使用一個數組,將計算過得n記錄下來,當下一次要使用的時候,去查表,便可知道。這裏就需要一個數組,去保存,這就是使用空間去換取時間的方法。
class Solution {
public int climbStairs(int n) {
int[] array = new int[n + 1];
// 先初始化數組
for(int i = 0; i < array.length; i++){
array[i] = -1; // 作爲標記,以此來判斷是否已經計算過了
}
return climb(n, array);
}
private int climb(int i, int[] arr){
if(i <= 1)
arr[i] = 1;
if(i == 2)
arr[i] = 2;
// 如果數組中的值爲-1,沒有計算,需要計算
if(arr[i] == -1)
arr[i] = climb(i-1, arr) + climb(i-2, arr);
// 走到這裏i個臺階的值已經計算過了直接返回
return arr[i];
}
}
解法三:自底向上的動態規劃法
我們要計算n=5的值,因爲本類型題,是下一個答案依靠於上兩個答案,那我只需要從1開始計算到5就行了呀!
這就是先求解子問題,再由子問題求解父問題。
所以代碼有了如下的改變,還是開闢數組保存前面子問題的解。
class Solution {
public int climbStairs(int n) {
if(n <= 0)
return 0;
if(n == 1)
return 1;
if(n == 2)
return 2;
int[] arr = new int[n+1];
arr[1] = 1;
arr[2] = 2;
for(int i = 3; i <= n; i++){
arr[i] = arr[i-1] + arr[i-2];
}
return arr[n];
}
}
解法四:自底向上解法的優化版
通過上面的解法我們可以知道,要求解父問題,我們可以先求解子問題,然後再由子問題求解父問題。所以我們在求解5的時候,只需要知道4和3就行了,不需要知道2、1,所以當我們消耗數組空間去存1、2時是不需要的步驟,所以我們會想到只用兩個int型的變量去存放父問題的兩個子問題的解就行了,所以上面的解法優化成了下面的解法:
class Solution {
public int climbStairs(int n) {
if(n == 1)
return 1;
if(n == 2)
return 2;
int num_i_2 = 1;
int num_i_1 = 2;
int num_i = 0;
for(int i = 3; i <= n; i++){
num_i = num_i_1 + num_i_2;
num_i_2 = num_i_1;
num_i_1 = num_i;
}
return num_i;
}
}
我們會發現,這種解法與斐波那契數列求解第n項的代碼一樣。