本篇是联名训练的第二篇,主题为递归与循环,如果我们需要重复地多次计算相同的问题,通常可以选择用递归或者循环两种不同的方法。递归是在一个函数的内部调用这个函数自身。而循环则是通过设置计算的初始值及终止条件,在一个范围内重复运算。
循环很常见,就是for、while、do while这样子,我们平时使用的也较为频繁,我们重点关注下递归的优缺点:
所以在对性能和时间要求不严格,以及限制递归次数的前提下,使用递归比较好,比较代码简洁清爽。反之,如果有强要求的时候,可以考虑使用循环。从递归的优缺点也可以归纳出递归的三大要素:
《剑指offer》关于递归与循环的算法训练共有4题:斐波那契数列【难度3】、跳台阶【难度3】、变态跳台阶【难度2】,矩形覆盖【难度3】。
大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0,第1项是1)。n<=39
按照第一篇学习过程中的方法来分析,分为如下几个要点:
当然了,最直观的解法当然是直接用递归公式去做:
public class Solution {
public int Fibonacci(int n) {
//设置边界条件
if(n>39||n<0)
return -1;
if(n==0)
return 0;
if(n==1)
return 1;
if(n>1&&n<=39)
return Fibonacci(n-1)+Fibonacci(n-2);
return -1;
}
}
但是提交结果是:
那么该怎么优化呢?不难看出,其实这里做了很多重复的计算,例如f(5)实际上是f(3)+f(4)…一直到f(1)+f(0)。如果能寄存中间值,就不用浪费这些性能了。**如果你使用递归的时候不进行优化,是有非常非常非常多的子问题被重复计算的。因此,使用递归的时候,必要须要考虑有没有重复计算,如果重复计算了,一定要把计算过的状态保存起来。
public class Solution {
public int Fibonacci(int n) {
if(n>39||n<0)
return -1;
if(n==0)
return 0;
if(n==1)
return 1;
int result=0;
int first=0;
int second=1;
for(int i=2;i<=n;i++){
result = first+second;
first=second;
second=result;
}
return result;
}
}
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。
按照第一篇学习过程中的方法来分析,分为如下几个要点:
我们把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)。分析到这里,我们不难看出这实际上就是斐波那契数列了。
public class Solution {
public int JumpFloor(int target) {
if(target<1) return -1;
if(target==1) return 1;
else if(target==2) return 2;
else return JumpFloor(target-1)+JumpFloor(target-2);
}
}
以及低性能损耗的循环版本。
public class Solution {
public int JumpFloor(int target) {
if(target<1) return -1;
if(target==1) return 1;
else if(target==2) return 2;
else{
int result=0;
int first=1;
int second=2;
for(int i=3;i<=target;i++){
result = first+second;
first=second;
second=result;
}
return result;
}
}
}
一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
确实有够变态的,不过按照第一篇学习过程中的方法来分析,分为如下几个要点:
我们把n级台阶时的跳法看成是n的函数,记为f(n)。当n>2时,第一次跳的时候就有两种不同的选择:一是第一次只跳1级,此时跳法数目等于后面剩下的n-1级台阶的跳法数目,即为f(n-1);另外一种选择是第一次跳2级,此时跳法数目等于后面剩下的n-2级台阶的跳法数目,即为f(n-2),最后一种选择是第一次跳3级,此时跳法数目等于后面剩下的n-3。因此n级台阶的不同跳法的总数归纳为f(n)=f(n-1)+f(n-2)+f(n-3)…+f(n-n)=f(0)+f(1)+…f(n-1)=f(n-1)+f(n-1)=2*f(n-1)
这个时候
public class Solution {
public int JumpFloorII(int target) {
if(target<1) return -1;
if(target==1) return 1;
else {
return 2*JumpFloorII(target-1);
}
}
}
以及低性能损耗的循环版本。
public class Solution {
public int JumpFloorII(int target) {
if(target<1) return -1;
if(target==1) return 1;
else {
int result=0;
int last=1;
for(int i=2;i<=target;i++){
result=2*last;
last=result;
}
return result;
}
}
}
我们可以用2X1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2X1的小矩形无重叠地覆盖一个2Xn的大矩形,总共有多少种方法?比如n=3时,2*3的矩形块有3种覆盖方法:
同样按照之前的方式去分析,不过按照第一篇学习过程中的方法来分析,分为如下几个要点:
用第一个1×2小矩形去覆盖大矩形的最左边时有两个选择,竖着放或者横着放。当竖着放的时候,右边还剩下2×2的区域,这种情形下的覆盖方法记为f(2)。接下来考虑横着放的情况。当1×2的小矩形横着放在左上角的时候,左下角必须也横着放一个1×2的小矩形,而在右边还还剩下2×1的区域,这种情形下的覆盖方法记为f(1)
public class Solution {
public int RectCover(int target) {
if(target<1) return 0;
if(target==1) return 1;
else if(target==2) return 2;
else {
return RectCover(target-1)+RectCover(target-2);
}
}
}
以及低性能损耗的循环版本。
public class Solution {
public int RectCover(int target) {
if(target<1) return 0;
if(target==1) return 1;
else if(target==2) return 2;
else {
int result=0;
int first=1;
int second=2;
for(int i=3;i<=target;i++){
result=first+second;
first=second;
second=result;
}
return result;
}
}
}
说实话递归虽然牛逼但还是蛮烧脑的,主要考虑想象能力。估计递归在二叉树上用的比较多吧,不过递归这里也有一些收获:
多练练抽象思维,还是有好处。