版本 | 编写时间 | 作者 | 说明 |
1.0 | 2023-07-19 | CodingWithZ | 初版 |
目录
一,什么是递归?
让我们开门见山吧,首先递归到底是什么?实际上递归就是有一个调用自己的方法, 就是这么简单。
二,递归与For循环的区别是什么呢?
(一)共同点:
递归和循环的本质都是代码复用。
递归和循环在理论上具有相同的计算能力。(在可计算性理论中,这两种计算模型所计算的函数类是相同的)
(二)不同点:
1.程序独立性:
递归由程序和系统共同完成。递归需要系统维护一个系统工作栈。
循环由程序单独完成,但是循环需要程序事先设定好循环条件。
2.程序规模:
递归的规模很小,因为受到系统工作栈的限制,这个我们后面会讲到。(递归只能达到4710)
循环的规模很大基本不会受到限制。(循环可以达到2 000 000 000)
3.复用单位:
递归的复用单位是函数。
循环的复用单位是语句(for循环或While循环)
4解题方向:
递归在使用中往往是自顶向下(1 <-- N),将问题的规模互逐步缩小,直到缩小至递归结束,条件成立(N == 1)。
For循环在使用中多用于自下向顶(1 --> N)。
三,JVM堆和栈
在理想状态下,JVM(java虚拟机)没有硬性要求的限制来确定可以创建的最大java对象数量。相反它受到可用内存的限制。
JVM会在运行时为Java对象分配内存,而分配的内存取决于对象的大小和类型。JVM使用堆来管理Java对象,并且通过垃圾回收器自动回收不再使用的对象。
堆(Heap):
Java堆是用于存储对象实例的内存区域。默认情况下,Java堆的大小通常受到系统内存的限制。在一般的桌面应用程序中,默认的堆大小通常为较小的数百MB到1GB。服务器端应用程序或具有大量内存需求的应用程序可能会具有更大的默认堆大小。
栈(Stack):
Java栈用于存储方法调用、局部变量和方法参数等。栈的大小通常是每个线程独立设置的。默认情况下,栈的大小在不同的JVM实现中可能会有所不同。通常情况下,栈的默认大小为几个MB,例如1MB或2MB。
如果要想了解到JVM实现的默认堆和栈的大小,可以参考下面两句命令:
java -XX:+PrintFlagsFinal -version | grep HeapSize
java -XX:+PrintFlagsFinal -version | grep ThreadStackSize
四,递归的正确使用:
了解完JVM的堆和栈到底是什么后,让我们从一个例子来开始学习如何正确使用递归吧,请看下方代码。
public class Test5 {
//主函数
public static void main(String[] args) {
sayHi();
}
//SayHi方法
private static void sayHi(){
System.out.println("hi");
}
}
可以看到,在上面的方法中我们定义了一个方法SayHi(),它的作用就是单纯的输出hi,同时我们也定义了一个主函数,在这个主函数里调用了SayHi()这个方法,这是递归吗?这不是递归,正如我最开始所说的"递归实际上就是自己调用自己",接下来看看下面这个代码吧。
public class Test5 {
//主函数
public static void main(String[] args) {
sayHi();
}
//SayHi方法
private static void sayHi(){
System.out.println("hi");
//自己调用自己
sayHi();
}
}
上面的这段代码,才是真正意义上的递归,因为我们在SayHi()的方法中调用了自己,让我们来看看运行这段代码会发生什么吧。
怎么回事?在我们打印了部分hi后就报错了,那么到底发生了什么呢?跟随我来看看吧,当我们在主函数第一次调用SayHi()的时候,它跳转到SayHi()方法中输出了 hi,但随后他对自己进行递归调用,SayHi()调用SayHi()...... ,就这样该方法开始了无限循环,在这种情况下我们的程序会报出StackOverflowError这个错误,然后停止执行,为什么我们会得到这个错误呢?
如上图所示,当Main方法启动时,java会将Main方法的所有信息放到调用堆栈中,在这个过程中我们首先放入Main()方法,然后Main()方法调用SayHi()方法,在SayHi()方法中我们放入的信息包括,传入的变量引用,方法名,等等,一旦该方法完成,并且在您的程序离开该方法时,它将从调用堆栈中取出该方法的信息,因为它不在需要他了,但是在此我们的递归方法一直在反复调用自身,这就会导致我们一直在往调用堆栈中放入SayHi()的方法,直到我们的调用堆栈放不下。
正确使用递归:
那么正确使用递归,我们需要做的仅仅是让这个方法在某个时刻停止递归调用自己,也就是说,我们需要一个拒绝策略,代码如下。
public class Test5 {
//主函数
public static void main(String[] args) {
//传入为3的值
sayHi(3);
}
//SayHi方法
private static void sayHi(int count){
System.out.println("hi");
if (count <= 1){
return;
}
//自己调用自己,并且将传过来的数值减一
sayHi(count - 1);
}
}
⚪输出结果
hi
hi
hi
输出结果显示SayHi()方法只执行了三次就结束了,这恰恰是因为我们给该方法定义了一个拒绝策略,那就是传入一个计数器,在方法内我们只需要判断我们计数器还剩下几次,每执行一次计数器减一,直到计数器为一时,离开即可。
五,递归有限制为什么我们不选择使用For循环呢?
虽然For循环应用场景更广,效率更高,但这并非代表就没有需要递归的场景,适用递归的场景有:单链表,二叉树,斐波那契数列等,且For循环可能一重,二重,乃至三重循环你能接受 ,但是到了四层循环及以上可能会导致您在开发的过程中遇到困难,所以在开发的过程中如何选择一个更高效更简洁的方法取决于你自己。