递归算法
一、定义
若一个算法直接或间接地调用自己本身,则称这个算法为递归算法。
例1:阶乘函数递归表示:若n=0,f(n)=1;若n>0,f(n)=n*f(n-1);
public int factorialByRecursion(int n) {
if(n<0) {
throw new IllegalArgumentException("参数非法");
}else if(n==0) {
return 1;
}
return factorialByRecursion(n-1)*n;
}
二、执行过程
在例1中,若要求2的阶乘2!,即求factorialByRecursion(2)[简写f(2)]。
此时的调用过程:首先main函数使用实参n=2调用f(2),而f(2)需要调用到f(1),f(1)需要调用到f(0)。这里得到f(0)=1,再将f(0)的值返回到前次调用f(1)=f(0)*1=1,再返回f(1)的值到f(2)=f(1)*2=2,得到结果f(2)=2;
三、运行时栈
1.非递归函数
非递归函数被调用时,系统需要保存:
a.调用函数的返回地址
b.调用函数的局部变量值
2.递归函数
递归函数被调用时,系统也需要保存以上信息。但递归函数的运行特点,是最后调用的函数最先返回,若按照以上方法保存信息会出错。由于栈的后进先出的特性与递归函数调用返回的过程吻合,一般可以使用栈来保存递归函数调用时的信息。
系统用于保存递归函数调用信息的栈就称为运行时栈。
每一层递归调用所保存的信息构成运行时栈的一条工作记录。每进入下一层递归调用时,系统就新建一条工作记录,并将此条工作记录进栈成为运行时栈的新的栈顶;每返回一层递归调用,就退栈一条工作记录。
因此,栈顶的工作记录必定是当前运行递归函数的工作记录,所以栈顶的工作记录也称活动记录。
当调用递归函数时,除了要保存调用函数的返回地址,还需要保存本次调用函数的实参值、局部变量值和函数返回值(函数名变量的值)。
四、递归算法的设计方法
递归算法的基本思想:是把一个相对复杂的问题,分解成若干个相对简单且类同的子问题,对简单到一定程度的子问题直接求解,从而也得到原问题的解。
适用于递归算法求解的问题的充分必要条件:
a.问题具有某种可借用类同子问题描述的性质;
b.某一有限步子问题(本原问题)有直接的解存在。
设计递归算法的方法:
a.把对原问题的求解设计成包含对子问题求解的形式;
b.设计递归出口。
例:汉诺塔问题
public void move(int n,char from,String buffer,String to) { //from起始位,buffer缓冲位,to目标位
if(n==1) {
System.out.println("将"+from+"中的"+n+"号盘移动至"+to); //n=1,为递归出口
}else {
move(n-1,from,to,buffer); //将上面n-1个移到buffer
System.out.println("将"+from+"中的"+n+"号盘移动至"+to); //将最底下的n号移到to
move(n-1,buffer,from,to); //将n-1个从buffer移到to
}
}
五、递归算法到非递归算法的转换
一般来说存在以下两种情况的递归算法:
1.存在不借助栈的循环结构的非递归算法,如阶乘问题、fibonacci数列、折半查找。
2.存在借助栈的循环结构的非递归算法,如汉诺塔问题。
所有递归算法都可以借助栈转换为循环结构的非递归算法。所有递归算法都可以用树结构表示。
例:用栈模拟汉诺塔问题
class Problem{ //该问题需要保存的信息,将n个盘从from移到to,缓冲区为buffer
int n;
char from;
char buffer;
char to;
public Problem(int n, char from, char buffer, char to) {
super();
this.n = n;
this.from = from;
this.buffer = buffer;
this.to = to;
}
}
- 将原始问题先入栈。
- 每次将栈顶元素出栈,然后解决栈顶元素,拆分为子问题或结果。知道栈内没有元素。如图:
//用栈模拟递归实现汉诺塔
public void moveByStack(int n,char from,char buffer,char to) {
Stack<Problem> stack=new Stack<>();
stack.push(new Problem(n,from,buffer,to)); //原始问题入栈
Problem p;
Stack<Integer> s1 = new Stack<>(); //初始化from位,1-n号自上而下摆放
for(int i=n;i>0;i--) {
s1.push(i);
}
//System.out.println("s1 pop:"+s1.pop());
List<Stack<Integer>> list=new ArrayList<>(); //把 from,buffer,to都先初始化起来,加到list中
list.add(s1);
list.add(new Stack<Integer>());
list.add(new Stack<Integer>());
while(!stack.isEmpty()&&((p=stack.pop())!=null)) { //当栈非空时,弹出栈顶元素,若p.n=1,直接解决该问题
if(p.n==1) { //若p.n!=1,则继续分解该问题
int num=list.get(indexDecode(p.from)).pop(); //这里模拟三个槽位,from位弹出需移动元素
list.get(indexDecode(p.to)).push(num); //到to位入栈
//count2++;
System.out.println("将"+p.from+"中的"+num+"号盘移动至"+p.to);
}else {
stack.push(new Problem(p.n-1,p.buffer,p.from,p.to)); //注意这里逆序入栈,才能使弹出的问题按原序
stack.push(new Problem(1,p.from,p.buffer,p.to));
stack.push(new Problem(p.n-1,p.from,p.to,p.buffer));
}
}
}
//对应的下标与位置
public int indexDecode(char n) {
switch(n) {
case 'A':return 0;
case 'B':return 1;
case 'C':return 2;
default:return -1;
}
}
测试一下两者的速度差异:(这里把以上方法中的输出语句全部注释掉,只计算移动次数)
static long count1=0l;
static long count2=0l;
public static void main(String[] args) {
HanoiTower ht=new HanoiTower();
int n=23;
long t1=System.currentTimeMillis();
ht.move(n, 'A', 'B', 'C');
System.out.println(count1+"次");
long t2=System.currentTimeMillis();
System.out.println("==========================");
ht.moveByStack(n, 'A', 'B', 'C');
System.out.println(count2+"次");
long t3=System.currentTimeMillis();
System.out.println("递归:"+(t2-t1)+"ms");
System.out.println("循环:"+(t3-t2)+"ms");
}
结果:递归比循环快,而且快很多,原因是线程栈要比手动创建的stack性能好太多了。
8388607次
==========================
8388607次
递归:16ms
循环:773ms
所以在递归深度不太大的时候,当没有更简洁的循环算法时,使用递归要比使用栈模拟递归来的好。如果递归深度太大,又没有不使用栈结构的循环算法时,才考虑使用栈模拟递归。
下面测试下递归深度:(win10-64bit jdk 9.0.1 默认设置[1M])
class StackLevel{
public int level=1;
public void levelTest() {
// StringBuilder s=new StringBuilder();
// StringBuilder s1=new StringBuilder();
// List<String> list=new ArrayList<>();
level++;
levelTest();
}
}
public static void main(String[] args) {
StackLevel s=new StackLevel();
//s.hanio(4, "A", "B", "C");
try {
s.levelTest();
} catch (StackOverflowError e) {
// TODO Auto-generated catch block
System.out.println(s.level);
}
}
结果:24466
如果把StackLevel中注掉的三条变量恢复,则结果是6697。原因是每次递归要保存的信息变多了。
参考:数据结构-第6章.递归算法
https://blog.csdn.net/typing_yes_no/article/details/50961559
https://blog.csdn.net/u013254061/article/details/52514440