recursion本身是一个function吗?
…还是只是一种习惯?
我是这样问的,因为和我的教授发生了争执:我没有按照recursion的方式调用函数,因为我们没有涉及到课堂上的recursion,而且我的观点是我们通过学习return
和方法隐含地学习了它。
我在这里问,因为我怀疑有人有一个明确的答案。
例如,以下两种方法有什么区别:
public static void a() { return a(); } public static void b() { return a(); }
除了“一直延续下去”(在实际的程序中,当提供了无效的input时,它被正确地用来再次提示用户), a
和b
之间是否有根本的区别? 对于未优化的编译器,它们是如何处理的?
最终归结为是否通过学习从b
return a()
,我们也学会了从a
return a()
。 我们呢?
回答你的具体问题:不,从学习语言的angular度来看,recursion不是一个特征。 如果你的教授真的使用他还没有教过的“function”来标记你的标记,那就错了。
在这两行之间进行阅读,一种可能性是通过使用recursion,你避免了使用本来应该是学习成果的function。 例如,也许你根本没有使用迭代,或者你可能只用于循环,而不是while
使用for
和while
。 一个作业的目的是testing你做某些事情的能力是常见的,如果你避免做这些作业,你的教授根本就不能给你留下这个特性的标记。 然而,如果这真的是你失去了分数的原因,教授应该把它作为自己的学习经验,如果certificate某些学习成果是作业的标准之一,应该向学生明确解释。
话虽如此,我同意大多数其他意见和答案,迭代是比这里recursion更好的select。 有几个原因,而其他人在某种程度上触动了他们,我不确定他们是否完全解释了他们背后的想法。
堆栈溢出
更明显的是你有冒险得到堆栈溢出错误。 实际上,你写的方法不太可能实际导致一个,因为用户必须多次input错误的input来实际触发堆栈溢出。
然而,有一点要记住,不仅是方法本身,而且调用链中的其他更高或更低的方法将在堆栈中。 正因为如此,随便吞噬可用堆栈空间对于任何方法来说都是非常不礼貌的事情。 没有人希望在编写代码时不得不经常担心可用的堆栈空间,因为其他代码可能会不必要地使用了大量的代码。
这是软件devise中被称为抽象的更一般原则的一部分。 本质上,当你调用DoThing()
,你应该关心的是事情完成了。 您不必担心如何完成的实现细节。 但贪婪的使用堆栈打破了这个原则,因为每一个代码都需要担心多less堆栈可以安全地假定它被调用链中其他地方的代码留下。
可读性
另一个原因是可读性。 代码应该追求的理想是成为一个可读的文档,每行只描述它在做什么。 采取这两种方法:
private int getInput() { int input; do { input = promptForInput(); } while (!inputIsValid(input)) return input; }
与
private int getInput() { int input = promptForInput(); if(inputIsValid(input)) { return input; } return getInput(); }
是的,这两个工作,是的,他们都很容易理解。 但是这两种方法怎么可能用英文来描述呢? 我想这应该是这样的:
我会提示input,直到input有效,然后返回
与
我会提示input,那么如果input有效,我会返回它,否则我得到的input,并返回的结果
也许你可以想到后者的粗略措辞,但是我想你总是会发现,第一个从概念上来说,是对你实际上要做的事情的更准确的描述。 这并不是说recursion总是不太可读。 对于像树遍历一样的情况,你可以在recursion和另一种方法之间进行同样的并行分析,你几乎可以肯定地发现recursion给出的代码更清楚地自我描述,一行一行。
孤立地说,这两个都是小点。 这是不太可能导致堆栈溢出,并且可读性的增加是微小的。 但是任何程序都将成为许多这些小决策的集合,所以即使孤立一点也不重要,学习正确的原则是很重要的。
为了回答这个字面问题,而不是元问题:recursion是一个特征,并不是所有的编译器和/或语言都必须允许它。 实际上,所有(普通的)现代编译器(当然也包括所有的Java编译器) – 但这不是普遍的事实。
作为一个为什么可能不支持recursion的人为的例子,考虑一个编译器,它将函数的返回地址存储在一个静态位置; 例如,对于没有堆栈的微处理器,编译器可能就是这种情况。
对于这样的编译器,当你调用这样的函数
a();
它被实现为
move the address of label 1 to variable return_from_a jump to label function_a label 1
和a()的定义,
function a() { var1 = 5; return; }
被执行为
label function_a move 5 to variable var1 jump to the address stored in variable return_from_a
希望当你尝试在这样的编译器中recursion地调用a()
时,这个问题是显而易见的。 编译器不再知道如何从外部调用返回,因为返回地址已被覆盖。
对于编译器我实际上使用(70年代后期或80年代初期,我认为)不支持recursion问题稍微微妙:返回地址将被存储在堆栈,就像在现代编译器,但局部variablesweren “T。 (理论上这应该意味着对于没有非静态局部variables的函数,recursion是可能的,但是我不记得编译器是否明确地支持这个variables,由于某种原因它可能需要隐式的局部variables。
outlook未来,我可以想象,专门的scheme – 大量并行的系统,也许 – 不必为每个线程提供堆栈可能是有利的,因此只有编译器可以将其重构为循环时才允许recursion。 (当然,我上面讨论的原始编译器不能处理像重构代码这样复杂的任务。)
老师想知道你是否学过。 显然你没有像他教你的方式解决问题( 好方法 ;迭代),因此,认为你没有。 我都是为了创造性的解决scheme,但在这种情况下,我不得不同意你的老师有一个不同的原因:
如果用户提供了无效的input次数太多(例如通过保持input按下),你将有一个堆栈溢出exception,你的解决scheme将崩溃。 另外,迭代解决scheme更高效,更易于维护。 我想这就是你的老师应该给你的原因。
扣分是因为“我们没有在课堂上recursion”是可怕的。 如果你学习了如何调用函数A,调用函数B调用函数C返回给返回给调用者的B,而教师没有明确地告诉你这些函数是不同的函数例如旧FORTRAN版本就是这种情况),没有理由说A,B和C不能全部是相同的function。
另一方面,我们不得不看实际的代码来决定在你的特定情况下使用recursion是否真的是正确的。 细节并不多,但听起来不对。
关于你所问的具体问题,有许多观点,但我可以说,从学习语言的angular度来看,recursion本身并不是一个特征。 如果你的教授真的使用他还没有教过的“function”来标记你的标记,那就错了,但是正如我所说的,还有其他观点可以在这里考虑,这使得教授在扣分的时候是正确的。
从我可以从你的问题推导出来,在input失败的情况下使用recursion函数来请求input不是一个好习惯,因为每个recursion函数的调用都会被推送到堆栈上。 由于这种recursion是由用户input驱动的,所以可能有一个无限recursion函数,从而产生一个StackOverflow。
这两个例子在你的问题中提到的两个例子之间没有任何区别(但是在其他方面有所不同) – 在这两种情况下,返回地址和所有方法信息都被加载到堆栈中。 在recursion情况下,返回地址就是方法调用之后的线(当然,它不是你在代码本身看到的东西,而是在编译器创build的代码中)。 在Java,C和Python中,与迭代(通常)相比,recursion相当昂贵,因为它需要分配一个新的栈帧。 更不用说,如果input无效次数太多,你可能会遇到堆栈溢出exception。
我相信教授扣除分数,因为recursion被认为是它自己的主题,并且不可能没有编程经验的人会想到recursion。 (当然这并不意味着他们不会,但不太可能)。
恕我直言,我认为教授是正确的扣除你的观点。 您可以轻松地将validation部分采用不同的方法,并像这样使用它:
public bool foo() { validInput = GetInput(); while(!validInput) { MessageBox.Show("Wrong Input, please try again!"); validInput = GetInput(); } return hasWon(x, y, piece); }
如果你所做的确能以这种方式得到解决,那么你所做的就是一个不好的做法,应该避免。
也许你的教授还没有教过,但是听起来你已经准备好了解recursion的优点和缺点了。
recursion的主要优点是recursionalgorithm通常更容易写得更快。
recursion的主要缺点是recursionalgorithm会导致堆栈溢出,因为每个recursion级别都需要一个额外的堆栈帧添加到堆栈。
对于生产代码来说,缩放比生成程序员的unit testing可以产生更多的生产recursion级别,缺点通常大于优点,实际上经常避免使用recursion代码。
关于具体问题,recursion是一个特点,我倾向于说是,但重新解释了这个问题之后。 有可能使recursion成为可能的语言和编译器的常见deviseselect,并且存在完全不允许recursion的图灵完备语言。 换句话说,recursion是一种由语言/编译器devise中的某些select所启用的能力。
-
支持一stream的函数使得recursion在极小的假设下成为可能; 请参阅以Unlambda为例编写循环 ,或者这个不包含自引用,循环或赋值的Pythonexpression式:
>>> map((lambda x: lambda f: x(lambda g: f(lambda v: g(g)(v))))( ... lambda c: c(c))(lambda R: lambda n: 1 if n < 2 else n * R(n - 1)), ... xrange(10)) [1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880]
-
使用后期绑定的语言/编译器,或定义前向声明的语言/编译器使recursion成为可能。 例如,Python允许下面的代码,这是一个deviseselect(后期绑定),而不是图灵完整系统的要求。 相互recursion函数通常依赖于对前向声明的支持。
factorial = lambda n: 1 if n < 2 else n * factorial(n-1)
-
允许recursion定义types的 静态types语言有助于启用recursion。 在Go中查看Y Combinator的这个实现 。 没有recursion定义的types,仍然可以在Go中使用recursion,但是我相信Y combinator是不可能的。
从我的问题中可以推断出,在input失败的情况下使用recursion函数来请求input不是一个好习惯。 为什么?
因为每个recursion函数调用都被推送到堆栈。 由于这种recursion是由用户input驱动的,所以可能有一个无限recursion函数,从而导致StackOverflow :-p
有一个非recursion循环来做到这一点是要走的路。
recursion是一个编程概念 ,一个特性 (如迭代)和一个练习 。 从链接中可以看到,这个主题有很大的研究领域。 也许我们不需要深入这个话题来理解这些观点。
recursion作为一个function
简而言之,Java支持它隐含的,因为它允许一个方法(这基本上是一个特殊的function)有自己的“知识”和其他方法构成它所属的类。 考虑一种不是这种情况的语言:你可以写出这个方法的主体,但是你不能在其中包含一个调用。 唯一的解决办法是使用迭代来获得相同的结果。 在这样的语言中,你将不得不区分函数意识到自己的存在(通过使用特定的语法标记),而不是那些! 实际上,一组语言确实有这样的区别(例如参见Lisp和ML系列)。 有趣的是,Perl甚至允许匿名函数(所谓的lambdas )recursion地调用自己(再次,用一个专用的语法)。
没有recursion?
对于甚至不支持recursion可能性的语言,通常还有另一种解决scheme,以定点组合器的forms,但是它仍然要求语言支持所谓的第一类对象(即可能是在语言本身内操纵)。
recursion作为一种实践
用语言提供这个function并不意味着它是惯用的。 在Java 8中,lambdaexpression式已经包含在内,所以采用一种function性的编程方法会变得更容易。 但是,有实际的考虑:
- 语法仍然不是非常recursion友好的
- 编译器可能无法检测到这种做法并对其进行优化
底线
幸运的是(或者更准确地说,为了便于使用),Java确实让方法默认了自己,因此支持recursion,所以这不是一个真正的实际问题,但它仍然是一个理论上的问题,我想你的老师想专门解决它。 另外,鉴于语言的最近发展,将来可能变成重要的东西。