在创建中间值时,我应该存储它吗?

时间:2010-01-10 04:26:07

标签: f# functional-programming primes factorization

我正在尝试学习F#,因此我访问了Project Euler,目前正在处理Problem 3

  

13195的主要因素是5,7,   13和29。

     

什么是最大的素数   数量系数600851475143?

需要考虑的一些事项:

  1. 我的首要任务是学习良好的功能习惯。
  2. 我的第二要务是我希望它快速有效。
  3. 在以下代码中,我已标记了此问题所涉及的部分。

    let isPrime(n:int64) = 
        let rec check(i:int64) = 
            i > n / 2L or (n % i <> 0L && check(i + 1L))
        check(2L) 
    
    let greatestPrimeFactor(n:int64) =
        let nextPrime(prime:int64):int64 = 
            seq { for i = prime + 1L to System.Int64.MaxValue do if isPrime(i) then yield i }  
            |> Seq.skipWhile(fun v -> n % v <> 0L) 
            |> Seq.hd
        let rec findNextPrimeFactor(number:int64, prime:int64):int64 =
            if number = 1L then prime else 
                //************* No variable
                (fun p -> findNextPrimeFactor(number / p, p))(nextPrime(prime))    
                //*************               
                //************* Variable
                let p = nextPrime(prime)
                findNextPrimeFactor(number / p, p) 
                //*************
        findNextPrimeFactor(n, 2L) 
    

    更新

    基于一些反馈,我已经重构了代码,速度提高了10倍。

    module Problem3
    
    module private Internal =
        let execute(number:int64):int64 = 
            let rec isPrime(value:int64, current:int64) = 
                current > value / 2L or (value % current <> 0L && isPrime(value, current + 1L))   
            let rec nextPrime(prime:int64):int64 = 
                if number % prime = 0L && isPrime(prime, 2L) then prime else nextPrime(prime + 1L)       
            let rec greatestPrimeFactor(current:int64, prime:int64):int64 =
                if current = 1L then prime else nextPrime(prime + 1L) |> fun p -> greatestPrimeFactor(current / p, p)
            greatestPrimeFactor(number, 2L)
    
    
    let execute() = Internal.execute(600851475143L)
    

    更新

    我要感谢大家的建议。这个最新版本汇集了我收到的所有建议。

    module Problem3
    
    module private Internal =
        let largestPrimeFactor number = 
            let rec isPrime value current = 
                current > value / 2L || (value % current <> 0L && isPrime value (current + 1L))   
            let rec nextPrime value =
                if number % value = 0L && isPrime value 2L then value else nextPrime (value + 1L) 
            let rec find current prime =
                match current / prime with
                | 1L -> prime
                | current -> nextPrime (prime + 1L) |> find current
            find number (nextPrime 2L)            
    
    let execute() = Internal.largestPrimeFactor 600851475143L
    

4 个答案:

答案 0 :(得分:7)

通过练习,功能性编程变得更容易,更自动化,所以如果你在第一次尝试时没有完全正确的话,不要出汗。

考虑到这一点,我们来看看你的示例代码:

let rec findNextPrimeFactor(number:int64, prime:int64):int64 =
        if number = 1L then prime else 
            //************* No variable
            (fun p -> findNextPrimeFactor(number / p, p))(nextPrime(prime))    
            //*************               
            //************* Variable
            let p = nextPrime(prime)
            findNextPrimeFactor(number / p, p) 
            //*************

您的no variable版本很奇怪,请勿使用它。我喜欢你的版本,带有明确的let绑定。

另一种写作方式是:

nextPrime(prime) |> fun p -> findNextPrimeFactor(number / p, p)

它的 ok 偶尔会像这样编写它,但仍然有点奇怪。大多数情况下,我们使用|>来衡量值,而需要命名我们的变量(以“pointfree”样式)。尝试预测如何使用您的函数,如果可能,重新编写它,以便您可以在没有显式声明变量的情况下将它与管道运算符一起使用。例如:

let rec findNextPrimeFactor number prime =
    match number / prime with
    | 1L -> prime
    | number' -> nextPrime(prime) |> findNextPrimeFactor number'

不再命名为args:)


好的,现在我们已经开始了,让我们来看看你的isPrime功能:

let isPrime(n:int64) = 
    let rec check(i:int64) = 
        i > n / 2L or (n % i <> 0L && check(i + 1L))
    check(2L) 

