while循环本质上是递归吗?


37

我想知道while循环本质上是否是递归?

我认为这是因为while循环可以看作是最后调用自身的函数。如果不是递归,那有什么区别?



13
您可以将递归转换为迭代,反之亦然,是的。这并不意味着它们相同,而只是具有相同的功能。有时候递归更自然,有时候迭代更自然。
Polygnome '16

18
@MooingDuck您可以通过归纳证明任何递归都可以写为迭代,反之亦然。是的,它看起来会非常不同,但是您仍然可以做到。
Polygnome '16

6
本质上相同是什么意思?在编程中,使用递归表示特定的事物,与迭代(循环)不同。在CS中,当您更接近事物的理论数学面时,这些事物将意味着不同的事物。
海德

3
@MooingDuck从递归到迭代的转换实际上是微不足道的。您只需保留函数调用参数的堆栈和函数调用的结果的堆栈。您可以通过将参数添加到调用堆栈中来替换递归调用。确保对堆栈的所有处理都破坏了算法的结构,但是一旦您理解了这一点,就很容易看到代码执行相同的操作。基本上,您是在明确编写递归定义中隐含的调用堆栈。
巴库里

Answers:


116

循环不是递归。实际上,它们是相反机制的主要示例:迭代

递归的重点是处理的一个元素调用了自身的另一个实例。回路控制机构只是回到起点。

在代码中跳转并调用另一个代码块是不同的操作。例如,当您跳转到循环的开始时,循环控制变量仍具有与跳转前相同的值。但是,如果您调用所在例程的另一个实例,则新实例将具有其所有变量的新的,不相关的副本。实际上,一个变量可以在处理的第一层具有一个值,而在较低层具有另一个值。

此功能对于许多递归算法的工作至关重要,因此这就是为什么您无法通过迭代来模拟递归而又不管理跟踪所有这些值的调用框架堆栈的原因。


10
@Giorgio可能是对的,但这是对没有给出答案的说法的评论。该答案中不存在“任意”字样,它将大大改变其含义。
hvd

12
@hvd原则上,尾递归与其他任何递归一样都是完全递归。智能编译器可以优化实际的“创建新的堆栈框架”部分,以使生成的代码与循环非常相似,但是我们所讨论的概念适用于源代码级别。我认为算法作为源代码的形式很重要,因此我仍将其称为递归
Kilian Foth,2016年

15
@Giorgio“这正是递归所做的:使用新的参数调用自身” —除了调用。还有争论。
hobbs

12
@Giorgio您使用的单词定义与此处大多数使用的定义不同。你知道,语言是沟通的基础。这是程序员,而不是CS Stack Exchange。如果我们按照您的建议使用诸如“参数”,“调用”,“功能”之类的词,就不可能讨论实际代码。
海德

6
@ Giorgio我正在看抽象概念。有重复出现的概念和循环出现的概念。他们是不同的概念。霍布斯(Hobbs)是100%正确的,没有论点,也没有电话。它们在根本上和抽象上是不同的。这很好,因为它们可以解决不同的问题。另一方面,您正在研究当您的唯一工具是递归时如何实现循环。具有讽刺意味的是,当您的方法论确实需要重新评估时,您是在告诉霍布斯停止思考实现并开始关注概念。
corsiKa '16

37

说X本质上是Y,只有在您要表达X的某种(正式)系统时才有意义while。如果以注册机的形式定义它,则可能不会。

在这两种情况下,如果仅因为函数包含while循环而调用函数递归,人们可能就不会理解您。

*虽然可能只是间接的,例如,如果您使用进行定义fold


4
公平地说,该函数在任何定义中都不是递归的。它只包含一个递归元素,即循环。
a安

@Luaan:的确如此,但是由于在具有while构造递归性的语言中,递归通常是函数的属性,因此在这种情况下,我只是想不出其他任何东西来描述“递归”。
安东·戈洛夫

36

这取决于您的观点。

如果您看一下可计算性理论,那么迭代和递归是同等表达的。这意味着您可以编写一个计算某些内容的函数,而无论递归还是迭代都无所谓,您将可以选择这两种方法。没有什么可以递归计算的,不能递归计算,反之亦然(尽管程序的内部工作方式可能有所不同)。

许多编程语言不会将递归和迭代视为相同,这是有充分理由的。通常,递归意味着语言/编译器会处理调用堆栈,而迭代意味着您可能必须自己进行堆栈处理。

