我正在阅读一些开发面试的实践,特别是面试中提出的技术问题和测试,我对这种类型的说法误解了很多次:“好的,您可以用while循环解决问题,现在可以递归”或“每个人都可以用100行while循环解决此问题,但是他们可以用5行递归函数来实现吗?” 等等
我的问题是,递归是否通常比if / while / for构造好?
老实说,我一直认为递归不是首选,因为它仅限于比堆小得多的堆栈内存,而且从性能的角度来看,执行大量的函数/方法调用也不理想。是错的...
我正在阅读一些开发面试的实践,特别是面试中提出的技术问题和测试,我对这种类型的说法误解了很多次:“好的,您可以用while循环解决问题,现在可以递归”或“每个人都可以用100行while循环解决此问题,但是他们可以用5行递归函数来实现吗?” 等等
我的问题是,递归是否通常比if / while / for构造好?
老实说,我一直认为递归不是首选,因为它仅限于比堆小得多的堆栈内存,而且从性能的角度来看,执行大量的函数/方法调用也不理想。是错的...
Answers:
递归在本质上并不比循环好或坏-循环各有优缺点,甚至有赖于编程语言(和实现)。
从技术上讲,迭代循环在硬件级别上更适合于典型的计算机系统:在机器代码级别,循环只是测试和有条件的跳转,而递归(天真的实现)涉及推入堆栈帧,跳转,返回和弹出从堆栈。OTOH,可以编写许多递归情况(尤其是那些等同于迭代循环的情况),从而可以避免堆栈推入/弹出操作;当递归函数调用是返回之前在函数主体中发生的最后一件事情时,这是可能的,并且通常称为尾调用优化(或尾递归优化)。适当的尾调用优化的递归函数在机器代码级别上大致等效于迭代循环。
另一个考虑因素是迭代循环需要破坏性的状态更新,这使它们与纯(无副作用)语言语义不兼容。这就是为什么像Haskell这样的纯语言根本没有循环构造的原因,而许多其他函数式编程语言要么完全缺少循环构造,要么尽可能避免使用它们。
这些问题之所以在面试中如此之多,是因为要回答这些问题,您需要对许多重要的编程概念(变量,函数调用,作用域,当然还有循环和递归)有透彻的了解,并且为您带来思维上的灵活性,使您能够从两个截然不同的角度解决问题,并在同一概念的不同表现形式之间移动。
经验和研究表明,有能力理解变量,指针和递归的人与没有理解变量,指针和递归的人之间存在着界限。可以通过学习和经验来获取编程中几乎所有其他内容,包括框架,API,编程语言及其优势,但是,如果您无法对这三个核心概念有直觉,则不适合成为程序员。将简单的迭代循环转换为递归版本是滤除非程序员的最快方法-即使是经验不足的程序员通常也可以在15分钟内完成,这是一个与语言无关的问题,因此应聘者可以选择他们选择的语言,而不是绊倒特质。
如果您在面试中遇到这样的问题,那就是一个好兆头:这意味着准雇主正在寻找可以编程的人,而不是记住了编程工具手册的人。
这取决于。
还值得注意的是,对尾部递归的支持使尾部递归和迭代循环等效,也就是说,递归并不一定总是浪费堆栈。
同样,始终可以通过使用显式堆栈来迭代地实现递归算法。
最后,我要指出,五行解决方案可能总是比一百行解决方案更好(假设它们实际上是等效的)。
在编程方面,关于“更好”的定义尚未达成共识,但我将其理解为“易于维护/阅读”。
递归比迭代循环结构具有更多的表达能力:之所以这么说,是因为while循环等效于尾递归函数,而递归函数不必是尾递归。强大的构造通常是一件坏事,因为它们使您能够执行难以阅读的事情。但是,递归使您无需使用可变性就可以编写循环,而在我看来,可变性比递归功能强大得多。
因此,从低表达能力到高表达能力,循环构造像这样堆积:
理想情况下,您将使用表达能力最低的构造。当然,如果您的语言不支持尾部调用优化,那么这也可能会影响您对循环结构的选择。
递归通常不太明显。不那么明显很难维护。
如果您for(i=0;i<ITER_LIMIT;i++){somefunction(i);}
在主流中编写代码,则可以很清楚地看到您正在编写一个循环。如果您写的somefunction(ITER_LIMIT);
话,您并不会很清楚会发生什么。只看到内容:该somefunction(int x)
调用somefunction(x-1)
告诉您实际上是使用迭代的循环。同样,您不能轻易将转义条件放置break;
在迭代的一半位置,您必须添加将一直传递的条件,或者引发异常。(异常再次增加了复杂性...)
本质上,如果在迭代和递归之间是一个显而易见的选择,请执行直观的操作。如果迭代能够轻松完成任务,那么从长远来看,节省2行几乎不值得其头痛。
当然,如果要节省98条线,那就完全不同了。
在某些情况下,递归完全适合,而且并非罕见。遍历树结构,多重链接的网络,可以包含其自身类型的结构,多维锯齿形数组,从本质上讲,它既不是简单的向量也不是固定维数的数组。如果遍历已知的直线路径,则进行迭代。如果您陷入未知,请递归。
本质上,如果somefunction(x-1)
要在每个级别内从自身内部多次调用,则无需进行迭代。
...为最好通过递归完成的任务迭代编写函数是可能的,但并不令人满意。无论您在哪里使用int
,都需要类似stack<int>
。我做过一次,更多的是作为练习,而不是出于实际目的。我可以向您保证,一旦您遇到这样的任务,您将不会像您所表达的那样有任何疑问。
map
可以定义为递归函数(例如,参见haskell.org/tutorial/functions.html),即使从直觉上清楚它遍历列表并将函数应用于列表的每个成员也是如此。
map
不是关键字,它是一个常规函数,但这有点不相关。当函数式程序员使用递归时,通常不是因为他们想要执行一系列动作,而是因为要解决的问题可以表示为函数和参数列表。然后可以将问题简化为另一个函数和另一个参数列表。最终,您有一个可以轻松解决的问题。
通常,这通常是无法回答的,因为存在其他因素,这些因素在实践中在案例之间广泛不平等,在用例中彼此不平等。这是一些压力。
递归是关于重复调用函数,循环是关于重复跳转到内存中。
还应该提到有关堆栈溢出的问题-http: //en.wikipedia.org/wiki/Stack_overflow
这实际上取决于便利性或要求:
如果您使用Python编程语言,则它支持递归,但是默认情况下,递归深度是有限制的(1000)。如果超过限制,我们将收到错误或异常消息。该限制可以更改,但是如果这样做,我们可能会遇到该语言的异常情况。
这时(调用数量大于递归深度),我们需要使用循环构造。我的意思是,如果堆栈大小不够,我们必须更喜欢循环结构。
使用策略设计模式。
根据您的负载(和/或其他条件),选择一个。