递归想法和实现介绍,消除递归

谈谈递归的理解


以下为个人理解,可能 对可能不对,有参考价值借点赞
递归专业解释为:是一种调用自身的编程技术。看起来比较简单,不就调用自身嘛,在函数体里调用自己的函数就是递归技术,注意设计递归结束的条件不然将会无限递归导致内存溢出。仅仅这样理解的话除了能看别人写的递归巧妙算法外似乎感觉自己没用过递归,自己不知道什么时候用递归只是发现别人用递归时觉得“美妙原来还可以这样写”,根据别人的思路自己写递归还会绕来绕去自己都被绕晕了,还是copy别人的吧。所以最主要的是理解其中的思想,从深层次的思想出发进行解决问题才能转化为自己的东西。

递归思想:递归实际为一种分而治之的思想( 分治算法),先把大问题转化为小问题,进而对小问题逐个解决合并为原问题的解决,既然能递归解决,那么大问题和划分的小问题解决的模型应该是一样的才能调用自己的函数,称为递归模型,使用递归思想编程者就是在建立这样的模型成为递归模型。递归过程为“”和“”两个过程,先是递即将大的问题通过一定的调用转递给解决问题的更小问题(把大化小的过程),达到一定条件后递结束,到归阶段即将问题逐个解决的过程(先解决小问题进而合并解决大问题)。(递而不归的不属于递归算法,应该属于动态规划啥的。所以一定是一种能递能归的模型)

步骤:既然知道了递归为大化小,小击大这么一个过程,而在编程中又有允许函数调用自身这么一个技术支持,接下来就是使用者的工作,如何把大问题化小从而可以达到以小击大的效果呢?这个过程为递归模型的建立。这样一个模型(即编写的函数)应该有怎样的条件呢?既能把问题一层一层的划分为更小的问题,而且能一层一层的解决小问题合并成原问题的解,划分很多种但是必须同时能合并解决,所以从能把小问题合并为大问题的方面入手,总结出能**“归”并出大问题的解决的办法,这个思维的难点就是要编程者调出原来思考方式的惯性,从另一个角度思考,所以会感觉非常的怪怪的所以就是难点,以往的思考点是,从大问题出发小解决一部分呢然后丢出来更小的问题去解决(这里举例快速排序,快速排序之所很容易就理解和编写,没有感觉绕来绕去的是因为符合我们通常思考的方式,先划分为两部分在划分两部分的基础上再更小部分,当更小部分解决完了就排序完了,没有回归的过程,所以好理解编码也不怎么绕晕),递归解决问题的思考方式是先把大问题化为小问题先解决小问题再在小问题的基础上解决更大一点的问题进而一层层大问题解决**(像递归排序一样,先排序小的部分,再在小部分排序好的基础上排序更大的部分,最后排序整个数列)。这种从大问题逆向思考是比较费劲的分分钟绕晕个人,不过这里提供一个好的数学工具-----数学归纳法

借鉴自《递归与数学归纳》
数学归纳法,想必每一个人都应该学习了数学归纳法,当我们需要去证明一个证明题时,很可能就要用到数学归纳法,数学归纳法的思想如下:
一般地,证明一个与自然数n有关的命题P(n),有如下步骤:
(1)证明当n取第一个值n0时命题成立。n0对于一般数列取值为0或1,但也有特殊情况;
(2)假设当n=k(k≥n0,k为自然数)时命题成立,证明当n=k+1时命题也成立。
综合(1)(2),对一切自然数n(≥n0),命题P(n)都成立。
其实,数学归纳法利用的是递推的原理,形象地可以叫做多米诺原理。因为N+1的成立就可以向前向后递推所有数都成立。
而递归利用的也是递推的原理,在整个程序中,反复实现的将是同一个原理。在这一点上,递归与数学归纳法本质是相同的。数学归纳法就是正向思考递归这种逆向实现的方法,从最小问题出发找解决办法而不是从大问题直接思考,感受到了计算机和数学的关系,计算机就是数学的儿子……所以可以利用数学归纳法来设计递归的实现。
用归纳法设计递归程序的步骤:
一、用数学归纳法分析问题,根据数学归纳法的第一步得出截至部分。
二、根据数学归纳法的第二步来构造函数的递归部分。其实这个求解过程就是找出R(N)与R(N-1)的关系式。

