递归算法分析-201805

递归算法

一、定义

    若一个算法直接或间接地调用自己本身,则称这个算法为递归算法。

    例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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值