第二个是应该有至关一部分的开发者认为递归程序很差写。这个结论来源于个人一个员工。这个员工大概有几年的开发经验,而且谈吐处事很得体稳重,给个人印象是不错的。在一次闲谈中他将会写递归程序做为一个亮点提出来的,言下之意本身的技术是很不错的。另外一次经验来自一个面试的人。他把项目组的头会写递归程序做为一个敬佩的理由。由此我判断应该有至关一部分的开发者以为递归程序很差写。 node
第三个理由就比较简单了,那就是递归程序确实颇有用,很值得去掌握。在开发Entity Model Studio的时序图时,须要遍历消息流通过的全部节点,从而实现一个方便的移动操做,这里就用到了递归遍历。由于结构上讲是在遍历一棵树。面试
一.递归的定义数据结构
递归的概念的严格定义应该是来自数学,这个google一下就能够知道的。固然数学上的定义确定是不太好理解的,有兴趣的能够本身看一下。这里给一个比较容易理解的版本,也是一个比较实用的说法。若是定义一个概念的时候使用到了这个概念自己那么这就是递归了。好比下面的二叉树的定义:google
二叉树(BinaryTree)是:n(n≥0)个结点的有限集,它或者是空集(n=0),或者由一个根结点及两棵互不相交的、分别称做这个根的左子树和右子树的二叉树组成。spa
在上面的文字中,冒号后面的内容就是二叉树的定义。在这个定义中又出现了二叉树这个概念,因此这是二叉树的递归定义。固然须要区分一下这和循环论证是不同的。.net
二.递归程序的结构设计
既然凭脑子想不能很好的解决问题,那么咱们就须要使用一个更好的方法。咱们能够从递归程序的结构出发来构造完成递归程序。因此这里介绍一下递归程序的结构。从结构上讲递归程序分为三个部分:递归出口,逻辑处理,递归调用。code
1. 递归的出口对象
所谓递归的出口,就是指知足什么条件时程序再也不须要递归调用了。这个时候每每是递归程序递归调用到最深层的时候,须要开始回归了。还有一种状况是作出判断决定是否执行当前的递归程序,好比对递归方法的参数的容错处理。blog
2. 逻辑处理
在考虑写递归程序的时候,至少须要知道在递归出口时须要执行的逻辑处理是什么。其次就是某一次递归调用先后须要执行的逻辑处理是什么。须要注意的是,这个时候的处理只是针对部分的数据,由于都是在某一次递归的执行中处理数据。不是对全部数据的完整的处理。完整的处理是整个递归程序执行完毕后才能完成的。
3. 递归调用
这个很好理解,就是在递归程序内部调用本身。
三.递归程序构造举例1
为了有一个感性的认识,这里举一个例子说明一下如何从递归程序的结构出发来完成递归程序的构造。这里就用教科书上遍历二叉树的例子来分析一下如何处理递归程序。首先咱们考虑一下若是咱们须要遍历一颗二叉树,那么什么状况下咱们能够不用再递归遍历或者不必继续遍历了?这个答案就是遇到一颗空树的时候就没有必要再遍历了。参考下面的方法定义:
其中参数node就表示了一颗二叉树的根节点,若是这个node的值是空的话,那么咱们就没有必要再递归了,能够按照需求直接处理或者什么都不作直接返回了。因此上述方法的内部须要包含以下代码来结束递归,也就是所谓的递归的出口:
至此已经完成递归程序的第一部分了,固然也是最简单的一部分。须要强调的是这部分虽然是最简单的,但仍是建议你们在构思递归程序时最好首先明确这部分的内容。不然一个递归程序没有出口的话,那么运行起来会把栈击穿的,从而致使崩溃。
下面第二步就要考虑核心的问题了,那就是若是node不为空时咱们如何处理?首先须要明确咱们要完成的逻辑是什么。通常教科书上的遍历例子,不会讲所谓逻辑处理的,只是描述遍历,这里咱们能够假设一个虚拟的逻辑处理。咱们假设这个逻辑处理由以下的方法完成:
因而加上逻辑执行部分,咱们的递归程序看上去就如同下面的样子了:
很明显Dosomething按照既定的要求完成了对节点node的处理,可是咱们须要处理二叉树中的每个节点,只执行DoSomething(node)这一行代码是不够的。因此这时咱们须要递归程序的第三部分,即递归调用。就这个例子而言,node表示一棵二叉树的根节点,而且在VisitBinaryTree方法内部咱们调用了DoSomething方法完成了对node节点的处理。那么剩下的工做就是要处理node的左子树和右子树了,只有这样才算是完成了对node为根的整棵二叉树的处理。
这个时候咱们能够再继续写代码来处理node的左子树和右子树,可是等等,因为咱们如今构造的方法VisitBinaryTree就是用来处理二叉树的,而左子树或者右子树自己也是一棵二叉树,因此咱们就没有必要再写额外的代码来处理而是直接递归调用该方法就能够了。因此加入递归调用的代码后,VisitBinaryTree方法差很少就是下面的样子了:
至此遍历二叉树的方法就完成了。下面咱们来讨论一个细节问题,那就是根节点的判空问题。在这个例子中node的判空处理既是递归的出口,也是一种容错处理。由于若是不进行容错处理的话,那么DoSomething方法内容若是访问了node对象的属性或者方法,就会出现null对象方法的异常。可是有些人更习惯于在递归执行前对是否为空值做出判断,从而决定是否递归调用,这能够保证每次递归调用时,传入的值都不为空。所以代码差很少是下面这个样子:
这样的代码确实保证了传入递归方法的根节点参数不为空。可是却忽略了一个问题,那就是第一次调用VisitBinaryTree方法时node为空的状况没有考虑。假设以下的状况,咱们在一个方法OneMethod中以下调用的例子:
因此为了应对这个状况,最初判断node是否为空的代码仍是须要的,这样代码就变成以下的样子了:
好,如今重点来了。上面的代码中最后两个判空的if语句还须要么?答案是不须要了。假设去掉最后两个判空的判断,那么传入的参数确实有可能为空,可是当这样的参数传入VisitBinaryTree方法时,该方法的最开始就对这个参数执行了判空的处理,若是为空就直接返回了。因此达到了一样的目的。请体会一下,递归程序就是这个样子的。
四.递归程序构造举例2
在数据结构的教材上,遍历二叉树的方法有六种不一样的版本,最经常使用的只有三种,分别是:先序优先遍历,左序优先遍历,右序优先遍历。上面的例子是用的先序优先遍历。下面看看用一样的方法来构造左序优先遍历的递归方法。所谓左序优先,是要求先遍历处理完二叉树的左子树,而后处理根节点,而后再遍历处理完二叉树的右子树。
好,咱们仍是首先考虑递归的出口,对于遍历二叉树而言,其出口仍旧不变,仍是判空,若是为空就直接返回不处理了。因此第一步的代码是同样的,就再也不列出来了。下面是若是节点不为空,咱们须要先遍历处理左子树,再处理根节点,而后再遍历处理右子树。根据这个要求咱们明确了能够执行的逻辑是处理根节点。至此,第二部分完成一半了,列出代码以下:
下面就是关键了,那就是如何递归调用了。为了易于理解,咱们能够先假设,须要处理的二叉树是没有任何左子树的,也就是说要么没有任何子树,要么就只有右子树。这样咱们只须要考虑右子树就能够了,把左子树忘了吧。根据遍历处理的要求是先处理根节点而后再处理左子树,因此咱们的代码以下:
当VisitBinaryTree被递归调用时,传入的是node的右子树的根节点。这个右子树根节点,传入后首先是被判空,而后是调用DoSomething执行逻辑处理,而后再次递归调用来处理右子树的右子树。显然这样的递归调用逻辑是对的。
如今再假设须要处理的二叉树是没有任何右子树的,也就是说要么没有任何子树,要么就只有左子树。这样咱们只须要考虑左子树就能够了,此次能够把右子树忘了吧。根据遍历处理的要求是先处理左子树而后再根节点,因此咱们的代码以下:
当VisitBinaryTree被递归调用时,传入的是node的左子树的根节点。这个左子树根节点,传入后首先是被判空,而后是再次递归调用VisitBinaryTree遍历处理左子树的左子树。而后再处理根节点,显然这样的逻辑也是对的。好了,至此咱们能够考虑既有左子树又有右子树的通常状况了,把两部分的代码合起来就能够了,以下所示:
五.递归程序构造的比较
举例1的构造过程是从递归程序结构自己直接推导出来的,是一个很天然的过程。在构造时并无考虑是否为后续遍历,只是构造完成后正好和后续遍历一致。在举例2中的构造过程当中使用了一点技巧,那就是为了简化问题,看清递归调用的位置,前后假设不存在左子树和右子树的状况,而后再将两部分合并,从而完成递归程序的构造。这就是说举例2中在使用这个方法时多了一个简化问题的步骤,这是使用已知的知识解决问题的一个例子。关于解决问题的更多讨论能够参考本系列中问题解决篇的讨论。再将这两个例子和教科书上的例子作一个比较。这里的讨论给出了递归程序构造的详细步骤,相比教科书上直接给出结果来讲,我以为这里讨论更容易理解。另外一个区别是,因为本文的例子是从递归的结构出发完成构造递归程序的,因此没有涉及讨论所谓递归程序执行时会用到的工做栈的问题。有兴趣的能够再看一下其它相关的资料,对工做栈的了解应该多少对递归程序的认识是有帮助的。
此次就写到这里,感谢阅读。下一篇仍是谈谈递归程序,介绍一个更强更"广谱适用"的方法来完成递归程序的设计。推广一下个人创业群:244054966,欢迎加入