题目描述
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。
首先我们考虑最简单的情况。如果只有1级台阶,那显然只有一种跳法。如果有两级台阶,那就有两种跳法:一种是分两次跳,每次跳1级;另一种就是一次跳2级。
接着我们再谈论一般情况。我们把n级台阶时的跳法看成n的函数,记为f(n)。当n>2 时,第一次跳的时候就有两种不同的选择:一是第一次只跳1 级,此时跳法数目等于后面剩下的n-1 级台阶的跳法数目,即为f(n-1);二是第一次跳2 级,此时跳法数目等于后面剩下的n-2 级台阶的跳法数目,即为f(n-2)。
因此n 级台阶时的不同跳法的总数f(n) = f(n-1) + f(n-2)。
我们把上面的分析用一个公式总结如下:
f(n) = 1 (n=1)
f(n) = 2 (n=2)
f(n) = f(n-1) + (f-2) (n>2)
分析到这里,我们不难看出这实际上是斐波那契数列(Fibonacci )了。
通常我们求解斐波那契数列的方式如下:
第一种方法:
public class Solution {
public static void main(String[] args) {
Long begintime = System.nanoTime();
JumpFloor(10);
Long endtime = System.nanoTime();
System.out.println("用时:"+(endtime-begintime)+"ns");
}
public static int JumpFloor(int target) {
if(target<1){
return 0;
}
if(target==1){
return 1;
}
if(target==2){
return 2;
}
return JumpFloor(target-1)+JumpFloor(target-2);
}
}
程序运行结果:
但是这种解法并不是最优的解决方法。因为这种递归的方式会产生很严重的效率问题。
我们以求解f(10)为例来分析递归的求解过程。向求得f(10),需要先求得f(9)和f(8)。同样,想求得f(9),需要先求得f(8)和f(7)……我们可以用树形结构来表示这种依赖关系,如下图所示:
我们可以发现,在这棵树中有很多节点都是重复的,而且重复的节点数会随着n的增大而急剧增加,这意味着计算量会随着n的增大而急剧增大。事实上,由于斐波那契数列是指数增长,所以该递归算法的时间复杂度也是以n的指数的方式递增的,即O(2^n)。
上述递归算法之所以慢,是因为重复的计算太多,我们只要想办法避免重复计算就行了。比如我们可以把已经得到的数列中间项保存起来,在下次需要计算的时候我们先查找一下,如果前面已经计算过就不用再重复计算了。
我们可以使用HashMap保存中间值。先创建一个哈希表,每次把不同参数的计算结果存入哈希。当遇到相同参数时,再从哈希表里取出,就不用重复计算了。
这种暂存结果的方式有一个很贴切的名字:[备忘录算法]。
第二种方法:备忘录算法的代码如下:
import java.util.HashMap;
public class Solution {
public static void main(String[] args) {
Long begintime = System.nanoTime();
int result = JumpFloor(10);
Long endtime = System.nanoTime();
System.out.println("result="+result+";用时:"+(endtime-begintime)+"ns");
}
public static int JumpFloor(int target) {
HashMap<Integer,Integer> map = new HashMap<Integer,Integer>();
if(target<1){
return 0;
}
if(target==1){
return 1;
}
if(target==2){
return 2;
}
if(map.containsKey(target)){
return map.get(target);
}else{
int value = JumpFloor(target-1)+JumpFloor(target-2);
map.put(target,value);
return value;
}
}
}
程序运行结果:
在以上代码中,集合map是一个备忘录。当每次需要计算F(N)的时候,会首先从map中寻找匹配元素。如果map中存在,就直接返回结果,如果map中不存在,就计算出结果,存入备忘录中。
从F(1)到F(N)共有n个不同的输入,在哈希表立存了n-2个结果,所以该算法的时间复杂度和空间复杂度都是O(n)。
虽然该算法的时间复杂度缩小了,但是却使用了额外的对象HashMap存储数据,提升了空间复杂度,而且HashMap存取数据的过程都需要时间。有没有一种算法,空间复杂度又小,同时又不用占用额外的存储空间呢?
上面的第一种方法是自顶向下计算。更简单的方法是自底向上计算。首先根据f(1)和f(2)算出f(3),再根据f(2)和f(3)算出f(4)……以此类推就可以算出第n项了。这种思路的时间复杂度是O(n),空间复杂度O(1),正好符合我们的需求。
第三种方法代码如下:
public class Solution {
public static void main(String[] args) {
Long begintime = System.nanoTime();
int result = JumpFloor(10);
Long endtime = System.nanoTime();
System.out.println("result="+result+";用时:"+(endtime-begintime)+"ns");
}
public static int JumpFloor(int target) {
if(target<1){
return 0;
}
if(target==1){
return 1;
}
if(target==2){
return 2;
}
int a = 1;
int b = 2;
int temp = 0;
for(int i=3; i<=target; i++){
temp = a + b;
a = b;
b = temp;
}
return temp;
}
}
程序执行结果:
除此之外,还可以使用动态规划解决这个问题。
动态规划中包含三个重要的概念:[最优子结构],[边界],[状态转移公式]。
刚才我们分析出F(10) = F(9) + F(8),因此F(9)和F(8)是F(10)的[最优子结构]。
当只有一级台阶或两级台阶时,我们可以直接得出结果,无需继续简化。我们称F(1)和F(2)是问题的[边界]。如果一个问题没有边界,将永远无法得到有限的结果。
F(n) = F(n-1) + F(n-2)是阶段与阶段之间的[状态转移方程],这是动态规划的核心,决定了问题的每一个阶段和下一个阶段的关系。在动态规划中,通常用数组表示状态转移方程。
分析完问题我们就可以动手写代码了。
第四种方法动态规划解法如下:
public class Solution {
public static void main(String[] args) {
Long begintime = System.nanoTime();
int result = JumpFloor(10);
Long endtime = System.nanoTime();
System.out.println("result="+result+";用时:"+(endtime-begintime)+"ns");
}
public static int JumpFloor(int target) {
//第一次走1台阶,剩下n-1阶,第一次走2台阶,剩下n-2阶
//所以状态转移方程为 dp[i] = dp[i-1] +dp[i-2];
int[] dp = new int[target+1];
dp[0] = dp[1] = 1;
for (int i=2; i<=target; ++i) {
dp[i] = (dp[i-1]+dp[i-2]);
}
return dp[target];
}
}
程序运行结果:
该算法的时间复杂度也是O(n) 。