本文已参与「新人创作礼」活动, 一起开启掘金创作之路。
按照习惯先上代码,然后讲解问题。
疑问
-
递归与迭代的区别?
-
递归有什么优点?
-
为什么递归那么好用,还需要尾递归?
-
递归在Java中的使用问题?
代码
迭代式的阶乘计算
static long factorialIterative(long n){
long r = 1;
for (int i = 1; i <=n ; i++) {
r *= i;
}
return r;
}
递归式的阶乘计算
static long factorialRecursive(long n){
return n == 1 ? 1 : n * factorialRecursive(n-1);
}
基于Stream的阶乘
static long factorialStreams(long n){
return LongStream.rangeClosed(1,n)
.reduce(1,(long a, long b) -> a * b);
}
基于尾递归的阶乘
static long factorialTailRecursive(long n){
return factorialHelper(1, n);
}
static long factorialHelper(long acc, long n){
return n == 1 ? acc : factorialHelper(acc * n, n-1);
}
问题解答
递归与迭代的区别?
迭代是什么?在Java中,迭代可以使用Iterator。并且java中的增强for循环底层使用的就是迭代器。可以想象一下,我们有个指针,一直在内存中找数据,找到一个返回数据并移动到下一个数据上。
递归是什么?我们可以这样理解,递归就是将一个大的问题,分解到小问题去解决(有点类似动态规划),并且在解决问题的过程中调用函数自身,此函数的功能并没有改变,一直解决相同的问题。
递归有递归条件和基线条件,递归条件就是什么时候可以开启递归,基线条件是什么时候可以终止递归。
ArrayList<Object> list = new ArrayList<>();
list.add("h");
list.add("e");
list.add("l");
list.add("l");
list.add("o");
Iterator<Object> iterator = list.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
// 增强for循环
for (Object o : list) {
System.out.println(o);
}
递归有什么优点?
装ABC[1]。
使代码阅读起来轻松愉悦,因为递归写的代码十分优雅简洁。有人说,递归符合思维习惯,我感觉哪个代码写起来都符合思维习惯😀。
为什么递归那么好用,还需要尾递归?
在我们思考问题的时候,去思考为什么是个好习惯。人们使用一个新鲜的事物并坚持长期使用,一定是此事物的某个功能满足了他,甚至的这个功能的有点盖过了缺点。
递归在Java这些语言上存在一个严重问题,如果递归的深度过大,就会出现栈溢出!每一次的递归都会创建的一个栈帧,每个栈帧保存了了一个函数的状态,对于递归而言就是递归函数本身,并且此函数没有运行完,栈帧中保存了该函数的返回地址和局部变量。
尾递归是在返回值上进行计算,避免了栈帧的无限创建。简而言之,尾递归避免了递归带来的栈溢出。但是!在Java中,没有对尾递归进行优化,一样会出现尾递归的栈溢出。😂
递归在Java中的使用问题?
上面说了,递归在Java中存在栈溢出问题,尾递归也可能出现栈溢出问题。所以需要使用递归的时候,可以使用Stream流来避免。
总结
return n == 1 ? 1 : n * factorialRecursive(n-1);
来分析下上面的递归我们可以写成if-else的形式,方便直观的看。
if (n==1){
return 1;
}else{
return n * factorialRecursive(n-1);
}
这里的递归在返回值这里,直接进行函数状态保存的。
下面看看尾递归
return n == 1 ? acc : factorialHelper(acc * n, n-1);
if (n==1){
return 1;
}else{
return factorialHelper(acc * n, n-1);
}
在尾递归中,数值的计算放在了尾部,实际上是已经计算完成的(acc * n),所以不会无限的创建栈帧,避免了栈溢出。
这里说下LongStream.rangeClosed()与LongStream.range()区别,前者是闭区间[a,b],后者是开区间(a,b)。
举个例子,LongStream.rangeClosed(1,5)是1,2,3,4,5,LongStream.range(1,5)是1,2,3,4。