但是,在某些语言(尤其是函数式语言)中,诸如循环(for,while)之类的东西实际上只是递归的语法糖,并以这种方式在幕后实现。这在函数式语言中通常是合乎需要的,因为它们通常不具有循环的概念,并且添加它会使其演算变得更加复杂,而没有任何实际原因。

所以不,它们本质上并不相同。它们具有同等的表现力,这意味着您不能迭代地计算某些内容,也不能递归地进行计算,反之亦然,但这就是一般情况下的情况(根据Church-Turing论文)。

请注意,我们在这里谈论的是递归程序。还有其他形式的递归,例如在数据结构(例如树)中。


如果从实现的角度来看它,那么递归和迭代几乎是不一样的。递归为每个调用创建一个新的堆栈框架。递归的每一步都是独立的,从被调用者(自己)获取用于计算的参数。

另一方面,循环不会创建呼叫帧。对于他们来说,上下文不会在每个步骤中保留。对于循环,程序仅跳回到循环的开始,直到循环条件失败为止。

要知道这一点非常重要,因为它可以在现实世界中产生巨大的差异。为了进行递归,必须在每次调用时保存整个上下文。对于迭代,您可以精确控制哪些变量在内存中以及什么保存在哪里。

如果您这样看,您很快就会发现,对于大多数语言而言,迭代和递归在本质上是不同的,并且具有不同的属性。根据情况,某些特性比其他特性更理想。

递归可以使程序更简单,更易于测试和证明。将递归转换为迭代通常会使代码更复杂,从而增加失败的可能性。另一方面,转换为迭代并减少调用堆栈帧的数量可以节省大量的内存。


具有局部变量和递归但没有数组的语言无法执行由具有局部变量且没有数组的迭代语言无法执行的任务。例如,报告输入是否包含未知长度的字母数字字符串,后跟一个空格,然后以相反的顺序报告原始字符串的字符。
超级猫

3
只要语言是完整的,就可以。例如,一个数组可以很容易地被一个(双重)链表替换。仅当您比较两种图灵完备的语言时,谈论迭代或递归以及它们是否相等才有意义。
Polygnome '16

我的意思是除了简单的静态或自动变量外,即没有图灵完备。纯迭代语言将限于可以通过简单的确定性有限自动机完成的任务,而递归语言将增加执行至少需要下推确定性有限自动机的任务的能力。
超级猫

1
如果语言不完整,一开始就毫无意义。DFA既不能进行任意迭代,也不能进行递归操作。
Polygnome

2
实际上,没有实现是图灵完备的,而不是图灵完备的语言可能对许多目的有用。DFA可以容纳任何可以在有限范围内使用有限数量的变量运行的程序,其中每种可能的值组合都是离散状态。
超级猫

12

区别在于隐式堆栈和语义。

“结束时调用自身”的while循环在完成后没有要爬回的堆栈。它的最后一次迭代设置了结束时的状态。

但是,如果没有这个隐式堆栈来记住以前完成的工作状态,就无法进行递归。

的确,如果您明确允许迭代访问堆栈,则可以解决任何递归问题。但是那样做是不一样的。

语义上的差异与以下事实有关:查看递归代码传达的想法与迭代代码完全不同。迭代代码一次完成一个步骤。它接受以前产生的任何状态,并且只能创建下一个状态。

递归代码将问题分解为分形。这小部分看起来像是大部分,因此我们可以用相同的方式来完成其中的一部分。这是思考问题的另一种方式。它非常强大,需要习惯。在几行中可以说很多。即使它可以访问堆栈,也无法从while循环中删除它。


5
我认为“隐式堆栈”具有误导性。递归是语言语义的一部分,而不是实现细节。(当然,大多数支持递归的语言都使用调用堆栈;但是,首先,有一些这样的语言不使用调用堆栈;其次,即使在有这种语言的情况下,并非每个递归调用都必须追加到调用堆栈中,因为许多语言都支持优化,例如作为消除尾部调用的一种方法。)了解通常的/简单的实现方式对于理解抽象很有用,但是您不应欺骗自己以为是完整的故事。
ruakh

2
@ruakh我认为使用尾调用消除在恒定空间中执行的函数确实是一个循环。这里的堆栈不是实现细节,而是抽象,它允许您为不同级别的递归积累不同的状态。
Cimbali

