即使存在多个不同的递归调用,函数是否可以针对尾递归进行优化?

时间:2013-02-22 19:03:23

标签: f# tail-recursion

正如我在recent SO question中提到的那样,我通过解决Project Euler问题来学习F#。

我现在对Problem 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条执行路径可能导致对findLargestPrimeFactor的不同调用,我不确定它是否可以针对尾递归进行优化。所以我想出了这个:

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. 如果两个版本都可以针对尾递归进行优化,那么还有一个更好(更“功能”,更快等)吗?

2 个答案:

答案 0 :(得分:8)

你的第一个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子句中的第一个分支(即if n % p = 0L)从IL_0013开始并一直持续到IL_0024,在那里它无条件地分支回到函数的入口点。

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

答案 1 :(得分:6)

  

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

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

  

如果两个版本都可以针对尾递归进行优化,那么是否有一个更好(更“功能”,更快等)?

两个版本之间略有不同。第二个版本创建了一个额外的元组,但它不会减慢计算量。我认为第一个函数更具可读性和直接性。

要进行挑剔,使用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

由于基础算法是相同的,它也不会更快。