【问题标题】:Trial division for primes with immutable collections in ScalaScala 中具有不可变集合的素数的试除法
【发布时间】:2015-05-31 21:46:05
【问题描述】:

我正在尝试通过重写基本练习来学习 Scala 和函数式编程思想。目前,我对生成素数“试除法”的幼稚方法遇到了麻烦。

下面描述的问题是我无法在函数式风格保持效率上重写众所周知的算法,因为我没有合适的不可变数据结构,如 List,但不仅在头部,而且在最后。

我开始编写 java 代码,该代码对每个奇数通过已找到的素数(受测试值的平方根限制)测试其可除性 - 如果未找到除数,则将其添加到列表的末尾。

http://ideone.com/QE8U0I

    List<Integer> primes = new ArrayList<>();
    primes.add(2);
    int cur = 3;
    while (primes.size() < 100000) {
        for (Integer x : primes) {
            if (x * x > cur) {
                primes.add(cur);
                break;
            }
            if (cur % x == 0) {
                break;
            }
        }
        cur += 2;
    }

现在我尝试以“功能方式”重写它——使用递归而不是循环没有问题,但我坚持使用不可变集合。核心思想如下:

http://ideone.com/4DQ6mi

def primes(n: Int) = {
    @tailrec
    def divisibleByAny(x: Int, list: List[Int]): Boolean = {
        if (list.isEmpty) false else {
            val h = list.head
            h * h <= x && (x % h == 0 || divisibleByAny(x, list.tail))
        }
    }
    @tailrec
    def morePrimes(from: Int, prev: List[Int]): List[Int] = {
        if (prev.size == n) prev else
            morePrimes(from + 2, if (divisibleByAny(from, prev)) prev else prev :+ from)
    }
    morePrimes(3, List(2))
}

但它 - 如果我理解正确,因为添加到不可变列表末尾的操作需要创建整个内容的新副本。

我搜索了文档以找到更合适的数据结构并尝试用不可变队列替换列表,因为据说:

将项目添加到队列总是需要花费 O(1) ... 删除项目平均需要 O(1)。

但它仍然更慢:

http://ideone.com/v8BsuQ

def primes(n: Int) = {
    @tailrec
    def divisibleByAny(x: Int, list: Queue[Int]): Boolean = {
        if (list.isEmpty) false else {
            val (h, t) = list.dequeue
            h * h <= x && (x % h == 0 || divisibleByAny(x, t))
        }
    }
    @tailrec
    def morePrimes(from: Int, prev: Queue[Int]): Queue[Int] = {
        if (prev.size == n) prev else
            morePrimes(from + 2, if (divisibleByAny(from, prev)) prev else prev.enqueue(from))
    }
    morePrimes(3, Queue(2))
}

出了什么问题或者我错过了什么?

附:我相信还有其他算法可以生成更适合函数式风格的素数。我想我见过一些纸。但现在我对这个感兴趣,或者更准确地说是对合适的数据结构的存在感兴趣。

【问题讨论】:

  • Vectors 添加、删除(从两端)和检索的成本为 O(1)。除此之外,添加到列表的开头是 O(1) 那么为什么不这样做呢?
  • 没关系,我明白为什么了。

标签: algorithm scala data-structures functional-programming


【解决方案1】:

根据http://docs.scala-lang.org/overviews/collections/performance-characteristics.htmlVectors 有一个用于追加、前置和查找的摊销固定成本。实际上,在您的解决方案中使用向量而不是列表要快得多

def primes(n: Int) = {
  @tailrec
  def divisibleByAny(x: Int, list: Vector[Int]): Boolean = {
    if (list.isEmpty) false else {
      val (h +: t) = list
      h * h <= x && (x % h == 0 || divisibleByAny(x, t))
    }
  }
  @tailrec
  def morePrimes(from: Int, prev: Vector[Int]): Vector[Int] = {
    if (prev.length == n) prev else
      morePrimes(from + 2, if (divisibleByAny(from, prev)) prev else prev :+ from)
  }
  morePrimes(3, Vector(2))
}

http://ideone.com/x3k4A3

【讨论】:

  • 谢谢,我试过了,它看起来和我要找的完全一样。之前浏览此比较表时,似乎将 Vector 误认为是类似名称的 java 类。我现在很开心!
【解决方案2】:

我认为你有两个主要选择

  1. 使用矢量 - 这比附加列表更好。它是一个位图 Trie 数据结构 (http://en.wikipedia.org/wiki/Trie)。附加到的“有效” O(1)(即平均 O(1))

或者...可能是您不想要的答案

  1. 使用像 ListBuffer 这样的可变数据结构 - 尝试实现不可变性非常好,并且应该是您的首选 - 但有时出于效率原因,您可能会使用可变结构。确保它不会“泄漏”到您的课程中的关键是什么。如果您查看 List.scala 实现,您会发现 ListBuffer 在内部使用了很多。但是,它在离开班级之前被转换回列表。如果它对核心 Scala 库来说足够好,那么在需要它的特殊情况下使用它可能是可以的。

【讨论】:

    【解决方案3】:

    除了使用Vector,还可以考虑使用高阶函数而不是递归。这也是一种完全有效的函数式风格。在我的机器上,divisibleByAny 的以下实现比运行primes(1000000) 时的@Pyetras tailrec 实现快大约 8 倍:

    def divisibleByAny(x: Int, list: Vector[Int]): Boolean =
      list.view.takeWhile(el => el * el <= x).exists(x % _ == 0)
    

    【讨论】:

      猜你喜欢
      • 2012-01-07
      • 2013-08-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-01-14
      • 1970-01-01
      • 1970-01-01
      • 2011-11-24
      相关资源
      最近更新 更多