用递归解决汉诺塔问题


假设n个盘子从from借助temp移动to,最上面为第一个
设想一下,如果只有一个盘子的话直接从from移动到to就可以了
如果两个盘子的话先将第一个盘子移动到temp上再将第二个盘子移动to再将temp移动到to上
三个盘子的话…………

从另外一个角度思考,其实每次移动我们都是会先思考如何把当前最上面的移到to上,就是说思考的方式是先移动第一个再移动第二个再第三,然后发现来来去去都是要把这个移到那里的下面先把那里先移动出来再移动回去,然后发现来来去去都是要把这个移走先移动一个小的方一边再移动大的放一边再把小的放到大的上面,再移动大的……发现了重复枯燥的一面,刚刚玩还觉得新鲜,玩着玩着发现来来去去都是那样最多就是盘子越来越多就是重复拉来拉去……而计算就是擅长这种无聊重复的工作。上面说的无论盘子多少规则都是一样的,就是说给了一种递推的可,就说移动第二个是在移走第一个的基础上,把第一个移走了就直接把第二个移动过去,第三个是在把第二个和第一个都移走了就直接移动过去……
每个盘子要移动到目的柱子上,都必须依赖于上面的盘子先全部移动到辅组的柱子上这就是递归的思想,先把大问题化为小问题先解决小问题再在小问题的基础上解决更大一点的问题进而大问题解决。从另外一个方面思考,不是想着从第一个再到第二个……一直移动到最后一个,而是想着要移动最后一个就是最大那个,先把上面的全部移动到temp的柱子上去,再把最大的移动到to上,要把上面的移动到temp的柱子上去,不能直接移动所以只能一个一个移动,先移动最下面借助to移动到temp……直到只有一个时才能直接移动再回归,就是递归的条件。这种思路时很难想到的,因为不属于常规思维,想到就时狠人级别的人物。下面用归纳法分析。

首先用数学归纳法分析:
1、当只移动一个盘子时,我们可以确定唯一动作:直接将圆盘从from移动到to上。
2、现在假设移动n个盘子,而我们也可以将这些圆盘最终按要求移动到to上,当然也可以移动到temp上(只是假设,这是归纳法假设下一步证明)
可以轻易的证明如果n+1个盘子我们也可以将圆盘全部按要求移动到to上,因为我们可以先将上面的N个移动到temp上(第二步已假设成立),再把剩下的最后一个移动到to上,再把temp上的移动到to上(这些都已经假设可以成立)。
这里可能会懵逼这里用归纳法证明了 啥
证明了一个函数可以实现汉诺塔问题,这个函数特征为:
当盘子数量为1时,直接对第1个执行移动move(1,from,to)从from直接移动到to
如果盘子数量不为n时,先将n-1个移动到temp,然后直接对移动第n个执行移动move(n,from,to)从from直接移动到to,再将temp上的n-1个移动到to上。

先将n-1个移动到temp和再将temp上的n-1个移动到to上即是同样的问题,调用自身函数即可。翻译成Java代码如下,如此简洁。。

public void hanoi(int n,String from,String temp,String to){
	if(n==1){
		System.out.println(1+":"+from+"->"+to);
		return;
	}
	hanoi(n-1,from,to,temp);
	System.out.println(n+":"+from+"->"+to);
	hanoi(n-1,temp,from,to);
}

阶乘问题
这个问题如果用循环也很难容易

int n=6;//假设为6的阶乘
int a=1;
while(n>0){
	a=a*n;
	n--;
}

