递归和调用栈
本节学习如何将问题分为基线条件和递归条件
递归体现在代码中就是调用自己,除非符合跳出条件结束程序或返回
基线条件和递归条件
举例,代码实现简单倒计时
private static void countdown(int i) throws InterruptedException{
System.out.println(i);
if (i <= 0) {
//基线条件
return;
}else{
//递归条件
Thread.sleep(1000);
countdown(--i);
}
}
每个正常的递归函数都有两个部分:基线条件(base case)和递归条件(recursive case)。
基线条件是让函数不再调用自己,从而避免无限循环的条件。
递归条件是让函数继续调用自己的条件。
栈(stack)
栈的基本概念
递归涉及一个叫做**调用栈(call stack)
的非常重要的概念**。
栈是常用的数据结构,只涉及压入、弹出两种操作。进出顺序是先进后出。栈分为栈底和栈顶,栈底无法进出元素,只有栈顶能进出,就像一个弹匣一样,先压入的子弹后打出,最后压入的子弹最先打出。
调用栈
在有些地方也叫调用堆栈,debug打断点时常常会见到这个东西。
调用栈用于存放以下东西:
- 函数结束后应当返回的地址
- 本地变量:子程序的变量可存入调用栈,以达到不同子程序间变量分离
- 参数传递:如果寄存器不足以容纳子程序的参数,可以在调用栈上存入参数。
- 环境传递:有些语言(如Pascal与Ada)支持“多层子程序”,即子程序中可以利用主程序的本地变量。这些变量可以通过调用栈传入子程序。
假设有函数如下
public class CallStackDemo {
public static void main(String[] args) {
int i = 0;
i = calFirst(i);
System.out.println(i);
}
public static int calFirst(int i){
i = calTwice(i);
return i += 1;
}
public static int calTwice(int i){
i = calThird(i);
return i += 2;
}
public static int calThird(int i){
return i += 3;
}
}
上述代码在可见内共有五个函数,分别是
-
main
函数 -
calFirst()
函数 -
calTwice()
函数 -
calThird()
函数 -
System.out.println()
函数
当调用main
函数时,计算机为该函数分配一块内存,并保存main
函数中的变量到该内存中。
调用calFirst()
函数时也是一样,计算机为该函数分配一块内存,并保存其中的变量 i
。
调用calTwice()
函数时也是一样,计算机为该函数分配一块内存,并保存其中的变量 i
。
……
计算机使用一个栈保存这些内存块,其中第二个内存块位于第一个内存块上,函数调用完毕返回后,内存块弹出,如下图变化
calThird()
是最先被执行,执行完毕后,calThird
内存块被弹出,等待内存回收,程序返回到calTwice
,直到calFirst
被执行完毕,返回到main函数中。
此时在栈顶,也就是main函数的顶部添加了System.out.println
函数所属的内存块,执行完打印函数后返回到main函数,随即main函数也执行完毕,等待内存回收。
这个栈用于保存多个函数的内存块等,体现调用关系,叫做调用栈。
在函数A中调用另一个函数时,函数A暂停并处于未完成状态,改函数所有变量值都还在内存中。
总结
栈的每一个内存块一定要保存自己被弹出时需要返回到的内存地址。
先加载完成的内存块所属的子程序会最后执行,最后加载最先执行。
每个递归函数都有基线条件和递归条件两个条件。
所有函数的调用都要进入调用栈。
调用栈很长时,如递归调用栈,将占用极大的内存,此时选择有如下两个:
- 重新编写代码,转而使用循环等方式。
- 使用尾递归。这是一个高级递归。