递归(recursion)
递归指函数体中直接或间接调用自身的一种方法,递归的解决思路通常是把一个大问题转化为许多结构相似的小问题,递归包括三个重要因素:边界条件、前进段、返回段,当边界条件满足时,递归前进(改变参数调用自身),反之则返回。
具体问题一:阶乘
下面用递归来实现阶乘
private int factorial(int n){
if(n < 2)
return 1;
else
return n * factorial(n - 1);
}
这里,边界条件就是n < 2,当n等于0或者1的时候,结果都是1,那为什么不写小于等于1呢?因为那样写执行效率不如小于高。
经过测试,这个函数确实解决了问题,但是考虑函数执行的原理,在执行最后一句时,原函数在执行factorial(n - 1)之前需要把自身的参数压栈,然后等n - 1调用结束后才能出栈释放资源,那么如果这种调用次数很多,就会stackoverflow,有没有办法解决这个问题呢?答案是肯定的,一种是使用迭代,很多时候用递归能够解决的问题,用迭代也能解决,但是代码不如递归好编写好懂,另一种就是在某些语言编译器支持优化尾递归的前提下,可以使用尾递归编写。
把刚才的例子改为尾递归:
private int factorial(int n, int accumulator){
if(n < 2)
return accumulator;
else
return factorial(n - 1, n * accumulator);
}
经过测试,两种方法执行的结果完全一致,但后者在执行最后一句之前,自身的参数已经没有任何用处,此时如果编译器支持优化,完全可以释放自身的资源。
尾递归的特点是将普通递归的结果转为其中的一个参数传递下去,这个参数我通常称它为累加器,如何选取这个累加器的初始值则要根据具体问题分析,比如这个阶乘,始终是相乘的操作,那么初值就应该选1,如果是加减的操作,初值则应该选0。
具体问题二:斐波那契数列(fibonacci array)
嗯,这个巨萌无比的兔子生兔子问题(分裂生殖),我这种极度喜欢兔子的人自然不会放过。上代码,普通递归:
/**
* 这里有必要写个注释
* @param n 数列的第几项(从1开始)
* */
private int fibonacci(int n){
if(n < 1)
throw new IllegalArgumentException("index of an array must be a positive number");
if(n < 3)
return 1;
else
return fibonacci(n - 1) + fibonacci(n - 2);
}
接下来分析尾递归应该怎么写,这个跟阶乘有些区别,前面说过递归其实就是把一个大问题分解成若干个相似的小问题的解决方案,对于阶乘而言,小问题中只依赖n和n - 1,返回值其实是二者相乘,所以改为尾递归时增加了一个累加器参数,而斐波那契数列则是从第三项开始,每一项都是前两项的和,所以需要增加两个参数,下面是具体实现:
/**
* 尾递归计算斐波那契数列第n项
* @param n 要计算第几项(从1开始)
* @param step 加数
* @param accumulator 累加器
* */
private int fibonacci(int n, int step, int accumulator){
if(n < 1)
throw new IllegalArgumentException("index of an array must be a positive number");
if(n < 3)
return accumulator;
else
return fibonacci(n - 1, accumulator, accumulator + step);
}
讲真,这个尾递归以我的智商而言非常难写。。。
斐波那契数列其实是从第三项开始计算,所以要求第n项的值,其实加法计算次数是n - 2,当n等于2的时候就不用算了,这个尾递归写法,参数n其实就是个计数器,并不参与加法运算,实际加法运算是由加数和累加器进行的。
总结
理论上来讲,尾递归更加优雅,在支持尾递归优化的语言中,经过优化的尾递归性能远远超过普通递归,java目前似乎还没有支持尾递归优化,但这是趋势,提前使用这种方法,一旦版本更新,就可能收益。但是,尾递归很多情况下不好理解,也更难编写,这种情况就需要测试驱动,先证实它具有跟普通递归相同的作用,再实际使用。
网友评论