【问题标题】:Idiomatic Scala solution for the Dutch national flag problem荷兰国旗问题的惯用 Scala 解决方案
【发布时间】:2018-07-20 21:40:47
【问题描述】:

我正在解决 Scala 中的Dutch national flag problem,并得出以下代码:

def dutchNationalFlag[T](a: Array[T])(implicit ordering: Ordering[T]) = {
  def sort(lo: Int, hi: Int): Unit = {
    Stream.iterate((lo, hi, lo + 1)) { acc =>
      val (lt, gt, i) = acc

      if (ordering.lt(a(i), a(lt))) {
        swap(lt, i, a)
        (lt + 1, gt, i + 1)
      } else if (ordering.gt(a(i), a(gt))) {
        swap(gt, i, a)
        (lt, gt - 1, i)
      } else {
        (lt, gt, i + 1)
      }
    }
      .takeWhile(acc => acc._2 >= acc._3)
      .lastOption
      .foreach { acc =>
        val (lt, gt, _) = acc

        sort(lo, lt - 1)
        sort(gt + 1, hi)
      }
  }

  sort(0, a.length - 1)
}

出于性能原因,我想修改现有数组,而不是创建新数组。上面的代码有效,但它在从iterate 调用swap 时有明显的副作用,这在纯函数式代码中是有问题的。我考虑将swap 替换为稍后在foreach 中执行的方法引用,类似于Haskell IO,但您可以想象,这样做会使代码有些复杂。

其他想法?

【问题讨论】:

  • 可变操作在纯函数式编程的上下文中毫无意义。如果您不能进行纯函数式编程并相信编译器会进行优化,那么就没有理由遵守更严格的标准。如果您保存所有交换并在最后应用它们,那仍然会产生您一开始就试图避免的内存开销,而不会获得纯 FP 的任何好处。
  • @Ethan 在我看来,我有一个问题想以有效的方式解决。如果纯 FP 让这变得困难,我没有理由订阅酷儿童俱乐部。也就是说,我不相信纯 FP 代码不能像命令式一样高效。
  • 对此有高效、简洁的纯 FP 解决方案。他们不会通过在可变数组上调用交换来做到这一点。并且根据定义这样做的任何解决方案都不能是纯 FP。强制答案生成数组交换意味着您不会得到有效的 FP 答案。
  • @Ethan “高效、简洁的纯 FP 解决方案”正是我想要的。请展示,而不是告诉。

标签: scala functional-programming quicksort side-effects dutch-national-flag-problem


【解决方案1】:

在将命令式算法转换为纯函数式算法时,“出于性能原因改变数组”是一个经典问题,但如果您对数据结构更聪明一点,就有很多方法可以解决这个问题。例如,如果你像这样表示你的数组怎么办:

class RearrangedArray[A](elements: Array[A], indexMap: Map[Int, Int] = Map.empty) {
  def apply(i: Int): A = elements(indexMap.getOrElse(i, i))
  def swap(i: Int, j: Int): RearrangedArray = {
    val newMap = indexMap + (i -> indexMap.getOrElse(j, j)) + (j -> indexMap.getOrElse(i, i))
    new RearrangedArray(elements, newMap)
  }
  def toArray: Array[A] = {
    (0 until elements.size).map(apply).toArray
  }
}

请注意,它是完全不可变的,使用 O(n) 内存,交换和查找在恒定时间内发生...所有相同的性能特征,但您可以在每次迭代时创建一个新的,并将其与您的索引一起传递给使其完全参照透明。与其改变数组,不如让你的所有函数都将其中一个作为参数,并返回一个新的作为结果。

【讨论】:

  • 您能否更新您的答案以显示RearrangedArray 将如何与我的问题中的代码一起使用?如果我理解你的想法,我可以只传递一个包含数组最新索引的索引映射。对于简单的Int 数组,此解决方案占用3 倍的内存量,O(n) 用于数组,2 * O(n) 用于映射。
【解决方案2】:

以下是我倾向于解决问题的方法

def sort[T](low: T, high: T)(items: List[T])(implicit ordering: Ordering[T])=
{
  val groupedItems = items.groupBy
  {
    _ match
    {
      case item if ordering.lt(item,low) => 0
      case item if ordering.gt(item,high) => 2
      case _ => 1
    }
  }
  groupedItems(0) ++ groupedItems(1) ++ groupedItems(2)
}

这可能比编写 Java 或 C 风格的代码效率低。一般来说,这是可以的。 Scala 编译器非常擅长优化事物,而且性能通常足够接近,以至于您不会真正注意到差异。如果您打算对大量数据执行此操作,Scala 版本的优势在于它已经是一种并行算法。如果您在输入列表上调用.par,您将获得一个并行集合,它将跨多个内核/线程运行操作。如果将List[T] 更改为org.apache.spark.rdd.RDD[T],那么代码就可以在拥有数十台机器的分布式集群上运行了。

【讨论】:

  • 上面的代码对项目进行了分组,但没有对它们进行排序;你错过了递归调用。
猜你喜欢
  • 2017-05-11
  • 2018-10-26
  • 2012-06-28
  • 2011-05-04
  • 2022-11-19
  • 1970-01-01
  • 1970-01-01
  • 2020-02-09
  • 2021-05-03
相关资源
最近更新 更多