@ruakh:单个递归调用中的任何状态都将存储在隐式堆栈中,除非该递归可以由编译器转换为迭代循环。尾部调用消除一个实现细节,如果要将函数重组为尾部递归,则需要注意这一点。另外,“很少有这种语言不需要” -您能提供一个不需要递归调用堆栈的语言示例吗?
Groo


@ruakh:CPS本身会创建相同的隐式堆栈,因此它必须依靠尾部调用消除才有意义(由于构造方式的原因,它是微不足道的)。即使您链接到的Wikipedia文章也是如此:使用不带尾部调用优化(TCO)的CPS不仅会导致递归过程中构造的延续可能增长,还会导致调用堆栈增加
Groo

7

这完全取决于您对术语的内在使用。在编程语言级别,它们在语法和语义上是不同的,并且它们在性能和内存使用上也有很大差异。但是,如果您对理论进行了足够深入的研究,它们可以彼此定义,因此在某种理论意义上是“相同的”。

真正的问题是:什么时候区分迭代(循环)和递归,什么时候将其视为相同的东西有用?答案是,在实际编程时(与编写数学证明相对),区分迭代和递归很重要。

递归创建一个新的堆栈框架,即每个调用都使用一组新的局部变量。这会产生开销,并占用堆栈上的空间,这意味着足够深的递归可能会使堆栈溢出,从而导致程序崩溃。另一方面,迭代仅修改现有变量,因此通常更快,并且仅占用恒定的内存量。因此,这对于开发人员而言是非常重要的区别!

在具有尾调用递归的语言(通常是功能语言)中,编译器可能能够以仅占用固定数量内存的方式优化递归调用。在这些语言中,重要的区别不是迭代与递归,而是非尾调用递归版本的尾调用递归和迭代。

底线:您需要能够分辨出差异,否则您的程序将崩溃。


3

while循环是递归的一种形式,例如参见对此问题的公认答案。它们在可计算性理论中对应于μ运算符(例如请参见此处)。

for在一定范围的数字,有限集合,数组等上迭代的循环的所有变体都对应于原始递归,请参见此处此处。请注意,forC,C ++,Java等while循环实际上是循环的语法糖,因此它不对应于原始递归。Pascal for循环是原始递归的一个示例。

一个重要的区别是原始递归总是终止,而广义递归(while循环)可能不会终止。

编辑

关于注释和其他答案的一些说明。“当根据事物本身或事物类型定义事物时,就会发生递归。” (请参阅Wikipedia)。所以,

while循环本质上是递归吗?

由于您可以while根据自身定义循环

while p do c := if p then (c; while p do c))

那么,是的while循环是递归的一种形式。递归函数是递归的另一种形式(递归定义的另一个示例)。列表和树是其他形式的递归。

许多答案和评论暗含的另一个问题是

while循环和递归函数是否等效?

这个问题的答案是否定的while循环对应于尾递归函数,其中循环访问的变量对应于隐式递归函数的参数,但是,正如其他人指出的那样,非尾递归函数如果不while使用额外的堆栈,则无法通过循环建模。

因此,“ while循环是递归的一种形式”这一事实与“某些递归函数不能由while循环表示”的事实并不矛盾。


2
@morbidCode:原始递归和μ递归是具有特定限制(或没有限制)的递归形式,例如在可计算性理论中进行了研究。事实证明,只有一个FOR循环的语言才能计算出所有原始递归函数,而只有一个WHILE循环的语言可以计算出所有µ递归函数(事实证明,µ递归函数正是那些图灵机可以计算)。或者,简而言之:原始递归和µ递归是数学/可计算性理论的技术术语。
约尔格W¯¯米塔格

2
我以为“递归”意味着函数调用了自己,导致当前的执行状态被压入堆栈等等。因此,大多数计算机对可以递归的级别有实际的限制。while循环没有任何这样的限制,因为它们在内部将使用“ JMP”之类的东西,并且不使用堆栈。仅凭我的理解,可能是错误的。
杰伊

13
该答案对“递归”一词的定义与OP所使用的完全不同,因此极易引起误解。
Mooing Duck

2
@DavidGrinberg:引用:“ C,C ++,Java for循环不是原始递归的示例。原始递归意味着在开始循环之前,最大迭代次数/递归深度是固定的。” Giorgio正在谈论可计算性理论原语。与编程语言无关。
Mooing Duck

3
我必须同意Mooing Duck。尽管可计算性理论在理论CS中可能很有趣,但我认为每个人都同意OP在谈论编程语言概念。
Voo

2

