问题描述:
一个台阶,一次可以跳3级或者5级,跳到第n级有多少种跳法。
问题分析:
刚开始的思路是,每次跳3级或者5级,不一定能跳到第n级,要求n是3的倍数,或者是5的倍数,或者是3i和5j的和(i>=0,j>=0)。所以考虑三种情况:
1、 n是3的倍数;
2、 n是5的倍数;
3、 3i+5j=n(i>=0且j>=0)。其他情况则是不可到达第n级。
情况1和情况2很容易,直接求倍数就可以。情况3考虑的是两层for循环,使用蛮力法找到满足条件的i和j的值。后来发现,情况1和情况2不需要在算法中写具体实现,不满足情况1和情况2,不可到达第n级,则跳法为0。
另一种是递归。递归要满足两个条件:1、存在相同的情景,可以重复调用;2、存在最终的结束条件。
最终的结束条件很容易想到,那就是最后只剩下3级台阶或者5级台阶的情况,一次可以跳完,只有一种跳法。相同的情景可以这么考虑,开始跳之前,还剩下n级台阶没有跳过,跳过一次后,比如跳3级或者5级,则剩下n-3或者n-5,再开始第二次跳,发现第二次跳和第一次跳面临的选择是相同的,也是跳3级或者5级,只是要跳的台阶数减少了3级或者5级。那么每次的跳是相同的情景,可以重复调用。如果用函数jumpCount(n)计算跳法的话,n级台阶的跳法即是jumpCount(n),按照递归的思路,如果第一次跳3级的话,则剩下n-3,再跳这n-3级台阶,这n-3级台阶的跳法是jumpCount(n-3)。同理,如果第一次跳5级的话,则剩下n-5级的跳法是jumpCount(n-5);以此递推,每次都是在剩下的台阶数中跳3级或者5级,剩下n-3或者n-5,再重复跳,一直到最后只剩下3级或者5级。那么n级台阶的跳法是jumpCount(n)= jumpCount(n-3)+ jumpCount(n-5)。其实这里发现和第一种思路的第3中情况是类似的。
这里当时有个思想误区,首先确实是考虑到了将剩下的台阶数作为下次跳跃的总数,每次跳跃进行递归的调用。但是受第一种思路的影响,每次都在考虑n-3i或者n-5j的情况,没有想到递归的核心思想是整体与局部的思想,问题分大的模块来看待,每个小模块再一步一步细分。每个大模块和小模块是相似的关系。每个小模块只需要考虑一步,不需要考虑整体情况,所以不需要考虑i和j了。
问题验证:
蛮力法代码:
1 /** 2 * 思路一:非递归,蛮力法找到满足条件的值 3 * @param n 待跳的台阶数 4 * @return 跳法 5 */ 6 public static int jumpCount1(int n) { 7 int count = 0;//跳法 8 //i最大不能超过n的约数,同理j 9 for (int i = 0; i <= n / 3; i++) { 10 for (int j = 0; j <= n / 5; j++) { 11 if (3 * i + 5 * j == n) { 12 count++;//满足条件,则找到了一种跳法 13 } 14 } 15 } 16 17 return count; 18 }
递归代码:
1 /** 2 * 思路二:递归 3 * @param n 待跳的台阶数。每次跳3级或者5级 4 * @return 跳法 5 */ 6 public static int jumpCount(int n) { 7 if (n < 3) { 8 return 0;//如果小于3,则跳不了 9 } 10 11 if (n == 3 || n == 5) { 12 return 1;//只剩下3级或者5级,则只有一种跳法,跳3级或者5级,一次跳完 13 } 14 15 return jumpCount(n - 3) + jumpCount(n - 5);//每次跳跃,剩下为n-3或者n-5,作为下次待跳跃的台阶数 16 }
蛮力法时间复杂度为O(n^2),递归为O(n),递归时间复杂度优于蛮力法。递归算法明显优于蛮力法,简介清晰,但是数据量大的情况下运行效率较低,内存开销大。
总结:
1、 蛮力法很容易想到,思想误区是要跳出判断是否能跳到第n级台阶的情况,这种情况跳法就是0,不需要代码中判断。
2、 递归法要牢牢抓住递归的思想,考虑整体就不要管部分,考虑部分就不要管整体。