什么是尾递归?

发布于 2024-07-05 04:52:08 字数 45 浏览 7 评论 0原文

在开始学习 lisp 时,我遇到了“尾递归”这个术语。 它到底是什么意思?

Whilst starting to learn lisp, I've come across the term tail-recursive. What does it mean exactly?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(30

把梦留给海 2024-07-12 04:52:08

递归函数是一种自行调用的函数,

它允许程序员使用最少量的代码编写高效的程序。

缺点是,如果编写不当,它们可能导致无限循环和其他意外结果。

我将解释简单递归函数和尾递归函数

为了编写简单递归函数

  1. 首先要考虑的一点是你应该什么时候决定出来
    循环的内容就是if循环
  2. 第二个就是如果我们是自己的函数要做什么流程

从给定的例子来看:

public static int fact(int n){
  if(n <=1)
     return 1;
  else 
     return n * fact(n-1);
}

从上面的例子

if(n <=1)
     return 1;

来看 是何时退出循环的决定因素

else 
     return n * fact(n-1);

是实际要完成的处理吗?

为了便于理解,我将任务一一分解。

让我们看看如果我运行 fact(4)

  1. 替换 n=4
public static int fact(4){
  if(4 <=1)
     return 1;
  else 
     return 4 * fact(4-1);
}

If 循环失败,那么它会转到 else< /代码> 循环
所以它返回 4 *fact(3)

  1. 在堆栈内存中,我们有 4 *fact(3)

    替换 n=3

public static int fact(3){
  if(3 <=1)
     return 1;
  else 
     return 3 * fact(3-1);
}

If 循环失败,因此进入 else 循环

,因此返回 3 * fact(2)

请记住,我们调用了 ```4 * fact(3)` `

fact(3) = 3 *fact(2) 的输出

到目前为止,堆栈有 4 *fact(3) = 4 * 3 *fact(2)

  1. < p>在堆栈内存中,我们有4 * 3 *fact(2)

    替换 n=2

public static int fact(2){
  if(2 <=1)
     return 1;
  else 
     return 2 * fact(2-1);
}

If 循环失败,因此进入 else 循环

,因此返回 2 * fact(1)

请记住,我们调用了 4 * 3 *fact (2)

fact(2) = 2 * fact(1) 的输出

到目前为止,堆栈有 4 * 3 *fact(2) = 4 * 3 * 2 *fact(1)

  1. 在堆栈内存中,我们有4 * 3 * 2 *fact(1)

    代入 n=1

public static int fact(1){
  if(1 <=1)
     return 1;
  else 
     return 1 * fact(1-1);
}

If 循环为 true,

因此返回 1

请记住,我们调用了 4 * 3 * 2 * fact(1)

的输出fact(1) = 1

到目前为止,堆栈有 4 * 3 * 2 *fact(1) = 4 * 3 * 2 * 1

最后, fact( 4) = 4 * 3 * 2 * 1 = 24

在此处输入图像描述

尾递归

public static int fact(x, running_total=1) {
    if (x==1) {
        return running_total;
    } else {
        return fact(x-1, running_total*x);
    }
}

  1. 替换 n=4
public static int fact(4, running_total=1) {
    if (x==1) {
        return running_total;
    } else {
        return fact(4-1, running_total*4);
    }
}

If 循环失败,因此进入 else 循环
所以它返回 fact(3, 4)

  1. 在堆栈内存中,我们有 fact(3, 4)

    代入 n=3

public static int fact(3, running_total=4) {
    if (x==1) {
        return running_total;
    } else {
        return fact(3-1, 4*3);
    }
}

If 循环失败,因此进入 else 循环

,因此返回 fact(2, 12)

  1. 在堆栈内存中,我们有 fact (2, 12)

    代入 n=2

public static int fact(2, running_total=12) {
    if (x==1) {
        return running_total;
    } else {
        return fact(2-1, 12*2);
    }
}

If 循环失败,因此进入 else 循环

,因此返回 fact(1, 24)

  1. 在堆栈内存中,我们有 fact (1, 24)

    代入 n=1

public static int fact(1, running_total=24) {
    if (x==1) {
        return running_total;
    } else {
        return fact(1-1, 24*1);
    }
}

If 循环为真,

因此返回 running_total

running_total = 24 的输出

最后,fact(4,1) = 24< /strong>

在此输入图像描述

The recursive function is a function which calls by itself

It allows programmers to write efficient programs using a minimal amount of code.

The downside is that they can cause infinite loops and other unexpected results if not written properly.

I will explain both Simple Recursive function and Tail Recursive function

In order to write a Simple recursive function

  1. The first point to consider is when should you decide on coming out
    of the loop which is the if loop
  2. The second is what process to do if we are our own function

From the given example:

public static int fact(int n){
  if(n <=1)
     return 1;
  else 
     return n * fact(n-1);
}

From the above example

if(n <=1)
     return 1;

Is the deciding factor when to exit the loop

else 
     return n * fact(n-1);

Is the actual processing to be done

Let me the break the task one by one for easy understanding.

Let us see what happens internally if I run fact(4)

  1. Substituting n=4
public static int fact(4){
  if(4 <=1)
     return 1;
  else 
     return 4 * fact(4-1);
}

If loop fails so it goes to else loop
so it returns 4 * fact(3)

  1. In stack memory, we have 4 * fact(3)

    Substituting n=3

public static int fact(3){
  if(3 <=1)
     return 1;
  else 
     return 3 * fact(3-1);
}

If loop fails so it goes to else loop

so it returns 3 * fact(2)

Remember we called ```4 * fact(3)``

The output for fact(3) = 3 * fact(2)

So far the stack has 4 * fact(3) = 4 * 3 * fact(2)

  1. In stack memory, we have 4 * 3 * fact(2)

    Substituting n=2

public static int fact(2){
  if(2 <=1)
     return 1;
  else 
     return 2 * fact(2-1);
}

If loop fails so it goes to else loop

so it returns 2 * fact(1)

Remember we called 4 * 3 * fact(2)

The output for fact(2) = 2 * fact(1)

So far the stack has 4 * 3 * fact(2) = 4 * 3 * 2 * fact(1)

  1. In stack memory, we have 4 * 3 * 2 * fact(1)

    Substituting n=1

public static int fact(1){
  if(1 <=1)
     return 1;
  else 
     return 1 * fact(1-1);
}

If loop is true

so it returns 1

Remember we called 4 * 3 * 2 * fact(1)

The output for fact(1) = 1

So far the stack has 4 * 3 * 2 * fact(1) = 4 * 3 * 2 * 1

Finally, the result of fact(4) = 4 * 3 * 2 * 1 = 24

enter image description here

The Tail Recursion would be

public static int fact(x, running_total=1) {
    if (x==1) {
        return running_total;
    } else {
        return fact(x-1, running_total*x);
    }
}

  1. Substituting n=4
public static int fact(4, running_total=1) {
    if (x==1) {
        return running_total;
    } else {
        return fact(4-1, running_total*4);
    }
}

If loop fails so it goes to else loop
so it returns fact(3, 4)

  1. In stack memory, we have fact(3, 4)

    Substituting n=3

public static int fact(3, running_total=4) {
    if (x==1) {
        return running_total;
    } else {
        return fact(3-1, 4*3);
    }
}

If loop fails so it goes to else loop

so it returns fact(2, 12)

  1. In stack memory, we have fact(2, 12)

    Substituting n=2

public static int fact(2, running_total=12) {
    if (x==1) {
        return running_total;
    } else {
        return fact(2-1, 12*2);
    }
}

If loop fails so it goes to else loop

so it returns fact(1, 24)

  1. In stack memory, we have fact(1, 24)

    Substituting n=1

public static int fact(1, running_total=24) {
    if (x==1) {
        return running_total;
    } else {
        return fact(1-1, 24*1);
    }
}

If loop is true

so it returns running_total

The output for running_total = 24

Finally, the result of fact(4,1) = 24

enter image description here

葬﹪忆之殇 2024-07-12 04:52:08

这是一个比较两个函数的快速代码片段。 第一个是传统的递归,用于查找给定数字的阶乘。 第二种使用尾递归。

非常简单且直观易懂。

判断递归函数是否为尾递归的一个简单方法是看它是否在基本情况下返回具体值。 这意味着它不会返回 1 或 true 或类似的值。 它很可能会返回方法参数之一的某种变体。