尾呼叫(或尾递归调用)是作为一个“转到与参数”(不按任何确切实现附加的呼叫帧调用堆栈),并且在一些功能的语言(特别是ocaml的)是循环的通常的方式。

因此,while循环(在具有它们的语言中)可以看作是对其主体(或头部测试)的尾部调用结束。

同样,普通(非尾调用)递归调用可以通过循环(使用某些堆栈)进行模拟。

另请参阅有关延续延续传递样式的信息

因此,“递归”和“迭代”极为等效。


1

的确,递归和无界while循环在计算表达性方面是等效的。也就是说,可以递归地编写任何程序,而可以使用循环将其重写为等效程序,反之亦然。两种方法都是图灵完备的,即可以用来计算任何可计算函数。

在编程方面的根本区别在于,递归使您可以利用存储在调用堆栈中的数据。为了说明这一点,假设您想使用循环或递归来打印单链接列表的元素。我将使用C作为示例代码:

 typedef struct List List;
 struct List
 {
     List* next;
     int element;
 };

 void print_list_loop(List* l)
 {
     List* it = l;
     while(it != NULL)
     {
          printf("Element: %d\n", it->element);
          it = it->next;
     }
 }

 void print_list_rec(List* l)
 {
      if(l == NULL) return;
      printf("Element: %d\n", l->element);
      print_list_rec(l->next);
 }

简单吧?现在,我们进行一些修改:以相反的顺序打印列表。

对于递归变量,这是对原始函数的几乎微不足道的修改:

void print_list_reverse_rec(List* l)
{
    if (l == NULL) return;
    print_list_reverse_rec(l->next);
    printf("Element: %d\n", l->element);
}

对于循环功能,我们有一个问题。我们的清单是单链的,因此只能向前浏览。但是由于我们要反向打印,所以我们必须开始打印最后一个元素。一旦到达最后一个元素,就无法再回到倒数第二个元素。

因此,我们要么必须进行大量重新遍历,要么必须构建一个辅助数据结构来跟踪所访问的元素,然后可以从中高效打印。

为什么我们的递归没有这个问题?因为在递归中,我们已经有一个辅助数据结构:函数调用堆栈。

由于递归允许我们返回到递归调用的上一个调用,而该调用的所有局部变量和状态仍保持不变,因此我们获得了一些灵活性,在迭代情况下进行建模很麻烦。


1
当然,第二个递归函数不是尾递归-由于无法使用TCO重用堆栈,因此优化空间要困难得多。实现双向链表将使两种算法都变得微不足道,这是以每个元素的指针/引用空间为代价的。
Baldrickk

@Baldrickk有关尾递归的有趣之处在于,最终得到的版本与循环版本的外观更加接近,因为它再次消除了将状态存储在调用堆栈中的能力。双链表可以解决此问题,但是遇到此问题时,通常不建议重新设计数据结构。尽管此处的示例受到了人为限制,但它说明了一种模式,该模式在递归代数类型的上下文中经常在功能语言中弹出。
ComicSansMS 2016年

我的观点是,如果遇到此问题,这更多是由于缺乏功能设计,而不是您使用哪种语言构造实现该问题,并且每种选择都有其自己的
优点

0

循环是实现特定任务(主要是迭代)的一种特殊形式的递归。可以用几种语言以相同的性能[1]以递归样式实现循环。在SICP [2]中,您可以看到for循环被描述为“合成糖”。在大多数命令式编程语言中,for和while块使用与其父函数相同的作用域。但是,在大多数函数式编程语言中,因为不需要循环,所以不存在for循环或while循环。

命令式语言具有for / while循环的原因是它们通过使状态变化来处理状态。但是实际上,如果您从不同的角度看待,如果您将while块视为一个函数本身,则需要对其进行参数处理,处理并返回新的状态-这也可能是同一函数具有不同参数的调用-可以将循环视为递归。

世界也可以定义为可变或不可变的。如果我们将世界定义为一组规则,并调用一个接受所有规则的终极函数,并将当前状态作为参数,然后根据这些具有相同功能的参数返回新状态(在同一状态下生成下一个状态方式),我们也可以说这是递归和循环。

在下面的示例中,life是函数采用两个参数“ rules”和“ state”,并且在下一次滴答中将构造新的状态。

life rules state = life rules new_state
    where new_state = construct_state_in_time rules state

[1]:尾部调用优化是函数式编程语言中的常见优化,它在递归调用中使用现有函数堆栈,而不是创建新函数。

