关于递归算法设计的思考

前言

以前公司管理系统项目须要配合前端VUE实现动态路由,返回的数据结构是一个树形结构,但在数据库中存储的是平行的数据项。这个时候就须要在代码中去进行数据结构的组装。很久没思考这些问题,花了好些时间才搞定。这其中涉及到递归算法的实现,乘着周末深刻的思考了下递归算法的设计,一点点拙见写下来作个笔记。前端

什么是递归?

对于递归其实咱们并不陌生。还记得中学时代常用的数学概括法?,递归其实并非计算机科学独有的概念。实际上数学概括法才是递归的理论基础。node

先举个简单的栗子:求n! 这里使用递归来求解。算法

int foo(int n) {
	if(n != 1) {
	return n*foo(n-1);
	}
	return 1;
	}

复制代码

栈.svg

上面代码所表达的含义是:当n不等于1的时候。一直去调用foo(n-1)而且和传入的n相乘。在这里解释下递归调用的实现原理:咱们知道计算机中函数的调用是使用栈的数据结构实现的,好比传入的n5时,第一次执行foo方法,当执行到 foo(n-1),方法foo会被压入栈中。其实递归方法的出栈入栈和普通方法是同样的。惟一的区别是递归调用的是自身的代码。这里不妨把foo的每次调用称做foo1,foo2等等。那么这样就好理解了,当foo方法执行到n==1的时候,栈里面依次存放着foo1,foo2,foo3,foo4,foo5。此时foo5在栈顶,出栈执行。foo5执行的时候n==1,由代码可知,n==1,方法直接返回1。接着foo4出栈,传入的n2,运算2*1,而后foo3,foo2,foo1依次出栈,运算依次为:3*2,4*6 当最后一个方法foo1 return时计算,5*24,运行结束。数据库

从这个过程当中不难看出,递归是个不断降低的过程。层层调用自身,而后求值又是个上升的过程。从递归结束的出口返回值开始,层层向上求值。bash

从这个例子中还能够看出递归算法的两个重要特征,好比:数据结构

  1. 不停调用自身
  2. 有终止的条件,否则就成了死循环了

须要思考的问题是终止条件是如何肯定的,以及他是怎么变化的。在上面的程序中是作-1操做。svg

什么时候考虑使用递归呢?

当定义是递归时

和上面求阶乘的例子类型同样。斐波那契数列的定义就是递归的。直接看代码:函数

int Fib(int n) {
	if(n==1 || n==2) {
	return 1;
	}else {
	Fib(n-1)+Fib(n-2)
	}
}

复制代码

斐波那契数列1,1,2,3,5 ...学习

当数据结构自己是递归时

最多见的好比链表的定义(这里讨论的是没有头节点的链表):ui

class Node {
	private String data; //节点数据域
	private Node next; //指向下一个节点
}
复制代码

当须要对链表数据域求和时,可使用递归实现:

int SUM(Node node){
	if(node == null) {
		return 0;
	}else {
		return node.getData()+SUM(node.getNext());
	}
}

复制代码

当问题须要用递归求解

这里详细讨论下汉诺塔算法的实现。

汉诺塔问题描述:有三个分别叫作X,Y,Z的塔座。在塔座X上有直径各不一样,从小到大依次标号为:1,2,3...n的盘片,如今要求把塔座X上的盘片移动到塔座Z上,按相同顺序叠放。移动时须要遵照规则:1.每次只能移动一个盘片。 2.盘片能够放在任意一个塔座上。 3.不能将较大的盘片放在较小的上面。

汉诺塔是典型的递归求解问题。光看描述不容易分析问题如何分解,不如在问题规模较小时,试着分析下解决方案。

当X塔座上只有一个盘片时:

2017-12-16 18.59.15.png

X==>Z

当X塔座上有两个盘片时:

2017-12-16 19.06.34.png

X1==>Y,X2==>Z

2017-12-16 19.07.10.png

Y1==>Z

2017-12-16 19.07.28.png

当X塔座上有三个盘片时:

2017-12-16 19.14.59.png

X1==>Z,X2==>Y

2017-12-16 19.16.36.png

Z==>Y,X==>Z

2017-12-16 19.17.01.png

Y1==>X,Y2==>Z

2017-12-16 19.17.33.png

X==>Z

2017-12-16 19.17.55.png

先给出递归算法,这个算法将打印出移动的步骤。

void Hanoi(int n,String X,String Y,String Z) {
	if(n == 1) {
	System.out.println("将第"+n+"个盘片从"+X+"移到"+Z); //X==>Z
	}else {
	Hanoi(n-1,X,Z,Y);
	System.out.println("将第"+n+"个盘片从"+X+"移到"+Z); //X==>Z
	Hanoi(n-1,Y,X,Z);
	}
}
复制代码

汉诺塔的基本要求是要把最大的放在最下面,因此当X塔座上的盘片数量大于1时,咱们首先要把X塔座上,1至n-1的盘片放到Y上,因而咱们能够忽略X最下面的盘片,把Y当作ZZ当作Y,这样问题回到了,当Xn-1个盘片,如何移动到Z的情形。记住这个时候的ZY。 如今咱们已经作到了把X1至n-1的盘片放到了Y上。别忘了X上还有一个咱们忽略的最大盘片。

好,如今把X最大盘片放到Z,这个时候X没有盘片,Y1至n-1的盘片,如今咱们忽略Z上的最大盘片。将Y当作XX当作Y。问题又回到了X上有n-1个盘片如何移动到Z的情形。

再看上面给出的代码。当n==1时,直接把X移到Z,不然,将Y当作ZZ当作Y,将1至n-1盘片移到Y上。这个时候取出X最大盘片放到Z。再把XYY当作X,继续递归。直到n==1,移动成功!其实不难发现当执行到第二个System.out.println("将第"+n+"个盘片从"+X+"移到"+Z);时。已经完成了将X的最大盘片移到Z的目标。汉诺塔的层数减一。问题回到了最初的情形,只不过,盘片在Y上,不在X上。可是这不妨碍调用Hanoi方法。实际上问题的关键在于,X,Y,Z三个塔座没有区别是同样的。还有一轮操做完以后,只是把最下面的盘片放到了Z,这个时候问题的规模就减一了。

如何设计递归算法?

找出递归结束条件

上文总结的递归特征中,有一条说明了递归必需要有终止条件。好比求阶乘,斐波那契数列以及汉诺塔问题,它们在递归调用时表示问题规模的n一直在递减,直到达到递归结束条件n==1。实际上,递归条件不必定是这种经过递减来达到结束条件判断值的,好比在构造树形结构的时候,能够经过判断数据项的isLeaf字段是否为true来决定是否退出递归。有一点能够确定的是,递归退出条件必定隐藏在初次调用传入方法的参数中以及一切在方法运行时能够访问的状态。

找出循环体

这是一个比较难的问题,对于相似数学定义问题,循环体每每就是定义问题的那几句话,好比斐波那契数列的定义:当0<n<3时,Fib=1,当n>2时,Fib(n)=Fib(n-1)+Fib(n-2)。当为递归数据结构设计算法时,循环体则是访问递归结构的指令。好比遍历链表时:

void foo(node) {
	if(node != null) {
    	foo(node.getNext());
	}else {
	return;
	}
}
复制代码

可是像汉诺塔这类问题,直观上没办法直接看出递归的特征。我的经验则是在较小问题规模上,进行演算。发现递归特征。

小结

递归程序设计自己是比较难以理解和掌握的,经过简单的理论学习没法熟练运用,只有不断的解决问题才会熟能生巧。

相关文章
相关标签/搜索