另一种方法是判断递归调用是否没有任何加法、算术、修改等...这意味着它只是一个纯粹的递归调用。

public static int factorial(int mynumber) {
    if (mynumber == 1) {
        return 1;
    } else {            
        return mynumber * factorial(--mynumber);
    }
}

public static int tail_factorial(int mynumber, int sofar) {
    if (mynumber == 1) {
        return sofar;
    } else {
        return tail_factorial(--mynumber, sofar * mynumber);
    }
}

Here is a quick code snippet comparing two functions. The first is traditional recursion for finding the factorial of a given number. The second uses tail recursion.

Very simple and intuitive to understand.

An easy way to tell if a recursive function is a tail recursive is if it returns a concrete value in the base case. Meaning that it doesn't return 1 or true or anything like that. It will more than likely return some variant of one of the method parameters.

Another way is to tell is if the recursive call is free of any addition, arithmetic, modification, etc... Meaning its nothing but a pure recursive call.

public static int factorial(int mynumber) {
    if (mynumber == 1) {
        return 1;
    } else {            
        return mynumber * factorial(--mynumber);
    }
}

public static int tail_factorial(int mynumber, int sofar) {
    if (mynumber == 1) {
        return sofar;
    } else {
        return tail_factorial(--mynumber, sofar * mynumber);
    }
}
蓝戈者 2024-07-12 04:52:08

尾递归函数是一个递归函数,它在返回之前执行的最后一个操作是进行递归函数调用。 即立即返回递归函数调用的返回值。 例如,您的代码如下所示:

def recursiveFunction(some_params):
    # some code here
    return recursiveFunction(some_args)
    # no code after the return statement

实现尾调用优化尾调用消除的编译器和解释器可以优化递归代码以防止堆栈溢出。 如果您的编译器或解释器没有实现尾部调用优化(例如 CPython 解释器),那么以这种方式编写代码没有任何额外的好处。

例如,这是 Python 中的标准递归阶乘函数:

def factorial(number):
    if number == 1:
        # BASE CASE
        return 1
    else:
        # RECURSIVE CASE
        # Note that `number *` happens *after* the recursive call.
        # This means that this is *not* tail call recursion.
        return number * factorial(number - 1)

这是阶乘函数的尾部调用递归版本:(

def factorial(number, accumulator=1):
    if number == 0:
        # BASE CASE
        return accumulator
    else:
        # RECURSIVE CASE
        # There's no code after the recursive call.
        # This is tail call recursion:
        return factorial(number - 1, number * accumulator)
print(factorial(5))

请注意,即使这是 Python 代码,CPython 解释器也不执行尾部调用优化,因此安排您的像这样的代码不会带来运行时优势。)

您可能必须使代码更加难以阅读才能利用尾部调用优化,如阶乘示例中所示。 (例如,基本情况现在有点不直观,并且累加器参数实际上被用作一种全局变量。)

但是尾部调用优化的好处是它可以防止堆栈溢出错误。 (我会注意到,通过使用迭代算法而不是递归算法,您可以获得同样的好处。)

当调用堆栈上推送了太多帧对象时,就会导致堆栈溢出。 当调用函数时,框架对象被压入调用堆栈,并在函数返回时从调用堆栈中弹出。 框架对象包含局部变量以及函数返回时返回到哪一行代码等信息。

如果您的递归函数进行过多的递归调用而不返回,则调用堆栈可能会超出其帧对象限制。 (该数量因平台而异;在 Python 中默认为 1000 个框架对象。)这会导致堆栈溢出错误。 (嘿,这就是这个网站名称的由来!)

但是,如果递归函数所做的最后一件事是进行递归调用并返回其返回值,那么它就没有理由需要保留当前帧对象在调用堆栈上。 毕竟,如果递归函数调用后没有代码,则没有理由保留当前帧对象的局部变量。 因此我们可以立即删除当前帧对象,而不是将其保留在调用堆栈上。 最终结果是您的调用堆栈的大小不会增加,因此不会堆栈溢出。

编译器或解释器必须将尾调用优化作为一项功能,以便能够识别何时可以应用尾调用优化。 即使如此,您也可能会重新排列递归函数中的代码以利用尾调用优化,而这种潜在的可读性下降是否值得优化则取决于您。

A tail recursive function is a recursive function where the last operation it does before returning is make the recursive function call. That is, the return value of the recursive function call is immediately returned. For example, your code would look like this:

def recursiveFunction(some_params):
    # some code here
    return recursiveFunction(some_args)
    # no code after the return statement

Compilers and interpreters that implement tail call optimization or tail call elimination can optimize recursive code to prevent stack overflows. If your compiler or interpreter doesn't implement tail call optimization (such as the CPython interpreter) there is no additional benefit to writing your code this way.

For example, this is a standard recursive factorial function in Python:

def factorial(number):
    if number == 1:
        # BASE CASE
        return 1
    else:
        # RECURSIVE CASE
        # Note that `number *` happens *after* the recursive call.
        # This means that this is *not* tail call recursion.
        return number * factorial(number - 1)

And this is a tail call recursive version of the factorial function:

def factorial(number, accumulator=1):
    if number == 0:
        # BASE CASE
        return accumulator
    else:
        # RECURSIVE CASE
        # There's no code after the recursive call.
        # This is tail call recursion:
        return factorial(number - 1, number * accumulator)
print(factorial(5))

