以前公司管理系统项目须要配合前端VUE
实现动态路由,返回的数据结构是一个树形结构,但在数据库中存储的是平行的数据项。这个时候就须要在代码中去进行数据结构的组装。很久没思考这些问题,花了好些时间才搞定。这其中涉及到递归算法的实现,乘着周末深刻的思考了下递归算法的设计,一点点拙见写下来作个笔记。前端
对于递归
其实咱们并不陌生。还记得中学时代常用的数学概括法
?,递归
其实并非计算机科学独有的概念。实际上数学概括法
才是递归的理论基础。node
先举个简单的栗子:求n! 这里使用递归来求解。算法
int foo(int n) {
if(n != 1) {
return n*foo(n-1);
}
return 1;
}
复制代码
上面代码所表达的含义是:当n
不等于1
的时候。一直去调用foo(n-1)
而且和传入的n
相乘。在这里解释下递归调用的实现原理:咱们知道计算机中函数的调用是使用栈的数据结构实现的,好比传入的n
为5
时,第一次执行foo
方法,当执行到 foo(n-1)
,方法foo
会被压入栈中。其实递归方法的出栈入栈和普通方法是同样的。惟一的区别是递归调用的是自身的代码。这里不妨把foo
的每次调用称做foo1
,foo2
等等。那么这样就好理解了,当foo
方法执行到n==1
的时候,栈里面依次存放着foo1,foo2,foo3,foo4,foo5。
此时foo5
在栈顶,出栈执行。foo5
执行的时候n==1
,由代码可知,n==1
,方法直接返回1
。接着foo4
出栈,传入的n
为2
,运算2*1
,而后foo3,foo2,foo1
依次出栈,运算依次为:3*2,4*6
当最后一个方法foo1 return
时计算,5*24
,运行结束。数据库
从这个过程当中不难看出,递归是个不断降低的过程。层层调用自身,而后求值又是个上升的过程。从递归结束的出口返回值开始,层层向上求值。bash
从这个例子中还能够看出递归算法的两个重要特征,好比:数据结构
须要思考的问题是终止条件是如何肯定的,以及他是怎么变化的。在上面的程序中是作-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塔座上只有一个盘片时:
X==>Z
当X塔座上有两个盘片时:
X1==>Y,X2==>Z
Y1==>Z
当X塔座上有三个盘片时:
X1==>Z,X2==>Y
Z==>Y,X==>Z
Y1==>X,Y2==>Z
X==>Z
先给出递归算法,这个算法将打印出移动的步骤。
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
当作Z
,Z
当作Y
,这样问题回到了,当X
有n-1
个盘片,如何移动到Z
的情形。记住这个时候的Z
是Y
。 如今咱们已经作到了把X
上1至n-1
的盘片放到了Y
上。别忘了X
上还有一个咱们忽略的最大盘片。
好,如今把X
最大盘片放到Z
,这个时候X
没有盘片,Y
有1至n-1
的盘片,如今咱们忽略Z
上的最大盘片。将Y
当作X
,X
当作Y
。问题又回到了X
上有n-1
个盘片如何移动到Z
的情形。
再看上面给出的代码。当
n==1
时,直接把X
移到Z
,不然,将Y
当作Z
,Z
当作Y
,将1至n-1
盘片移到Y
上。这个时候取出X
最大盘片放到Z
。再把X
当Y
,Y
当作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;
}
}
复制代码
可是像汉诺塔这类问题,直观上没办法直接看出递归的特征。我的经验则是在较小问题规模上,进行演算。发现递归特征。
递归程序设计自己是比较难以理解和掌握的,经过简单的理论学习没法熟练运用,只有不断的解决问题才会熟能生巧。