不过这里说一下递归的解法思路,阶乘算法很典型的思考小问题解决合并成大问题的情况,拿到一个数的阶乘很容易就想到直接算从1乘到n解出来,如果解不出来就是一直想办法怎么可以从1乘到n,很难跳出思维的怪圈想到n的阶乘和n-1的阶乘的关系从而用递归解决。这种情况可以试想一下和上一个阶乘的关系用递归。
当是1时直接是1
当为2时为12
当为3时为1
2*3
如果为n的阶乘知道了
那么n+1的阶乘就是再乘以一个n+1就可以,递推方程式就出来了

public int factor(n){
	if(n==1)
		return 1;
	else
		return(n*factor(n-1));
}

消除递归


递归对于分析问题比较有优势,但是基于递归的实现效率就不高了,而且因为函数栈大小的限制,递归的层次也有限制。消除递归,这样可以在分析阶段采用递归思想,而实现阶段采用非递归算法。
递归即先把大问题化为小问题先解决小问题再在小问题的基础上解决更大一点的问题进而一层层大问题解决,如果能保存小问题的解决结果那么就可以不用调用自身解决大问题了,消除递归有的可以通过循环的方式就可以消除,例如上面的阶乘实现,不过一般情况下是不可以的,因为如果可以用循环实现就一般不会用递归了。之所以用递归是因为递归过程可以保存上一步的结果,也就是说在前人的基础之上进行的所以代码才简洁。如果通过其他方式保存上一步保存的结果那么就用不上调用自己的函数了,提高效率,比如上面的阶乘在循环里用一个变量保存上一次循环的计算结果,消除调用自己的函数就是要想办法保存上一步的结果。基本上函数的调用系统上都是基于栈,每次调用都涉及如下操作:
调用开始时:将返回地址和局部变量入栈。
调用结束时:出栈并将返回到入栈时的返回地址
自然的想到我们也可以用栈来实现递归的消除。那么什么时候入栈什么时候出栈,栈保存什么呢,
看一下如果用栈来消除递归的话

//要存储的数据
class Data{
    //方法参数
    private int n;
    Data(int n){
    	this.n=n;
    }
    public int getData(){
    	return n;
    }
}

//栈
Stack<Data> myStack = new Stack<>();
//状态机实现运算过程
    public static int execute(int num){
        int i = 1;   
        int result = 1;
        while(i!=6){       //结束
            switch(i){
                case 1:     //初始化
                    i=2;
                    break;
                case 2:     //条件是否结束
                    if(num==1){
                        result=1;
                        i=4;
                    }else{
                        i=3;
                    }
                    break;
                case 3:     //递归入栈
                    Data data = new Data(num);
                    myStack.push(data);
                    num--;   //条件发生变化
                    i=2;
                    break;
                case 4:     //栈是否空
                    if(myStack.isEmpty()){
                        i=6;
                    }else{
                        i=5;
                    }
                    break;
                case 5:     //回溯出栈
                    Data data1 = myStack.pop();
                    result*=data1.getData();
                    i=4;
                    break;
                }
        }
        return result;
    }

显然模仿函数的调用过程即可,思想还是递归的思想只是实现的方式从调用函数转变为栈的出入和执行的跳转,回答上面的问题,入栈的是当前环境的参数包括变量和执行的位置,出栈后改变环境参数下面用递归思想非调用自身实现汉诺塔问题,标注很清楚可以而且可以直接执行。

import java.util.Stack;

/**
 * 这里用页面来表示每个栈里的数据项
 * 每入栈一次,相当于翻页一次,像书一样好理解
 * 每出栈一次相当于往回翻书一样
 * 当要进入下一页面时,先保存当前页面信息,再进入
 * 出栈即将之前的页面信息弹出,改变当前的参数
 *
 * 这样子想象容易理解而已,其实所谓的翻页就是参数的变化而已
 * 说是页面翻页还不如直接说参数变成啥啥的,只是这样子不好理解
 * 不同的页面表现出来就是参数的不一样,页面唯一表示可以用num即n表示
 *
 * **/