你可能听说过使用递归而不是循环,这是正确的。但是,只要有可能,您应该使用折叠,贴图或更高阶函数来抽象递归。有两个原因:

  1. 更具可读性,

  2. 不正确的写入递归会导致堆栈溢出。例如,你的函数不是尾递归的,所以它会夸大n的大值。

  3. 我会像这样重写isPrime

    let isPrime n = seq { 2L .. n / 2L } |> Seq.exists (fun i -> n % i = 0L) |> not
    

    大多数情况下,如果您可以抽象出显式循环,那么您只需将转换应用于输入序列,直到获得结果:

    let maxFactor n =
        seq { 2L .. n - 1L }                        // test inputs
        |> Seq.filter isPrime                       // primes
        |> Seq.filter (fun x -> n % x = 0L)         // factors
        |> Seq.max                                  // result
    

    我们在这个版本中甚至没有中间变量。凉意!


      

    我的第二个优先事项是我希望它   要快速有效。

    大多数时候,F#在速度方面与C#相当,或者说“足够快”。如果您发现代码需要很长时间才能执行,则可能意味着您使用的是错误的数据结构或错误的算法。有关具体示例,请阅读评论on this question

    所以,我写的代码是“优雅的”,因为它简洁,给出了正确的结果,并且不依赖于任何技巧。不幸的是,它不是很快。首先:

    • 当Eratosthenes筛子快得多时,它会使用试验分区创建一系列素数。 [编辑:我写了一个有点天真的筛子版本,对于大于Int32.MaxValue的数字不起作用,所以我删除了代码。]

    • 阅读维基百科关于prime counting function的文章,它将为您提供有关计算第一个n素数以及估计nth素数的上限和下限的指示

    [编辑:我包含了一些代码,其中有一些有点幼稚的eratosthenes筛子。它仅适用于小于int32.MaxValue的输入,因此它可能不适合项目euler。]

答案 1 :(得分:5)

关于“良好的功能习惯”或相当好的做法,我看到三件小事。使用序列中的yield比使用过滤器更难阅读。类型推断语言中不必要的类型注释会导致难以重构并使代码更难阅读。如果发现困难,请不要过分尝试删除所有类型的注释。最后制作一个只使用一个值作为临时变量的lambda函数会降低可读性。

就个人风格而言,我更喜欢更多空格,只有当数据合理分组时才使用tupled参数。

我会写这样的原始代码。

let isPrime n = 
    let rec check i = 
        i > n / 2L || (n % i <> 0L && check (i + 1L))
    check 2L

let greatestPrimeFactor n =
    let nextPrime prime = 
        seq {prime + 1L .. System.Int64.MaxValue}
        |> Seq.filter isPrime
        |> Seq.skipWhile (fun v -> n % v <> 0L) 
        |> Seq.head

    let rec findNextPrimeFactor number prime =
        if number = 1L then 
            prime 
        else 
            let p = nextPrime(prime)
            findNextPrimeFactor (number / p) p

    findNextPrimeFactor n 2L

您的更新代码最适合您的方法。你必须使用不同的算法,如尹朱的答案才能更快。我写了一个测试来检查F#是否使“检查”函数尾递归并且确实如此。

答案 2 :(得分:3)

变量 p实际上是名称绑定,而不是变量。使用名称绑定并不是一种糟糕的风格。它更具可读性。 nextPrime的懒惰风格很好,实际上它在整个程序中仅对每个数字进行一次测试。

我的解决方案

let problem3 = 
    let num = 600851475143L
    let rec findMax (n:int64) (i:int64) =
        if n=i || n<i then
            n
        elif n%i=0L then
            findMax (n/i) i
        else
            findMax n (i+1L)
    findMax num 2L

我基本上将数字从2,3,4除以...并且不考虑任何素数。因为如果我们将所有2除以num,那么我们将无法将它除以4,8等等。

关于这个数字,我的解决方案更快:

> greatestPrimeFactor 600851475143L;;
Real: 00:00:01.110, CPU: 00:00:00.702, GC gen0: 1, gen1: 1, gen2: 0
val it : int64 = 6857L
> 
Real: 00:00:00.001, CPU: 00:00:00.000, GC gen0: 0, gen1: 0, gen2: 0

val problem3 : int64 = 6857L

答案 3 :(得分:1)

我认为带有临时绑定的代码更容易阅读。创建匿名函数然后立即将其应用于其他情况下的值是非常不寻常的。如果你真的想避免使用临时值,我认为在F#中最常用的方法是使用(|>)运算符将值传递给匿名函数,但我仍然认为这不是非常可读。