2

我正在学习 OCaml(请原谅我的风格),并正在尝试编写一个函数来生成一个素数列表,直到某个上限。我已经设法以几种不同的方式做到这一点,所有这些都有效,直到您将它们缩放到相对较高的上限。

如何更改这些(其中任何一个)以使递归不会填满堆栈?我认为我的 while 循环版本会实现这一点,但显然不是!

发电机

let primes max =
  let isPrime p x =
    let hasDivisor a = (x mod a = 0) in
    not (List.exists hasDivisor p) in

  let rec generate p test =
    if test < max then
      let nextTest = test + 2 in
      if isPrime p test then generate (test :: p) nextTest
                        else generate p nextTest
    else p in

  generate [5; 3; 2] 7;;

这是我最成功的解决方案,因为它在运行时不会立即溢出堆栈primes 2000000;;。但是它只是坐在那里消耗CPU;我只能假设它最终会完成!以下替代方案都存在堆栈溢出问题:

埃拉托色尼的递归筛选

let primes max =
  let rec sieve found toTest =
    let h = List.hd toTest
    and t = List.tl toTest in

    let newPrimes = h :: found
    and doesntDivide x = (x mod h <> 0) in

    let nonDivisors = List.filter doesntDivide t in
      if nonDivisors = [] then newPrimes
                          else sieve newPrimes nonDivisors in

  let rec range a b =
    if a > b then []
             else a :: range (a + 1) b in

  let p = range 2 max in

  sieve [] p;;

Eratosthenes v2 的递归筛选

let primes max =
  let rec sieve toTest =
    let h = List.hd toTest
    and t = List.tl toTest in
    let doesntDivide x = (x mod h <> 0) in
    let nonDivisors = List.filter doesntDivide t in
      if nonDivisors = [] then [h]
                          else (h :: sieve nonDivisors) in

  let rec range a b =
    if a > b then []
             else a :: range (a + 1) b in

  let p = range 2 max in

  sieve p;;

埃拉托色尼环筛

let primes max =
  let rec range a b =
    if a > b then []
             else a :: range (a + 1) b in

  let tail = ref (range 2 max)
  and p    = ref [] in

  while !tail <> [] do
    let h = List.hd !tail
    and t = List.tl !tail in
    let doesntDivide x = (x mod h <> 0) in
    let newTail = ref (List.filter doesntDivide t) in

    tail := !newTail;
    p := h :: !p
  done;

  !p;;
4

2 回答 2

9

发生堆栈溢出是因为您的范围函数不是尾递归的。一个有效的方法是,例如

  let rec range store a b =
    if a > b then store
    else range (a :: store) (a + 1) b
  in

  let p = List.rev (range [] 2 max) in

使用该定义和格式行,给出

$ ocamlopt -o primes2 primes2.ml
$ ./primes2
2
3
5
7
11
13
17
19
23
29
31
37
41
43
47
53
59
...

既然你正在学习,我也会给你一些不请自来的风格评论:)

  • 不要使用 hd 和 tl。更喜欢模式匹配。然后编译器可以告诉你你错过的情况。例如

    let rec sieve found toTest = let h = List.hd toTest and t = List.tl toTest in

将会

let rec sieve found = function
  | h :: t -> ...
  | [] -> Error handling...
  • 不要使用 x = []。使用图案修补。

    将 x 与 | 匹配 [] -> ... | h::t -> ...

  • 使用匿名函数而不是简短(即 <= 1 行)命名的单次使用函数:

    let dontDivide x = (x mod h <> 0) in let nonDivisors = List.filter doesntDivide t in

    让 nonDivisors = List.filter (fun x -> (x mod h <> 0)) t in

  • 谨慎使用命令式功能。

于 2013-08-13T16:59:12.570 回答
4

您声称是埃拉托色尼筛法的算法实际上不是;他们使用试除法而不是筛分法,这很容易通过寻找余数(mod 运算符)与零的比较来发现。这是 Eratosthenes 筛的一个简单实现,用伪代码代替 Ocaml,因为我已经很久没有写 Ocaml 代码了:

function primes(n)
    sieve := makeArray(2..n, True)
    for p from 2 to n
        if sieve[p]
            output p
            for i from p*p to n step p
                sieve[i] := False

这可以进一步优化,尽管对于像 n = 2000000 这样的小限制,这样做没有什么意义;在任何情况下,筛子都会比您使用的试用版快得多。如果你对使用素数编程感兴趣,我在我的博客上谦虚地推荐这篇文章。

于 2013-08-13T17:24:42.653 回答