class Hanoi{
	//只是用来记步骤,和实现无关
	 int count=1;

	public static void main(String[]u){
		Hanoi han =new Hanoi();
		//这个是递归实现的,放这里对比错误,看非递归结果是否正确
		han.hanoi1(4,"A","B","C");
		System.out.println("-------------------");
		han.count=0;
		han.hanoi(4,"A","B","C");
	}
	class Data{
		private int num;   //保存页面栈,表示当前处于那个页面
		private String from;  //保存当前页面需要移动的起始
		private String temp;//辅助
		private String to;//目的
		private int location;//当前页面执行到哪个位置
		Data(int n,String from,String temp,String to,int location){
			num=n;
			this.from=from;
			this.temp=temp;
			this.to=to;
			this.location=location;
		}

		public int getNum() {
			return num;
		}

		public String getFrom() {
			return from;
		}

		public String getTemp() {
			return temp;
		}

		public String getTo() {
			return to;
		}

		public int getLocation() {
			return location;
		}
	}

	public void hanoi1(int n,String from,String temp,String to){
		if(n==1){
			System.out.println(count+++"|"+1+":"+from+"->"+to);
			return;
		}
		hanoi1(n-1,from,to,temp);
		System.out.println(count+++"|"+n+":"+from+"->"+to);
		hanoi1(n-1,temp,from,to);
	}
	public void hanoi(int n,String from,String temp,String to){
		//存页面数据的栈
		Stack<Data> myStack1 = new Stack<>();
		int i=1;//用来做状态转变的,即执行什么步骤
		String temp1;
		while(i!=4){
			switch(i){
				case 1:
					if(n==1){
						//当为最后一页时,即只有一个盘时,和递归部分一样
						System.out.println(count+++"||"+n+":"+from+"->"+to);
						i=3;//转到3处,即转到弹出栈,返回前一个页面的地方
					}
					else{
						//否则转到2,继续入栈
						i=2;
					}
					break;
				case 2:
					//将当前页面信息入栈,这里执行到第一个函数
					myStack1.push(new Data(n,from,temp,to,1));
					//当前页面信息入栈后,将页面翻到下一页,(即将改变参数)(相当于递归的hanoi1(n-1,from,to,temp);)
					n--;
					temp1=to;
					to=temp;
					temp=temp1;
					i=1;//然后进入到另外一个页面相当于递归从头执行函数
					break;
				case 3:
					//空栈即为结束
					if (myStack1.isEmpty()){
						i=4;
						break;
					}
					else {
						//往回翻一个页面(弹出页面信息用于修改参数)
						Data data = myStack1.pop();
						//即为之前执行到第一个函数后入栈的相当于递归的执行到hanoi1(n-1,from,to,temp);
						if (data.getLocation()==1) {
							//相当于递归的System.out.println(count+++"|"+n+":"+from+"->"+to);
							System.out.println(count+++"||"+data.getNum() + ":" + data.getFrom() + "->" + data.to);
							//用栈里的信息更新参数(翻页了参数变)
							n=data.getNum();
							from = data.getFrom();
							temp = data.getTemp();
							to=data.getTo();
							//执行到另外的函数,相当于执行到递归里的hanoi1(n-1,temp,from,to);
							//将当前页面信息入栈,这里和上面不一样,因为执行到了不一样的地方
							myStack1.push(new Data(n, from, temp, to,2));
							//当前页面信息入栈后,将页面翻到下一页,(即将改变参数)(相当于递归的hanoi1(n-1,temp,from,to))
							n--;
							temp1 = from;
							from = temp;
							temp = temp1;
							i = 1;//然后进入到另外一个页面相当于递归从头执行函数
						}
						//如果location==2的话说明上次入栈时为执行第二个函数,直接结束当前函数,即再弹出前一个页面的栈
						else{
							i=3;
						}
						break;
					}
			}
		}
	}
}

你可能感兴趣的:(数据结构和算法学习笔记)