荷兰国旗问题的惯用Scala解决方案

时间:2018-07-20 21:40:48

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

我正在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)
}

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

还有其他想法吗?

2 个答案:

答案 0 :(得分:0)

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

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)内存,交换和查找在恒定时间内进行...所有相同的性能特征,但是您可以在每次迭代中创建一个新变量,并将其与索引一起传递给使它完全参照透明。不必使数组发生变异,而是让您的所有函数都将其中一个作为参数,然后返回一个新的函数。

答案 1 :(得分:0)

以下是我倾向于解决

中的问题的方法
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],则代码可以在具有数十台计算机的分布式集群上运行。