[2]:麻省理工学院计算机程序的结构和解释。https://mitpress.mit.edu/books/structure-and-interpretation-computer-programs


4
@Giorgio不是我的不赞成,而是一个猜测:我认为大多数程序员都认为,递归意味着存在递归函数调用,因为,这就是递归函数本身。在循环中,没有递归函数调用。因此,如果按照此定义进行操作,则说没有递归函数调用的循环是一种特殊的递归形式将是绝对错误的。
海德

1
好吧,也许从更抽象的角度来看,似乎不同的事物实际上在概念上是相同的。我觉得很沮丧和悲伤而认为人们downvote的答案,只是因为他们不符合他们的期望,而不是把他们作为一个机会学到一些东西。试图说的所有答案:“嘿,看,这些东西表面上看起来不同,但实际上在抽象层次上是相同的”。
Giorgio

3
@Georgio:该站点的目的是获得问题的答案。有用和正确的答案值得赞扬,令人困惑和无用的答案值得赞扬。巧妙地使用一个通用术语的不同定义而不弄清楚使用哪个不同定义的答案是令人困惑和无助的。答案只有在您已经知道答案的情况下才有意义(可以这么说),它无济于事,仅用于显示作者对术语的高级掌握。
JacquesB '16

2
@JacquesB:“只有在您已经知道答案的情况下才有意义的答案,可以这么说,对我们没有帮助...”:这也只能说是确认读者已经知道或想知道的答案。如果答案引入了不清楚的术语,则可以在降低投票率之前写评论以要求更多详细信息。
Giorgio

4
循环不是递归的特殊形式。查看可计算性理论,例如理论WHILE语言和µ-微积分。是的,某些语言使用循环作为语法糖来实际在后台使用递归,但之所以可以这样做是因为递归和迭代具有同等的表达力,而不是因为它们是相同的。
Polygnome '16

-1

while循环与递归不同。

调用函数时,将发生以下情况:

  1. 堆栈框架已添加到堆栈中。

  2. 代码指针移到函数的开头。

当while循环结束时,将发生以下情况:

  1. 条件询问是否为真。

  2. 如果是这样,代码会跳到一个点。

通常,while循环类似于以下伪代码:

 if (x)

 {

      Jump_to(y);

 }

最重要的是,递归和循环具有不同的汇编代码表示形式和机器代码表示形式。这意味着它们不相同。它们可能具有相同的结果,但是不同的机器代码证明它们不是100%同一件事。


2
您正在谈论过程调用和while循环的实现,由于它们的实现方式不同,因此您可以得出结论,它们是不同的。但是,从概念上讲非常相似。
Giorgio

1
根据编译器的不同,优化的内联递归调用很可能会产生与普通循环相同的程序集。
海德

@hyde ...这仅仅是一个众所周知的事实的例子,一个事实可以通过另一个表达出来;并不意味着它们是相同的。有点像质量和能量。当然,可以说所有产生相同输出的方法都是“相同的”。如果世界是有限的,那么最后所有程序都是constexpr。
彼得-恢复莫妮卡

@Giorgio Nah,这是合乎逻辑的描述,而不是实现。我们知道两者是等价的 ; 但是等价不是同一性,因为问题(和答案)正是我们如何得出结果,即它们必然包含算法描述(可以用堆栈和变量等表示)。
彼得-恢复莫妮卡

1
@ PeterA.Schneider是的,但是这个答案说“在所有...不同的汇编代码中最重要的”,这不太正确。
海德

-1

仅仅迭代不足以等同于递归,但是使用堆栈的迭代通常等效。可以将任何递归函数重新编程为带有堆栈的迭代循环,反之亦然。但是,这并不意味着它是实用的,并且在任何特定情况下,一种或另一种形式可能比另一种形式具有明显的优势。

我不确定为什么会引起争议。堆栈的递归和迭代是相同的计算过程。可以说,它们是相同的“现象”。

我唯一能想到的是,当将它们视为“编程工具”时,我同意您不应将它们视为同一件事。它们在数学上或计算上都是等效的(同样是使用堆栈进行迭代,而不是通常进行迭代),但这并不意味着您应该以任何一个都可以做到的思想来对待问题。从实现/问题解决的角度来看,某些问题可能以一种或两种方式更好地解决问题,而作为程序员的工作是正确地确定哪个更适合。

为了澄清,问题的答案是while循环本质上是递归吗?是一个肯定的no,或者至少是“除非您也有堆栈”否则。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.