(Note that even though this is Python code, the CPython interpreter doesn't do tail call optimization, so arranging your code like this confers no runtime benefit.)

You may have to make your code a bit more unreadable to make use of tail call optimization, as shown in the factorial example. (For example, the base case is now a bit unintuitive, and the accumulator parameter is effectively used as a sort of global variable.)

But the benefit of tail call optimization is that it prevents stack overflow errors. (I'll note that you can get this same benefit by using an iterative algorithm instead of a recursive one.)

Stack overflows are caused when the call stack has had too many frame objects pushed onto. A frame object is pushed onto the call stack when a function is called, and popped off the call stack when the function returns. Frame objects contain info such as local variables and what line of code to return to when the function returns.

If your recursive function makes too many recursive calls without returning, the call stack can exceed its frame object limit. (The number varies by platform; in Python it is 1000 frame objects by default.) This causes a stack overflow error. (Hey, that's where the name of this website comes from!)

However, if the last thing your recursive function does is make the recursive call and return its return value, then there's no reason it needs to keep the current frame object needs to stay on the call stack. After all, if there's no code after the recursive function call, there's no reason to hang on to the current frame object's local variables. So we can get rid of the current frame object immediately rather than keep it on the call stack. The end result of this is that your call stack doesn't grow in size, and thus cannot stack overflow.

A compiler or interpreter must have tail call optimization as a feature for it to be able to recognize when tail call optimization can be applied. Even then, you may have rearrange the code in your recursive function to make use of tail call optimization, and it's up to you if this potential decrease in readability is worth the optimization.

巡山小妖精 2024-07-12 04:52:08

这个问题有很多很好的答案......但我忍不住插话如何定义“尾递归”,或者至少是“正确的尾递归”。 即:是否应该将其视为程序中特定表达式的属性? 或者我们应该将其视为编程语言实现的一种属性?

有关后一种观点的更多信息,有一个经典的 Will Clinger 的论文,“正确的尾部递归和空间效率”(PLDI 1998),将“正确的尾部递归”定义为编程语言实现的属性。 该定义的构造是为了允许忽略实现细节(例如调用堆栈实际上是通过运行时堆栈还是通过堆分配的帧链表表示)。

为了实现这一目标,它使用渐近分析:不是像通常看到的那样分析程序执行时间,而是程序空间使用。 这样,堆分配链表与运行时调用堆栈的空间使用最终是渐近等效的; 因此,人们会忽略编程语言的实现细节(这一细节在实践中确实非常重要,但是当人们试图确定给定的实现是否满足“属性尾递归”的要求时,可能会使情况变得更加混乱)

这篇论文值得仔细研究,原因如下:

  • 它给出了程序的尾部表达式尾部调用的归纳定义。 (这样的定义以及为什么这样的调用很重要,似乎是此处给出的大多数其他答案的主题。)

    以下是这些定义,只是为了提供文本的风格:

    <块引用>

    定义1用CoreScheme编写的程序的尾部表达式归纳定义如下。

    1. lambda 表达式的主体是尾表达式
    2. 如果(if E0 E1 E2)是尾部表达式,则E1E2都是尾部表达式。
    3. 没有别的东西是尾部表达式。

    定义 2尾部调用是作为过程调用的尾部表达式。

(尾部递归调用,或者如论文所述,“自尾部调用”是尾部调用的一种特殊情况,其中过程本身被调用。)

  • 它为用于评估 Core 的六种不同“机器”提供了正式定义方案,其中每台机器都具有相同的可观察行为,除了各自所处的渐近空间复杂度类之外。

    例如,在分别给出了以下机器的定义:1.基于堆栈的内存管理,2.垃圾收集但无尾部调用,3.垃圾收集和尾部调用,本文继续介绍更高级的存储管理策略,例如 4.“evlis 尾递归”,其中在尾部调用中最后一个子表达式参数的求值过程中不需要保留环境,5.将闭包的环境减少到只是 该闭包的自由变量,以及 6. 由 Appel 和 Shao

  • 为了证明这些机器实际上属于六个不同的空间复杂度类别,本文针对每对进行比较的机器,提供了具体的程序示例,将在一台机器上暴露渐近空间爆炸,而在另一台机器上则不会。


(现在阅读我的答案,我不确定我是否能够真正抓住Clinger paper。但是,唉,我不能现在就花更多时间来制定这个答案。)

This question has a lot of great answers... but I cannot help but chime in with an alternative take on how to define "tail recursion", or at least "proper tail recursion." Namely: should one look at it as a property of a particular expression in a program? Or should one look at it as a property of an implementation of a programming language?

For more on the latter view, there is a classic paper by Will Clinger, "Proper Tail Recursion and Space Efficiency" (PLDI 1998), that defined "proper tail recursion" as a property of a programming language implementation. The definition is constructed to allow one to ignore implementation details (such as whether the call stack is actually represented via the runtime stack or via a heap-allocated linked list of frames).

To accomplish this, it uses asymptotic analysis: not of program execution time as one usually sees, but rather of program space usage. This way, the space usage of a heap-allocated linked list vs a runtime call stack ends up being asymptotically equivalent; so one gets to ignore that programming language implementation detail (a detail which certainly matters quite a bit in practice, but can muddy the waters quite a bit when one attempts to determine whether a given implementation is satisfying the requirement to be "property tail recursive")

The paper is worth careful study for a number of reasons:

  • It gives an inductive definition of the tail expressions and tail calls of a program. (Such a definition, and why such calls are important, seems to be the subject of most of the other answers given here.)

    Here are those definitions, just to provide a flavor of the text:

    Definition 1 The tail expressions of a program written in Core Scheme are defined inductively as follows.

    1. The body of a lambda expression is a tail expression
    2. If (if E0 E1 E2) is a tail expression, then both E1 and E2 are tail expressions.
    3. Nothing else is a tail expression.

    Definition 2 A tail call is a tail expression that is a procedure call.

(a tail recursive call, or as the paper says, "self-tail call" is a special case of a tail call where the procedure is invoked itself.)

  • It provides formal definitions for six different "machines" for evaluating Core Scheme, where each machine has the same observable behavior except for the asymptotic space complexity class that each is in.

    For example, after giving definitions for machines with respectively, 1. stack-based memory management, 2. garbage collection but no tail calls, 3. garbage collection and tail calls, the paper continues onward with even more advanced storage management strategies, such as 4. "evlis tail recursion", where the environment does not need to be preserved across the evaluation of the last sub-expression argument in a tail call, 5. reducing the environment of a closure to just the free variables of that closure, and 6. so-called "safe-for-space" semantics as defined by Appel and Shao.

  • In order to prove that the machines actually belong to six distinct space complexity classes, the paper, for each pair of machines under comparison, provides concrete examples of programs that will expose asymptotic space blowup on one machine but not the other.


(Reading over my answer now, I'm not sure if I'm managed to actually capture the crucial points of the Clinger paper. But, alas, I cannot devote more time to developing this answer right now.)

初懵 2024-07-12 04:52:08

递归有两种基本类型:头递归尾递归。

头递归中,函数进行递归调用,然后
执行更多计算,也许使用的结果
例如,递归调用。

尾递归函数中,所有计算首先发生并且
递归调用是最后发生的事情。

摘自这篇超级棒的帖子。
请考虑阅读它。

There are two basic kinds of recursions: head recursion and tail recursion.

In head recursion, a function makes its recursive call and then
performs some more calculations, maybe using the result of the
recursive call, for example.

In a tail recursive function, all calculations happen first and
the recursive call is the last thing that happens.

Taken from this super awesome post.
Please consider reading it.

三生池水覆流年 2024-07-12 04:52:08

递归意味着函数调用自身。 例如:

(define (un-ended name)
  (un-ended 'me)
  (print "How can I get here?"))

Tail-Recursion 表示结束函数的递归:

(define (un-ended name)
  (print "hello")
  (un-ended 'me))

看,未结束的函数(过程,在方案术语中)所做的最后一件事就是调用自身。 另一个(更有用的)例子是:

(define (map lst op)
  (define (helper done left)
    (if (nil? left)
        done
        (helper (cons (op (car left))
                      done)
                (cdr left))))
  (reverse (helper '() lst)))

在辅助过程中,如果 left 不为零,它所做的最后一件事是调用自身(在 cons Something 和 cdr Something 之后)。 这基本上就是映射列表的方式。

尾递归有一个很大的优点,解释器(或编译器,取决于语言和供应商)可以优化它,并将其转换为相当于 while 循环的东西。 事实上,在Scheme传统中,大多数“for”和“while”循环都是以尾递归方式完成的(据我所知,没有for和while)。

Recursion means a function calling itself. For example:

(define (un-ended name)
  (un-ended 'me)
  (print "How can I get here?"))

Tail-Recursion means the recursion that conclude the function:

(define (un-ended name)
  (print "hello")
  (un-ended 'me))

See, the last thing un-ended function (procedure, in Scheme jargon) does is to call itself. Another (more useful) example is:

(define (map lst op)
  (define (helper done left)
    (if (nil? left)
        done
        (helper (cons (op (car left))
                      done)
                (cdr left))))
  (reverse (helper '() lst)))

In the helper procedure, the LAST thing it does if the left is not nil is to call itself (AFTER cons something and cdr something). This is basically how you map a list.

The tail-recursion has a great advantage that the interpreter (or compiler, dependent on the language and vendor) can optimize it, and transform it into something equivalent to a while loop. As matter of fact, in Scheme tradition, most "for" and "while" loop is done in a tail-recursion manner (there is no for and while, as far as I know).

反差帅 2024-07-12 04:52:08

为了了解尾调用递归和非尾调用递归之间的一些核心区别,我们可以探索这些技术的 .NET 实现。

以下文章包含一些 C#、F# 和 C++\CLI 示例: C#、F# 和 C++\CLI 中的尾递归冒险

C# 不会优化尾部调用递归,而 F# 会优化。

原理上的差异涉及循环与 Lambda 演算。 C# 的设计考虑了循环,而 F# 是根据 Lambda 演算原理构建的。 有关 Lambda 演算原理的一本非常好的(免费)书籍,请参阅 计算机程序的结构和解释,作者:阿贝尔森、萨斯曼和萨斯曼

关于 F# 中的尾部调用,有关非常好的介绍性文章,请参阅 F# 中尾部调用详细介绍。 最后,这里有一篇文章介绍了非尾递归和尾调用递归(在 F# 中)之间的区别:F 升调中的尾递归与非尾递归

如果您想了解 C# 和 F# 之间尾部调用递归的一些设计差异,请参阅生成尾部- 在 C# 和 F# 中调用操作码

如果您非常想知道哪些条件会阻止 C# 编译器执行尾部调用优化,请参阅本文:JIT CLR 尾调用条件

To understand some of the core differences between tail-call recursion and non-tail-call recursion we can explore the .NET implementations of these techniques.

Here is an article with some examples in C#, F#, and C++\CLI: Adventures in Tail Recursion in C#, F#, and C++\CLI.

C# does not optimize for tail-call recursion whereas F# does.

The differences of principle involve loops vs. Lambda calculus. C# is designed with loops in mind whereas F# is built from the principles of Lambda calculus. For a very good (and free) book on the principles of Lambda calculus, see Structure and Interpretation of Computer Programs, by Abelson, Sussman, and Sussman.

Regarding tail calls in F#, for a very good introductory article, see Detailed Introduction to Tail Calls in F#. Finally, here is an article that covers the difference between non-tail recursion and tail-call recursion (in F#): Tail-recursion vs. non-tail recursion in F sharp.

If you want to read about some of the design differences of tail-call recursion between C# and F#, see Generating Tail-Call Opcode in C# and F#.

If you care enough to want to know what conditions prevent the C# compiler from performing tail-call optimizations, see this article: JIT CLR tail-call conditions.

一个人的夜不怕黑 2024-07-12 04:52:08

对我来说,理解尾调用递归的最好方法是递归的一种特殊情况,其中最后一次调用(或尾调用)是函数本身。

比较Python中提供的示例:

def recsum(x):
 if x == 1:
  return x
 else:
  return x + recsum(x - 1)

^RECURSION

def tailrecsum(x, running_total=0):
  if x == 0:
    return running_total
  else:
    return tailrecsum(x - 1, running_total + x)

^TAIL RECURSION

正如您在一般递归版本中看到的,代码块中的最终调用是x + recsum(x - 1)。 所以在调用recsum方法之后,还有一个操作是x + .. 。

然而,在尾递归版本中,代码块中的最终调用(或尾调用)是 tailrecsum(x - 1, running_total + x) 这意味着最后一次调用是对方法本身进行的之后就没有任何操作了。

这一点很重要,因为这里看到的尾递归不会使内存增长,因为当底层虚拟机看到一个函数在尾部位置(函数中要计算的最后一个表达式)调用自身时,它会消除当前的堆栈帧,这称为尾调用优化(TCO)。

编辑

注意。 请记住,上面的示例是用 Python 编写的,其运行时不支持 TCO。 这只是一个例子来解释这一点。 Scheme、Haskell 等语言支持 TCO

The best way for me to understand tail call recursion is a special case of recursion where the last call(or the tail call) is the function itself.

Comparing the examples provided in Python:

def recsum(x):
 if x == 1:
  return x
 else:
  return x + recsum(x - 1)

^RECURSION

def tailrecsum(x, running_total=0):
  if x == 0:
    return running_total
  else:
    return tailrecsum(x - 1, running_total + x)

^TAIL RECURSION

As you can see in the general recursive version, the final call in the code block is x + recsum(x - 1). So after calling the recsum method, there is another operation which is x + ...

However, in the tail recursive version, the final call(or the tail call) in the code block is tailrecsum(x - 1, running_total + x) which means the last call is made to the method itself and no operation after that.

This point is important because tail recursion as seen here is not making the memory grow because when the underlying VM sees a function calling itself in a tail position (the last expression to be evaluated in a function), it eliminates the current stack frame, which is known as Tail Call Optimization(TCO).

EDIT

NB. Do bear in mind that the example above is written in Python whose runtime does not support TCO. This is just an example to explain the point. TCO is supported in languages like Scheme, Haskell etc

故人如初 2024-07-12 04:52:08

这意味着您不需要将指令指针压入堆栈,只需跳转到递归函数的顶部并继续执行即可。 这允许函数无限递归而不会溢出堆栈。

我在 http://blogs.msdn.com/chrsmith/archive/2008/08/07/understanding-tail-recursion.aspx" rel="noreferrer">博客 上发表了一篇文章主题,其中有堆栈帧的图形示例。

It means that rather than needing to push the instruction pointer on the stack, you can simply jump to the top of a recursive function and continue execution. This allows for functions to recurse indefinitely without overflowing the stack.

I wrote a blog post on the subject, which has graphical examples of what the stack frames look like.

执笔绘流年 2024-07-12 04:52:08

尾递归是指递归算法中最后一条逻辑指令中的最后一次递归调用。

通常在递归中,您有一个基本情况,它可以停止递归调用并开始弹出调用堆栈。 举一个经典的例子,尽管阶乘函数比 Lisp 更接近 C,但它说明了尾递归。 检查基本情况条件后,递归调用发生。

factorial(x, fac=1) {
  if (x == 1)
     return fac;
   else
     return factorial(x-1, x*fac);
}

对阶乘的初始调用为 factorial(n),其中 fac=1(默认值),n 是要计算阶乘的数字。

Tail recursion refers to the recursive call being last in the last logic instruction in the recursive algorithm.

Typically in recursion, you have a base-case which is what stops the recursive calls and begins popping the call stack. To use a classic example, though more C-ish than Lisp, the factorial function illustrates tail recursion. The recursive call occurs after checking the base-case condition.

factorial(x, fac=1) {
  if (x == 1)
     return fac;
   else
     return factorial(x-1, x*fac);
}

The initial call to factorial would be factorial(n) where fac=1 (default value) and n is the number for which the factorial is to be calculated.

后来的我们 2024-07-12 04:52:08

我不是 Lisp 程序员,但我认为会有帮助。

基本上,这是一种编程风格,递归调用是您所做的最后一件事。

I'm not a Lisp programmer, but I think this will help.

Basically it's a style of programming such that the recursive call is the last thing you do.

︶葆Ⅱㄣ 2024-07-12 04:52:08

尾递归就是你现在的生活。 您不断地一遍又一遍地回收相同的堆栈帧,因为没有理由或方法返回到“前一个”帧。 过去的事情已经过去了,所以可以抛弃它。 你得到一帧,永远进入未来,直到你的进程不可避免地消亡。

当您考虑某些进程可能会利用额外的帧但如果堆栈不无限增长时仍被视为尾递归时,这个类比就不成立了。

Tail recursion is the life you are living right now. You constantly recycle the same stack frame, over and over, because there's no reason or means to return to a "previous" frame. The past is over and done with so it can be discarded. You get one frame, forever moving into the future, until your process inevitably dies.

The analogy breaks down when you consider some processes might utilize additional frames but are still considered tail-recursive if the stack does not grow infinitely.

痴情 2024-07-12 04:52:08

与普通递归相比,尾递归相当快。
它很快,因为祖先调用的输出不会写入堆栈中以保持跟踪。
但在正常递归中,所有祖先都调用堆栈中写入的输出来保持跟踪。

Tail Recursion is pretty fast as compared to normal recursion.
It is fast because the output of the ancestors call will not be written in stack to keep the track.
But in normal recursion all the ancestor calls output written in stack to keep the track.

水染的天色ゝ 2024-07-12 04:52:08

如果每个递归情况包含对函数本身的调用(可能具有不同的参数),则函数是尾递归的。 或者,尾递归是没有待处理工作的递归。 请注意,这是一个独立于编程语言的概念。

考虑函数定义为:

g(a, b, n) = a * b^n

一个可能的尾递归公式是:

g(a, b, n) | n is zero = a
           | n is odd  = g(a*b, b,   n-1)
           | otherwise = g(a,   b*b, n/2)

如果您检查涉及递归情况的 g(...) 的每个 RHS,您会发现 RHS 的整个主体是对 g(...) 的调用,并且。 这个定义是尾递归

为了进行比较,非尾递归公式可能是:

g'(a, b, n) = a * f(b, n)
f(b, n) | n is zero = 1
        | n is odd  = f(b, n-1) * b
        | otherwise = f(b, n/2) ^ 2

f(...) 中的每个递归情况都有一些需要在递归调用之后发生的待处理工作

请注意,当我们从 g'g 时,我们充分利用了关联性
乘法(和交换律)。 这并非偶然,大多数需要将递归转换为尾递归的情况都会利用这样的属性:如果我们想急切地做一些工作而不是让它悬而未决,我们必须使用诸如关联性之类的东西来证明答案是一样的。

尾递归调用可以通过向后跳转来实现,而不是使用堆栈进行普通递归调用。 请注意,检测尾部调用或发出向后跳转通常很简单。 然而,通常很难重新排列参数以使向后跳转成为可能。 由于此优化不是免费的,因此语言实现可以选择不实现此优化,或者需要通过使用“tailcall”指令标记递归调用和/或选择更高的优化设置来选择加入。

然而,某些语言(例如Scheme)确实需要所有实现来优化尾递归函数,甚至可能是尾部位置的所有调用。

在大多数命令式语言中,向后跳转通常被抽象为 (while) 循环,而尾递归在优化为向后跳转时,与循环同构。

A function is tail recursive if each recursive case consists only of a call to the function itself, possibly with different arguments. Or, tail recursion is recursion with no pending work. Note that this is a programming-language independent concept.

Consider the function defined as:

g(a, b, n) = a * b^n

A possible tail-recursive formulation is:

g(a, b, n) | n is zero = a
           | n is odd  = g(a*b, b,   n-1)
           | otherwise = g(a,   b*b, n/2)

If you examine each RHS of g(...) that involves a recursive case, you'll find that the whole body of the RHS is a call to g(...), and only that. This definition is tail recursive.

For comparison, a non-tail-recursive formulation might be:

g'(a, b, n) = a * f(b, n)
f(b, n) | n is zero = 1
        | n is odd  = f(b, n-1) * b
        | otherwise = f(b, n/2) ^ 2

Each recursive case in f(...) has some pending work that needs to happen after the recursive call.

Note that when we went from g' to g, we made essential use of associativity
(and commutativity) of multiplication. This is not an accident, and most cases where you will need to transform recursion to tail-recursion will make use of such properties: if we want to eagerly do some work rather than leave it pending, we have to use something like associativity to prove that the answer will be the same.

Tail recursive calls can be implemented with a backwards jump, as opposed to using a stack for normal recursive calls. Note that detecting a tail call, or emitting a backwards jump is usually straightforward. However, it is often hard to rearrange the arguments such that the backwards jump is possible. Since this optimization is not free, language implementations can choose not to implement this optimization, or require opt-in by marking recursive calls with a 'tailcall' instruction and/or choosing a higher optimization setting.

Some languages (e.g. Scheme) do, however, require all implementations to optimize tail-recursive functions, maybe even all calls in tail position.

Backwards jumps are usually abstracted as a (while) loop in most imperative languages, and tail-recursion, when optimized to a backwards jump, is isomorphic to looping.

冷︶言冷语的世界 2024-07-12 04:52:08

尾递归是函数调用的递归函数
本身位于函数的末尾(“尾部”),其中不进行任何计算
递归调用返回后完成。 许多编译器优化为
将递归调用更改为尾递归或迭代调用。

考虑计算数字的阶乘问题。

一个简单的方法是:

  factorial(n):

    if n==0 then 1

    else n*factorial(n-1)

假设您调用阶乘(4)。 递归树将是:

       factorial(4)
       /        \
      4      factorial(3)
     /             \
    3          factorial(2)
   /                  \
  2                factorial(1)
 /                       \
1                       factorial(0)
                            \
                             1    

上述情况的最大递归深度是 O(n)。

但是,请考虑以下示例:

factAux(m,n):
if n==0  then m;
else     factAux(m*n,n-1);

factTail(n):
   return factAux(1,n);

factTail(4) 的递归树将是:

factTail(4)
   |
factAux(1,4)
   |
factAux(4,3)
   |
factAux(12,2)
   |
factAux(24,1)
   |
factAux(24,0)
   |
  24

这里,最大递归深度也是 O(n),但没有任何调用将任何额外变量添加到堆栈中。 因此编译器可以取消堆栈。

A tail recursion is a recursive function where the function calls
itself at the end ("tail") of the function in which no computation is
done after the return of recursive call. Many compilers optimize to
change a recursive call to a tail recursive or an iterative call.

Consider the problem of computing factorial of a number.

A straightforward approach would be:

  factorial(n):

    if n==0 then 1

    else n*factorial(n-1)

Suppose you call factorial(4). The recursion tree would be:

       factorial(4)
       /        \
      4      factorial(3)
     /             \
    3          factorial(2)
   /                  \
  2                factorial(1)
 /                       \
1                       factorial(0)
                            \
                             1    

The maximum recursion depth in the above case is O(n).

However, consider the following example:

factAux(m,n):
if n==0  then m;
else     factAux(m*n,n-1);

factTail(n):
   return factAux(1,n);

Recursion tree for factTail(4) would be:

factTail(4)
   |
factAux(1,4)
   |
factAux(4,3)
   |
factAux(12,2)
   |
factAux(24,1)
   |
factAux(24,0)
   |
  24

Here also, maximum recursion depth is O(n) but none of the calls adds any extra variable to the stack. Hence the compiler can do away with a stack.

攀登最高峰 2024-07-12 04:52:08

这是前面提到的 tailrecsum 函数的 Perl 5 版本。

sub tail_rec_sum($;$){
  my( $x,$running_total ) = (@_,0);

  return $running_total unless $x;

  @_ = ($x-1,$running_total+$x);
  goto &tail_rec_sum; # throw away current stack frame
}

here is a Perl 5 version of the tailrecsum function mentioned earlier.

sub tail_rec_sum($;$){
  my( $x,$running_total ) = (@_,0);

  return $running_total unless $x;

  @_ = ($x-1,$running_total+$x);
  goto &tail_rec_sum; # throw away current stack frame
}
回眸一遍 2024-07-12 04:52:08

这是关于尾递归的计算机程序的结构和解释的摘录。

在对比迭代和递归时,我们必须注意不要
将递归过程的概念与递归过程的概念混淆
递归过程。 当我们将一个过程描述为递归时,我们是
指的是过程定义所指的语法事实
(直接或间接)到程序本身。 但当我们
将一个过程描述为遵循某种模式,即线性
递归,我们谈论的是过程如何演变,而不是
如何编写过程的语法。 这可能看起来令人不安
我们将诸如fact-iter之类的递归过程称为生成
迭代过程。 然而,这个过程确实是迭代的:它的状态
完全由其三个状态变量捕获,并且
解释器只需要跟踪三个变量即可
执行该过程。

过程和过程之间的区别可能的一个原因是
令人困惑的是,大多数通用语言的实现(包括 Ada、Pascal 和
C) 的设计方式使得任何递归的解释
程序消耗的内存量随着程序数量的增加而增加
过程调用,即使所描述的过程原则上是:
迭代。 因此,这些语言可以描述迭代
仅通过诉诸特殊用途的“循环结构”来进行处理
例如 do、repeat、until、for 和 while。 实施
方案没有这个缺陷。 它
将在恒定空间中执行迭代过程,即使
迭代过程由递归过程描述。 一个
具有此属性的实现称为尾递归。

尾递归实现,迭代可以使用
普通的过程调用机制,这样特殊的迭代
结构仅作为语法糖有用。

This is an excerpt from Structure and Interpretation of Computer Programs about tail recursion.

In contrasting iteration and recursion, we must be careful not to
confuse the notion of a recursive process with the notion of a
recursive procedure. When we describe a procedure as recursive, we are
referring to the syntactic fact that the procedure definition refers
(either directly or indirectly) to the procedure itself. But when we
describe a process as following a pattern that is, say, linearly
recursive, we are speaking about how the process evolves, not about
the syntax of how a procedure is written. It may seem disturbing that
we refer to a recursive procedure such as fact-iter as generating an
iterative process. However, the process really is iterative: Its state
is captured completely by its three state variables, and an
interpreter need keep track of only three variables in order to
execute the process.

One reason that the distinction between process and procedure may be
confusing is that most implementations of common languages (including Ada, Pascal, and
C) are designed in such a way that the interpretation of any recursive
procedure consumes an amount of memory that grows with the number of
procedure calls, even when the process described is, in principle,
iterative. As a consequence, these languages can describe iterative
processes only by resorting to special-purpose “looping constructs”
such as do, repeat, until, for, and while. The implementation of
Scheme does not share this defect. It
will execute an iterative process in constant space, even if the
iterative process is described by a recursive procedure. An
implementation with this property is called tail-recursive.
With a
tail-recursive implementation, iteration can be expressed using the
ordinary procedure call mechanism, so that special iteration
constructs are useful only as syntactic sugar.

当梦初醒 2024-07-12 04:52:08

在 Java 中,以下是斐波那契函数的可能尾部递归实现:

public int tailRecursive(final int n) {
    if (n <= 2)
        return 1;
    return tailRecursiveAux(n, 1, 1);
}

private int tailRecursiveAux(int n, int iter, int acc) {
    if (iter == n)
        return acc;
    return tailRecursiveAux(n, ++iter, acc + iter);
}

将其与标准递归实现进行对比:

public int recursive(final int n) {
    if (n <= 2)
        return 1;
    return recursive(n - 1) + recursive(n - 2);
}

In Java, here's a possible tail recursive implementation of the Fibonacci function:

public int tailRecursive(final int n) {
    if (n <= 2)
        return 1;
    return tailRecursiveAux(n, 1, 1);
}

private int tailRecursiveAux(int n, int iter, int acc) {
    if (iter == n)
        return acc;
    return tailRecursiveAux(n, ++iter, acc + iter);
}

Contrast this with the standard recursive implementation:

public int recursive(final int n) {
    if (n <= 2)
        return 1;
    return recursive(n - 1) + recursive(n - 2);
}
动听の歌 2024-07-12 04:52:08

这是一个使用尾递归进行阶乘的 Common Lisp 示例。 由于无堆栈的性质,人们可以执行非常大的阶乘计算......

(defun ! (n &optional (product 1))
    (if (zerop n) product
        (! (1- n) (* product n))))

然后为了好玩,你可以尝试 (format nil "~R" (! 25))

Here is a Common Lisp example that does factorials using tail-recursion. Due to the stack-less nature, one could perform insanely large factorial computations ...

(defun ! (n &optional (product 1))
    (if (zerop n) product
        (! (1- n) (* product n))))

And then for fun you could try (format nil "~R" (! 25))

纸伞微斜 2024-07-12 04:52:08

简而言之,尾递归将递归调用作为函数中的last语句,因此不必等待递归调用。

所以这是一个尾递归,即 N(x - 1, p * x) 是函数中的最后一个语句,编译器很聪明地发现它可以优化为 for 循环(阶乘)。 第二个参数p携带中间产品值。

function N(x, p) {
   return x == 1 ? p : N(x - 1, p * x);
}

这是编写上述阶乘函数的非尾递归方式(尽管某些 C++ 编译器无论如何都可以对其进行优化)。

function N(x) {
   return x == 1 ? 1 : x * N(x - 1);
}

但这不是:

function F(x) {
  if (x == 1) return 0;
  if (x == 2) return 1;
  return F(x - 1) + F(x - 2);
}

我确实写了一篇标题为“理解的长篇文章尾递归 – Visual Studio C++ – 程序集视图"

在此处输入图像描述

In short, a tail recursion has the recursive call as the last statement in the function so that it doesn't have to wait for the recursive call.

So this is a tail recursion i.e. N(x - 1, p * x) is the last statement in the function where the compiler is clever to figure out that it can be optimised to a for-loop (factorial). The second parameter p carries the intermediate product value.

function N(x, p) {
   return x == 1 ? p : N(x - 1, p * x);
}

This is the non-tail-recursive way of writing the above factorial function (although some C++ compilers may be able to optimise it anyway).

function N(x) {
   return x == 1 ? 1 : x * N(x - 1);
}

but this is not:

function F(x) {
  if (x == 1) return 0;
  if (x == 2) return 1;
  return F(x - 1) + F(x - 2);
}

I did write a long post titled "Understanding Tail Recursion – Visual Studio C++ – Assembly View"

enter image description here

泡沫很甜 2024-07-12 04:52:08

重要的一点是尾递归本质上等同于循环。 这不仅仅是编译器优化的问题,而是关于表达能力的基本事实。 这是双向的:您可以采用任何形式的循环

while(E) { S }; return Q

,其中 EQ 是表达式,S 是语句序列,然后将当然

f() = if E then { S; return f() } else { return Q }

,必须定义ESQ来计算某些变量的一些有趣的值。 例如,循环函数

sum(n) {
  int i = 1, k = 0;
  while( i <= n ) {
    k += i;
    ++i;
  }
  return k;
}

相当于尾递归函数

sum_aux(n,i,k) {
  if( i <= n ) {
    return sum_aux(n,i+1,k+i);
  } else {
    return k;
  }
}

sum(n) {
  return sum_aux(n,1,0);
}

(用参数较少的函数“包装”尾递归函数是常见的函数惯用语。)

An important point is that tail recursion is essentially equivalent to looping. It's not just a matter of compiler optimization, but a fundamental fact about expressiveness. This goes both ways: you can take any loop of the form

while(E) { S }; return Q

where E and Q are expressions and S is a sequence of statements, and turn it into a tail recursive function

f() = if E then { S; return f() } else { return Q }

Of course, E, S, and Q have to be defined to compute some interesting value over some variables. For example, the looping function

sum(n) {
  int i = 1, k = 0;
  while( i <= n ) {
    k += i;
    ++i;
  }
  return k;
}

is equivalent to the tail-recursive function(s)

sum_aux(n,i,k) {
  if( i <= n ) {
    return sum_aux(n,i+1,k+i);
  } else {
    return k;
  }
}

sum(n) {
  return sum_aux(n,1,0);
}

(This "wrapping" of the tail-recursive function with a function with fewer parameters is a common functional idiom.)

世态炎凉 2024-07-12 04:52:08

这段摘录自《Programming in Lua》一书,展示了如何进行正确的尾递归 (在 Lua 中,但也应该适用于 Lisp)以及为什么它更好。

尾调用 [尾递归] 是一种 goto 修饰
作为呼叫。 当一个尾调用发生时
函数调用另一个函数作为其最后一个函数
行动,所以它没有其他事可做。
例如,在下面的代码中,
g 的调用是尾调用:

函数 f (x) 
    返回 g(x) 
  结尾 
  

f调用g之后,就没有别的了
去做。 在这种情况下,程序
不需要返回调用方
函数被调用时的函数
结束。 因此,在尾部调用之后,
该程序不需要保留任何
有关调用函数的信息
在堆栈中。 ...

因为正确的尾调用不使用
堆栈空间,没有限制
“嵌套”尾部调用的数量
程序可以使。 例如,我们可以
使用任何调用以下函数
数字作为参数; 它永远不会
堆栈溢出:

函数 foo (n) 
    如果n>   0 然后返回 foo(n - 1) 结束 
  结尾 
  

...正如我之前所说,尾部调用是
有点转到。 因此,一个非常有用的
正确尾部调用的应用
Lua 用于状态机编程。
此类应用程序可以代表每个
通过函数来​​表示; 改变状态
是去(或打电话)一个特定的
功能。 举个例子,让我们
考虑一个简单的迷宫游戏。 迷宫
有多个房间,每个房间最多可容纳
四门:东、南、北
西方。 在每一步中,用户输入一个
运动方向。 如果有一扇门
朝那个方向,用户去
对应的房间; 否则,
程序打印一条警告。 目标是
从最初的房间到最后的房间
房间。

这个游戏是一个典型的状态机,
当前房间是状态。
我们可以用一个来实现这样的迷宫
每个房间的功能。 我们用尾巴
要求从一个房间搬到另一个房间
其他。 有四个房间的小迷宫
可能看起来像这样:

功能室1 () 
    本地移动 = io.read() 
    如果 move == "south" 则返回 room3() 
    elseif move == "east" 然后返回 room2() 
    else print("无效移动") 
         return room1() -- 留在同一个房间 
    结尾 
  结尾 

  多功能厅2 () 
    本地移动 = io.read() 
    如果 move == "south" 则返回 room4() 
    elseif move == "west" 然后返回 room1() 
    else print("无效移动") 
         返回房间2() 
    结尾 
  结尾 

  多功能厅3 () 
    本地移动 = io.read() 
    如果 move == "north" 则返回 room1() 
    elseif move == "east" 然后返回 room4() 
    else print("无效移动") 
         返回房间3() 
    结尾 
  结尾 

  多功能厅4 () 
    打印(“恭喜!”) 
  结尾 
  

所以你看,当你进行如下递归调用时:

function x(n)
  if n==0 then return 0
  n= n-2
  return x(n) + 1
end

这不是尾递归,因为在进行递归调用后,你在该函数中仍然有事情要做(加 1)。 如果你输入一个非常大的数字,可能会导致堆栈溢出。

This excerpt from the book Programming in Lua shows how to make a proper tail recursion (in Lua, but should apply to Lisp too) and why it's better.

A tail call [tail recursion] is a kind of goto dressed
as a call. A tail call happens when a
function calls another as its last
action, so it has nothing else to do.
For instance, in the following code,
the call to g is a tail call:

function f (x)
  return g(x)
end

After f calls g, it has nothing else
to do. In such situations, the program
does not need to return to the calling
function when the called function
ends. Therefore, after the tail call,
the program does not need to keep any
information about the calling function
in the stack. ...

Because a proper tail call uses no
stack space, there is no limit on the
number of "nested" tail calls that a
program can make. For instance, we can
call the following function with any
number as argument; it will never
overflow the stack:

function foo (n)
  if n > 0 then return foo(n - 1) end
end

... As I said earlier, a tail call is a
kind of goto. As such, a quite useful
application of proper tail calls in
Lua is for programming state machines.
Such applications can represent each
state by a function; to change state
is to go to (or to call) a specific
function. As an example, let us
consider a simple maze game. The maze
has several rooms, each with up to
four doors: north, south, east, and
west. At each step, the user enters a
movement direction. If there is a door
in that direction, the user goes to
the corresponding room; otherwise, the
program prints a warning. The goal is
to go from an initial room to a final
room.

This game is a typical state machine,
where the current room is the state.
We can implement such maze with one
function for each room. We use tail
calls to move from one room to
another. A small maze with four rooms
could look like this:

function room1 ()
  local move = io.read()
  if move == "south" then return room3()
  elseif move == "east" then return room2()
  else print("invalid move")
       return room1()   -- stay in the same room
  end
end

function room2 ()
  local move = io.read()
  if move == "south" then return room4()
  elseif move == "west" then return room1()
  else print("invalid move")
       return room2()
  end
end

function room3 ()
  local move = io.read()
  if move == "north" then return room1()
  elseif move == "east" then return room4()
  else print("invalid move")
       return room3()
  end
end

function room4 ()
  print("congratulations!")
end

So you see, when you make a recursive call like:

function x(n)
  if n==0 then return 0
  n= n-2
  return x(n) + 1
end

This is not tail recursive because you still have things to do (add 1) in that function after the recursive call is made. If you input a very high number it will probably cause a stack overflow.

九命猫 2024-07-12 04:52:08

使用常规递归,每个递归调用都会将另一个条目推送到调用堆栈上。 递归完成后,应用程序必须将每个条目一直弹出回来。

使用尾递归,根据语言的不同,编译器可能能够将堆栈折叠为一个条目,因此可以节省堆栈空间......大型递归查询实际上可能会导致堆栈溢出。

基本上尾递归可以优化为迭代。

Using regular recursion, each recursive call pushes another entry onto the call stack. When the recursion is completed, the app then has to pop each entry off all the way back down.

With tail recursion, depending on language the compiler may be able to collapse the stack down to one entry, so you save stack space...A large recursive query can actually cause a stack overflow.

Basically Tail recursions are able to be optimized into iteration.

不可一世的女人 2024-07-12 04:52:08

行话文件对尾递归的定义有这样的说法:

尾递归 /n./

如果您还没有厌倦它,请参阅尾递归。

The jargon file has this to say about the definition of tail recursion:

tail recursion /n./

If you aren't sick of it already, see tail recursion.

天煞孤星 2024-07-12 04:52:08

这里不再用文字来解释,而是举个例子。 这是阶乘函数的方案版本:

(define (factorial x)
  (if (= x 0) 1
      (* x (factorial (- x 1)))))

这是尾递归的阶乘版本:

(define factorial
  (letrec ((fact (lambda (x accum)
                   (if (= x 0) accum
                       (fact (- x 1) (* accum x))))))
    (lambda (x)
      (fact x 1))))

您会注意到在第一个版本中,对事实的递归调用被馈送到乘法表达式中,因此在进行递归调用时必须将状态保存在堆栈上。 在尾递归版本中,没有其他 S 表达式等待递归调用的值,并且由于没有进一步的工作要做,因此状态不必保存在堆栈上。 通常,Scheme 尾递归函数使用常量堆栈空间。

Instead of explaining it with words, here's an example. This is a Scheme version of the factorial function:

(define (factorial x)
  (if (= x 0) 1
      (* x (factorial (- x 1)))))

Here is a version of factorial that is tail-recursive:

(define factorial
  (letrec ((fact (lambda (x accum)
                   (if (= x 0) accum
                       (fact (- x 1) (* accum x))))))
    (lambda (x)
      (fact x 1))))

You will notice in the first version that the recursive call to fact is fed into the multiplication expression, and therefore the state has to be saved on the stack when making the recursive call. In the tail-recursive version there is no other S-expression waiting for the value of the recursive call, and since there is no further work to do, the state doesn't have to be saved on the stack. As a rule, Scheme tail-recursive functions use constant stack space.

护你周全 2024-07-12 04:52:08

传统递归中,典型的模型是先执行递归调用,然后获取递归调用的返回值并计算结果。 通过这种方式,直到每次递归调用返回后,您才能获得计算结果。

尾递归中,您首先执行计算,然后执行递归调用,将当前步骤的结果传递到下一个递归步骤。 这导致最后一条语句采用 (return (recursive-function params)) 的形式。 基本上,任何给定递归步骤的返回值与下一个递归调用的返回值相同

这样做的结果是,一旦您准备好执行下一个递归步骤,您就不再需要当前的堆栈帧。 这允许进行一些优化。 事实上,使用适当编写的编译器,您永远不应该在尾递归调用中出现堆栈溢出偷笑。 只需在下一个递归步骤中重用当前堆栈帧即可。 我很确定 Lisp 会这么做。

In traditional recursion, the typical model is that you perform your recursive calls first, and then you take the return value of the recursive call and calculate the result. In this manner, you don't get the result of your calculation until you have returned from every recursive call.

In tail recursion, you perform your calculations first, and then you execute the recursive call, passing the results of your current step to the next recursive step. This results in the last statement being in the form of (return (recursive-function params)). Basically, the return value of any given recursive step is the same as the return value of the next recursive call.

The consequence of this is that once you are ready to perform your next recursive step, you don't need the current stack frame any more. This allows for some optimization. In fact, with an appropriately written compiler, you should never have a stack overflow snicker with a tail recursive call. Simply reuse the current stack frame for the next recursive step. I'm pretty sure Lisp does this.

像极了他 2024-07-12 04:52:08

考虑一个将前 N 个自然数相加的简单函数。 (例如 sum(5) = 0 + 1 + 2 + 3 + 4 + 5 = 15)。

下面是一个使用递归的简单 JavaScript 实现:

function recsum(x) {
    if (x === 0) {
        return 0;
    } else {
        return x + recsum(x - 1);
    }
}

如果您调用了 recsum(5),这就是 JavaScript 解释器将计算的结果:

recsum(5)
5 + recsum(4)
5 + (4 + recsum(3))
5 + (4 + (3 + recsum(2)))
5 + (4 + (3 + (2 + recsum(1))))
5 + (4 + (3 + (2 + (1 + recsum(0)))))
5 + (4 + (3 + (2 + (1 + 0))))
5 + (4 + (3 + (2 + 1)))
5 + (4 + (3 + 3))
5 + (4 + 6)
5 + 10
15

请注意每个递归调用必须在 JavaScript 解释器开始实际执行之前完成计算总和的工作。

下面是同一函数的尾递归版本:

function tailrecsum(x, running_total = 0) {
    if (x === 0) {
        return running_total;
    } else {
        return tailrecsum(x - 1, running_total + x);
    }
}

下面是调用 tailrecsum(5) 时会发生的事件序列(实际上是 tailrecsum(5, 0) code>,因为默认的第二个参数)。

tailrecsum(5, 0)
tailrecsum(4, 5)
tailrecsum(3, 9)
tailrecsum(2, 12)
tailrecsum(1, 14)
tailrecsum(0, 15)
15

在尾递归情况下,每次评估递归调用时,running_total 都会更新。

注意:原始答案使用了 Python 中的示例。 这些已更改为 JavaScript,因为 Python 解释器不支持尾部调用优化。 然而,虽然尾部调用优化是 ECMAScript 2015 的一部分规范,大多数 JavaScript 解释器不支持)。

Consider a simple function that adds the first N natural numbers. (e.g. sum(5) = 0 + 1 + 2 + 3 + 4 + 5 = 15).

Here is a simple JavaScript implementation that uses recursion:

function recsum(x) {
    if (x === 0) {
        return 0;
    } else {
        return x + recsum(x - 1);
    }
}

If you called recsum(5), this is what the JavaScript interpreter would evaluate:

recsum(5)
5 + recsum(4)
5 + (4 + recsum(3))
5 + (4 + (3 + recsum(2)))
5 + (4 + (3 + (2 + recsum(1))))
5 + (4 + (3 + (2 + (1 + recsum(0)))))
5 + (4 + (3 + (2 + (1 + 0))))
5 + (4 + (3 + (2 + 1)))
5 + (4 + (3 + 3))
5 + (4 + 6)
5 + 10
15

Note how every recursive call has to complete before the JavaScript interpreter begins to actually do the work of calculating the sum.

Here's a tail-recursive version of the same function:

function tailrecsum(x, running_total = 0) {
    if (x === 0) {
        return running_total;
    } else {
        return tailrecsum(x - 1, running_total + x);
    }
}

Here's the sequence of events that would occur if you called tailrecsum(5), (which would effectively be tailrecsum(5, 0), because of the default second argument).

tailrecsum(5, 0)
tailrecsum(4, 5)
tailrecsum(3, 9)
tailrecsum(2, 12)
tailrecsum(1, 14)
tailrecsum(0, 15)
15

In the tail-recursive case, with each evaluation of the recursive call, the running_total is updated.

Note: The original answer used examples from Python. These have been changed to JavaScript, since Python interpreters don't support tail call optimization. However, while tail call optimization is part of the ECMAScript 2015 spec, most JavaScript interpreters don't support it).

好久不见√ 2024-07-12 04:52:08

这里很多人已经解释了递归。 我想引用 Riccardo Terrell 所著的《.NET 中的并发,并发和并行编程的现代模式》一书中关于递归带来的一些优点的一些想法:

“函数递归是 FP 中迭代的自然方式,因为它
避免状态突变。 在每次迭代期间,都会传递一个新值
进入循环构造函数而不是进行更新(变异)。 在
另外,可以编写递归函数,使您的程序
更加模块化,并引入了利用的机会
并行化。”

以下还有同一本书中关于尾递归的一些有趣的注释:

尾调用递归是一种将常规递归转换为尾调用递归的技术
函数变成可以处理大输入的优化版本
无任何风险和副作用。

注意尾部调用作为优化的主要原因是
提高数据局部性、内存使用率和缓存使用率。 通过做尾巴
调用时,被调用者使用与调用者相同的堆栈空间。 这减少了
内存压力。 它稍微改善了缓存,因为相同
内存被后续调用者重用,并且可以保留在缓存中,
而不是逐出旧的缓存行来为新的缓存腾出空间
线。

Many people have already explained recursion here. I would like to cite a couple of thoughts about some advantages that recursion gives from the book “Concurrency in .NET, Modern patterns of concurrent and parallel programming” by Riccardo Terrell:

“Functional recursion is the natural way to iterate in FP because it
avoids mutation of state. During each iteration, a new value is passed
into the loop constructor instead to be updated (mutated). In
addition, a recursive function can be composed, making your program
more modular, as well as introducing opportunities to exploit
parallelization."

Here also are some interesting notes from the same book about tail recursion:

Tail-call recursion is a technique that converts a regular recursive
function into an optimized version that can handle large inputs
without any risks and side effects.

NOTE The primary reason for a tail call as an optimization is to
improve data locality, memory usage, and cache usage. By doing a tail
call, the callee uses the same stack space as the caller. This reduces
memory pressure. It marginally improves the cache because the same
memory is reused for subsequent callers and can stay in the cache,
rather than evicting an older cache line to make room for a new cache
line.

我的鱼塘能养鲲 2024-07-12 04:52:08

已经发布了许多好的答案,但让我添加一个非常简短的答案。

递归调用尾递归函数时,不会增加堆栈。

这是上述所有解释的本质结果。 程序员使用的技术因问题而异,但通常归结为将待处理的结果作为参数传递给函数,而不是将部分结果留在堆栈上。

请参阅 Yilmaz 的上述答案,他提供了一个阶乘示例。

There are numerous good answers already posted, but let me add a very brief answer.

A tail recursive function when called recursively does not grow the stack.

This is the essential result of all the above explanations. The technique a programmer uses will vary from one problem to the next, but it often comes down to passing the pending result as an argument to the function rather than leaving a partial result on the stack.

See the above answer by Yilmaz who provides a factorial example.

仅冇旳回忆 2024-07-12 04:52:08
  • Tail Recursive Function 是一种递归函数,其中递归调用是函数中最后执行的事情。

常规递归函数,我们有堆栈,每次我们在该递归函数中调用递归函数时,都会在调用堆栈中添加另一层。 在正常的递归中
空间:O(n)尾递归使空间复杂度来自

O(N)=>O(1)
  • 尾调用优化意味着可以从另一个函数调用一个函数而不增加调用堆栈。

  • 我们应该在递归解决方案中编写尾递归。 但某些语言实际上并不支持其引擎中向下编译语言的尾递归。 从 ecma6 开始,规范中就出现了尾递归。 但是编译js的引擎都没有实现尾递归。 在js中你不会实现O(1),因为编译器本身不知道如何实现这个尾递归。 截至 2020 年 1 月 1 日,Safari 是唯一支持尾部调用优化的浏览器。

  • Haskell 和 Java 都有尾递归优化

常规递归阶乘

function Factorial(x) {
  //Base case x<=1
  if (x <= 1) {
    return 1;
  } else {
    // x is waiting for the return value of Factorial(x-1)
    // the last thing we do is NOT applying the recursive call
    // after recursive call we still have to multiply.
    return x * Factorial(x - 1);
  }
}

我们的调用堆栈中有 4 个调用。

Factorial(4); // waiting in the memory for Factorial(3)
4 * Factorial(3); //  waiting in the memory for Factorial(2)
4 * (3 * Factorial(2)); //  waiting in the memory for Factorial(1)
4 * (3 * (2 * Factorial(1)));
4 * (3 * (2 * 1));
  • 我们正在进行 4 个 Factorial() 调用,空间复杂度为 O(n)
  • 这可能会导致 Stackoverflow

尾部递归阶乘

function tailFactorial(x, totalSoFar = 1) {
  //Base Case: x===0. In recursion there must be base case. Otherwise they will never stop
  if (x === 0) {
    return totalSoFar;
  } else {
    // there is nothing waiting for tailFactorial to complete. we are returning another instance of tailFactorial()
    // we are not doing any additional computaion with what we get back from this recursive call
    return tailFactorial(x - 1, totalSoFar * x);
  }
}
  • 我们在进行递归调用后不需要记住任何东西
  • Tail Recursive Function is a recursive function in which recursive call is the last executed thing in the function.

Regular recursive function, we have stack and everytime we invoke a recursive function within that recursive function, adds another layer to our call stack. In normal recursion
space: O(n) tail recursion makes space complexity from

O(N)=>O(1)
  • Tail call optimization means that it is possible to call a function from another function without growing the call stack.

  • We should write tail recursion in recursive solutions. but certain languages do not actually support the tail recursion in their engine that compiles the language down. since ecma6, there has been tail recursion that was in the specification. BUt none of the engines that compile js have implemented tail recursion into it. you wont achieve O(1) in js, because the compiler itself does not know how to implement this tail recursion. As of January 1, 2020 Safari is the only browser that supports tail call optimization.

  • Haskell and Java have tail recursion optimization

Regular Recursive Factorial

function Factorial(x) {
  //Base case x<=1
  if (x <= 1) {
    return 1;
  } else {
    // x is waiting for the return value of Factorial(x-1)
    // the last thing we do is NOT applying the recursive call
    // after recursive call we still have to multiply.
    return x * Factorial(x - 1);
  }
}

we have 4 calls in our call stack.

Factorial(4); // waiting in the memory for Factorial(3)
4 * Factorial(3); //  waiting in the memory for Factorial(2)
4 * (3 * Factorial(2)); //  waiting in the memory for Factorial(1)
4 * (3 * (2 * Factorial(1)));
4 * (3 * (2 * 1));
  • We are making 4 Factorial() calls, space is O(n)
  • this mmight cause Stackoverflow

Tail Recursive Factorial

function tailFactorial(x, totalSoFar = 1) {
  //Base Case: x===0. In recursion there must be base case. Otherwise they will never stop
  if (x === 0) {
    return totalSoFar;
  } else {
    // there is nothing waiting for tailFactorial to complete. we are returning another instance of tailFactorial()
    // we are not doing any additional computaion with what we get back from this recursive call
    return tailFactorial(x - 1, totalSoFar * x);
  }
}
  • We dont need to remember anything after we make our recursive call
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文