将此F#代码重构为尾递归

时间:2010-06-04 10:25:28

标签: f# tail-recursion

我写了一些代码来学习F#。 这是一个例子:

let nextPrime list=
    let rec loop n=
        match n with
        | _ when (list |> List.filter (fun x -> x <= ( n |> double |> sqrt |> int)) |> List.forall (fun x -> n % x <> 0)) -> n
        | _ -> loop (n+1)
    loop (List.max list + 1)

let rec findPrimes num=
    match num with
    | 1 -> [2]
    | n -> 
        let temp = findPrimes <| n-1
        (nextPrime temp ) :: temp

//find 10 primes
findPrimes 10 |> printfn "%A"

我很高兴它能奏效!

我完全是递归

的初学者

递归是一件很棒的事情。

我认为 findPrimes 效率不高。

如果可能,有人帮我将findPrimes重构为尾递归吗?

顺便说一下,有没有更有效的方法来找到第一个n素数

5 个答案:

答案 0 :(得分:4)

关于问题的第一部分,如果要以递归方式编写递归列表构建函数,则应将中间结果列表作为额外参数传递给函数。在你的情况下,这将是

let findPrimesTailRecursive num =
    let rec aux acc num = 
        match num with
        | 1 -> acc
        | n -> aux ((nextPrime acc)::acc) (n-1)
    aux [2] num

递归函数aux将其结果收集在一个额外的参数中,方便地称为acc(如在acc-umulator中)。当你达到结束条件时,只需吐出累积的结果。我已将尾递归辅助函数包装在另一个函数中,因此函数签名保持不变。

正如您所看到的,对aux的调用是唯一的,因此也是最后一次调用n&lt;&gt; 1例。它现在是尾递归的,并将编译成一个while循环。

我把你的版本和我的版本计时,产生2000个素数。我的版本速度提高了16%,但仍然相当慢。为了生成质数,我喜欢使用命令式阵列筛。不是很实用,但非常(非常)快。

答案 1 :(得分:3)

为什么不简单地写:

let isPrime n =
    if n<=1 then false
    else
        let m = int(sqrt (float(n)))
        {2..m} |> Seq.forall (fun i->n%i<>0)

let findPrimes n = 
    {2..n} |> Seq.filter isPrime |> Seq.toList

或筛子(非常快):

let generatePrimes max=
    let p = Array.create (max+1) true
    let rec filter i step = 
        if i <= max then 
            p.[i] <- false
            filter (i+step) step
    {2..int (sqrt (float max))} |> Seq.iter (fun i->filter (i+i) i) 
    {2..max} |> Seq.filter (fun i->p.[i]) |> Seq.toArray

答案 2 :(得分:3)

另一种方法是使用额外的continuation参数来使findPrimes尾递归。这项技术总能奏效。它将避免堆栈溢出,但可能不会使您的代码更快。

另外,我将你的nextPrime函数更接近我使用的样式。

let nextPrime list=
    let rec loop n = if list |> List.filter (fun x -> x*x <= n) 
                             |> List.forall (fun x -> n % x <> 0) 
                     then n
                     else loop (n+1)
    loop (1 + List.head list)

let rec findPrimesC num cont =
        match num with
        | 1 -> cont [2]
        | n -> findPrimesC (n-1) (fun temp -> nextPrime temp :: temp |> cont)

let findPrimes num = findPrimesC num (fun res -> res)        
findPrimes 10

正如其他人所说,有更快的方法来产生素数。

答案 3 :(得分:2)

  顺便说一下,有没有更有效的方法来找到前n个素数?

我在F#here中描述了一个快速任意大小的Eratosthenes筛子,它将结果累积到一个不断增长的ResizeArray中:

> let primes =
    let a = ResizeArray[2]
    let grow() =
      let p0 = a.[a.Count-1]+1
      let b = Array.create p0 true
      for di in a do
        let rec loop i =
          if i<b.Length then
            b.[i] <- false
            loop(i+di)
        let i0 = p0/di*di
        loop(if i0<p0 then i0+di-p0 else i0-p0)
      for i=0 to b.Length-1 do
        if b.[i] then a.Add(p0+i)
    fun n ->
      while n >= a.Count do
        grow()
      a.[n];;
val primes : (int -> int)

答案 4 :(得分:2)

我知道这有点晚了,答案已经被接受了。但是,我认为OP或者其他任何人都可以对一个好的一步一步指导使尾部递归。以下是一些肯定帮助我的技巧。我将使用除了素数之外的一个直接的例子,因为正如其他人所说,有更好的方法来产生素数。

考虑一个count函数的简单实现,它将创建一个从一些n倒数的整数列表。这个版本不是尾递归的,所以对于长列表,你会遇到堆栈溢出异常:

let rec countDown = function
  | 0 -> []
  | n -> n :: countDown (n - 1)
(*         ^
           |... the cons operator is in the tail position
                as such it is evaluated last.  this drags
                stack frames through subsequent recursive
                calls *)

解决此问题的一种方法是使用参数化函数应用延续传递样式:

let countDown' n =
  let rec countDown n k =
    match n with
    | 0 -> k [] (*            v--- this is continuation passing style *)
    | n -> countDown (n - 1) (fun ns -> n :: k ns)
(*          ^
            |... the recursive call is now in tail position *)
  countDown n (fun ns -> ns) 
(*              ^
                |... and we initialize k with the identity function *)

然后,将此参数化函数重构为专用表示。请注意,函数countDown'实际上并没有倒计时。这是在n > 0时构建延续的方式的工件,然后在n = 0时进行评估。如果您有类似第一个示例的内容并且无法弄清楚如何使其尾递归,那么我建议您编写第二个示例,然后尝试对其进行优化以消除函数参数k 。这肯定会提高可读性。这是第二个例子的优化:

let countDown'' n =
  let rec countDown n ns =
    match n with
    | 0 -> List.rev ns  (* reverse so we are actually counting down again *)
    | n -> countDown (n - 1) (n :: ns)
  countDown n []