当前位置: 首页 > 知识库问答 >
问题:

即使有多个不同的递归调用,函数也可以针对尾递归进行优化吗?

荣俊
2023-03-14

正如我在最近的一个SO问题中提到的,我正在通过学习Euler问题来学习F。

现在,我对问题3有了一个有效的答案,如下所示:

let rec findLargestPrimeFactor p n = 
    if n = 1L then p
    else
        if n % p = 0L then findLargestPrimeFactor p (n/p)
        else findLargestPrimeFactor (p + 2L) n

let result = findLargestPrimeFactor 3L 600851475143L

但是,由于有2个执行路径可能导致对findLarggPrimeFactor的不同调用,我不确定它是否可以针对尾递归进行优化。所以我想出了这个:

let rec findLargestPrimeFactor p n = 
    if n = 1L then p
    else
        let (p', n') = if n % p = 0L then (p, (n/p)) else (p + 2L, n)
        findLargestPrimeFactor p' n'

let result = findLargestPrimeFactor 3L 600851475143L

由于只有一条路径会导致对findLargestPrimeFactor的尾部调用,我认为它确实会针对尾部递归进行优化。

所以我的问题是:

  1. 即使有两个不同的递归调用,第一个实现是否可以针对尾部递归进行优化

共有2个答案

锺星洲
2023-03-14

即使有两个不同的递归调用,第一个实现是否可以针对尾部递归进行优化?

递归分支的数量与尾部递归正交。第一个函数是尾部递归的,因为findLargestPrimeFactor是两个分支上的最后一个操作。如果有疑问,您可以尝试在释放模式下运行该函数(默认情况下,尾部调用优化选项处于打开状态),并观察结果。

如果两个版本都可以针对尾部递归进行优化,是否有一个版本比另一个更好(更“实用”、更快等)?

两个版本之间只有一点不同。第二个版本创建了一个额外的元组,但它不会降低那么多计算速度。我认为第一个函数更具可读性,更直截了当。

为了挑剔,第一个变体使用elif关键字更短:

let rec findLargestPrimeFactor p n = 
    if n = 1L then p
    elif n % p = 0L then findLargestPrimeFactor p (n/p)
    else findLargestPrimeFactor (p + 2L) n

另一个版本是使用模式匹配:

let rec findLargestPrimeFactor p = function
    | 1L -> p
    | n when n % p = 0L -> findLargestPrimeFactor p (n/p)
    | n -> findLargestPrimeFactor (p + 2L) n

既然底层算法一样,也不会更快。

艾子石
2023-03-14

第一个findLargestPrimeFactor函数是尾部递归的-如果所有递归调用都发生在尾部位置,即使有多个递归调用,也可以使函数成为尾部递归的。

这是编译函数的IL:

.method public static int64  findLargestPrimeFactor(int64 p,
                                                    int64 n) cil managed
{
  .custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationArgumentCountsAttribute::.ctor(int32[]) = ( 01 00 02 00 00 00 01 00 00 00 01 00 00 00 00 00 ) 
  // Code size       56 (0x38)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldarg.1
  IL_0002:  ldc.i8     0x1
  IL_000b:  bne.un.s   IL_000f
  IL_000d:  br.s       IL_0011
  IL_000f:  br.s       IL_0013
  IL_0011:  ldarg.0
  IL_0012:  ret
  IL_0013:  ldarg.1
  IL_0014:  ldarg.0
  IL_0015:  rem
  IL_0016:  brtrue.s   IL_001a
  IL_0018:  br.s       IL_001c
  IL_001a:  br.s       IL_0026
  IL_001c:  ldarg.0
  IL_001d:  ldarg.1
  IL_001e:  ldarg.0
  IL_001f:  div
  IL_0020:  starg.s    n
  IL_0022:  starg.s    p
  IL_0024:  br.s       IL_0000
  IL_0026:  ldarg.0
  IL_0027:  ldc.i8     0x2
  IL_0030:  add
  IL_0031:  ldarg.1
  IL_0032:  starg.s    n
  IL_0034:  starg.s    p
  IL_0036:  br.s       IL_0000
} // end of method LinkedList::findLargestPrimeFactor

else子句中的第一个分支(即,如果p=0L,则从IL_0013开始,一直持续到IL_0024,在此无条件分支回函数的入口点。

else子句中的第二个分支从IL_0026开始,一直持续到函数结束,在那里它再次无条件地分支回到函数的开始。对于包含递归调用的else子句的两种情况,F#编译器已将递归函数转换为循环。

 类似资料:
  • 我试图了解如何将各种递归函数转换为尾递归。我已经查看了许多将斐波那契和阶乘转换为尾递归的示例,并理解了这些示例,但很难跳到具有某种不同结构的问题。一个例子是: 如何将其转换为尾部递归实现? 我已经看过类似的问题,例如:将正常递归转换为尾部递归,但这些似乎并没有转化为这个问题。

  • 我只是想知道这样的函数是否可以递归地完成。我觉得很难,因为它自己叫了两次。 这是我在javascript中的非尾部递归实现。(是的,我知道大多数javascript引擎不支持TCO,但这只是理论上的。)目标是找到给定数组(arr)中具有特定长度(大小)的所有子列表。例如:getSublistsWithFixedSize([1,2,3],2)返回[[1,2]、[1,3]、[2,3]]

  • 我有一个家庭作业问题,它给出了一个递归函数,我必须使用尾部递归来实现它。函数为f(0)=1 f(n)=1 2*f(n-1) 我并不擅长尾部递归,我试着查找示例,但我发现的都是没有斐波那契序列的示例,这没有多大帮助。 我真正拥有的是 我知道尾递归基本上每次调用都计算函数,我只是不知道如何实现它。 编辑:我打了一个错字f(n)应该是1 2*f(n-1)

  • 假设我编写这样的代码: 我如何让Kotlin优化这些相互递归的函数,以便在不抛出StackOverflower错误的情况下运行main?tailrec关键字适用于单函数递归,但没有更复杂的功能。我还看到一个警告,在使用关键字tailrec的地方没有找到尾部调用。也许这对编译器来说太难了?

  • 我对函数式编程很陌生,尤其是下面使用的Scheme。我正在尝试使以下函数是递归的,尾递归的。基本上,该函数的作用是对两个字符串的对齐方式进行评分。当给定两个字符串作为输入时,它会比较每个“列”字符,并根据在称为 scorer 的函数中实现的评分方案(由下面的代码中的函数调用)来累积该对齐的分数。 我有一个想法,用一个帮助函数来累积分数,但我不太确定如何去做,因此我该如何让下面的函数尾递归呢?

  • 本文向大家介绍C#函数式编程中的递归调用之尾递归详解,包括了C#函数式编程中的递归调用之尾递归详解的使用技巧和注意事项,需要的朋友参考一下 关于递归相信大家已经熟悉的不能再熟悉了,所以笔者在这里就不多费口舌,不懂的读者们可以在博客园中找到很多与之相关的博客。下面我们直接切入正题,开始介绍尾递归。 尾递归 普通递归和尾递归如果仅仅只是从代码的角度出发来看,我们可能发现不了他的特点,所以笔者利用两张堆