【问题标题】:When creating an intermediary value should I store it?创建中间值时,我应该存储它吗?
【发布时间】:2011-01-03 09:53:00
【问题描述】:

我正在努力学习 F#,所以我访问了 Project Euler,目前正在研究 Problem 3

13195 的质因数是 5, 7, 13 和 29。

什么是最大的素数 数字600851475143的因数?

需要考虑的一些事项:

  1. 我的首要任务是学习良好的功能习惯。
  2. 我的第二个优先事项是我希望它快速高效。

在下面的代码中,我标记了这个问题所涉及的部分。

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

【问题讨论】:

    标签: f# functional-programming primes factorization


    【解决方案1】:

    通过练习,函数式编程变得更容易、更自动化,所以如果您在第一次尝试时没有完全正确,请不要担心。

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

    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,偶尔这样写也很有用,但还是有点奇怪。大多数时候,我们使用|&gt; 来curry 值而不 需要命名我们的变量(在“pointfree”风格)。试着预测你的函数将如何被使用,如果可能的话,重写它,这样你就可以在不显式声明变量的情况下将它与管道运算符一起使用。例如:

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

    没有更多的命名参数 :)


    好的,现在我们已经解决了这个问题,让我们看看你的 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 的大值上爆炸。

    我会像这样重写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# 相当,或者说“足够快”。如果您发现您的代码需要很长时间才能执行,这可能意味着您使用了错误的数据结构或错误的算法。具体例子请阅读 cmets on this question

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

    • 它使用试除法来创建一个素数序列,而 Eratosthenes 的筛子会快得多。 [编辑:我写了这个筛子的一个有点幼稚的版本,它不适用于大于 Int32.MaxValue 的数字,所以我删除了代码。]

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

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

    【讨论】:

    • 真的很酷!让 isPrime n = seq { 2L .. n / 2L } |> Seq.exists (fun i -> n % i = 0L) |> 不是。这是否会生成提前停止的代码,就像 for 循环中的 break 语句一样。谢谢。
    • 是的,Seq.exists 一找到真正的元素就会中断。
    • 不管怎样,它看起来像 removeMultiples 函数可以并行化以获得更快的速度。至少在我自己的试运行中,我发现创建一堆短期线程的开销导致性能非常糟糕。
    • 请注意,您的最后一个代码中有一个错误。试试 maxFactor (2L*17L)。我们需要将 n 除以所有小于 sqrt(n) 的因子,看看是否还有一个大素数。
    • 印珠:哎呀!此外,我的素数筛使用了一个位数组,它只能表示从 2 到 int32.MaxValue 的素数,这使得它不适合项目 euler(再次糟糕!)。
    【解决方案2】:

    关于“良好的功能习惯”或者说是好的实践,我看到了三件小事。在您的序列中使用产量比过滤器更难阅读。类型推断语言中不必要的类型注释会导致难以重构并使代码更难阅读。不要过分尝试删除每个类型注释,但如果您发现它很困难。最后,制作一个仅将一个值用作临时变量的 lambda 函数会降低可读性。

    就个人风格而言,我更喜欢更多的空间,并且仅在将数据组合在一起有意义时才使用元组参数。

    我会这样写你的原始代码。

    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# 是否使“检查”函数尾递归并且确实如此。

    【讨论】:

    • 删除类型注释使代码更加友好。谢谢。
    【解决方案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
    

    我基本上将 num 从 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
    

    【讨论】:

    • 您的解决方案很好,但我认为问题更多是关于提问者解决方案的风格的反馈。
    • 令人印象深刻但不是很有帮助。
    • 看看你的算法,我想知道它是否适用于任何n
    • 如果n 是一个大素数,则该算法不起作用。在这种情况下,我们需要生成sqrt(n)下的所有素数。
    • 我认为您的意思是“绑定”而不是“致盲”。
    【解决方案4】:

    我认为带有临时绑定的代码更容易阅读。创建一个匿名函数然后立即将它应用到一个值是非常不寻常的,就像你在其他情况下所做的那样。如果您真的想避免使用临时值,我认为在 F# 中最惯用的方法是使用 (|&gt;) 运算符将值传递到匿名函数中,但我仍然认为这不是可读性很好。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-01-05
      • 1970-01-01
      • 2011-03-03
      • 2018-07-05
      • 2017-07-15
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多