【问题标题】:recursively convert Map[Int, Map[Int, X]] to Array[Array[X]]递归地将 Map[Int, Map[Int, X]] 转换为 Array[Array[X]]
【发布时间】:2011-05-20 08:55:21
【问题描述】:

我正在尝试编写一个函数,将带有整数键的 Maps 转换为相应的数组。我已经完成了基本案例,但正在尝试编写递归案例(即多维数组:将 Map[Int, Map[Int, X]] 转换为 Array[Array[X]])。

这个任务的产生是因为需要在不知道数组有多大的情况下从流中构造一个数组,从而允许元素以随机顺序从流中脱落,也有可能出现重复元素脱落流。

我有一个功能:

def toArrayHard[X:ClassManifest](x:scala.collection.Map[Int, X]):Array[X] =
{
    if (x.size == 0) new Array(0)
    else 
    {
        val max:Int = 1 + x.keys.max

        val a:Array[X] = new Array(max)

        var i = 0
        while (i < max)
        {
            a(i) = x(i)
            i += 1
        }
        a
    }
}

请注意,我知道如果映射包含键 k 但不包含键 i,其中 0

现在我希望对任意深度的多维数组做同样的事情。 例如,将 Map[Int, Map[Int, X]] 转换为 Array[Array[X]]。不幸的是,我被这些类型绊倒了。使用上述作为基本案例,这是我目前所拥有的:

def toArrayHardRec[X:ClassManifest](x:scala.collection.Map[Int, X]):Array[X] =
{
    import scala.collection.Map

    if (x.size == 0) new Array(0)
    else 
    {
        x match
        {
            case t:Map[Int, Map[Int, Y]] forSome { type Y } =>
            {
                val f0 = t.mapValues{m => toArrayHardRec[Map[Int, Y]](m)}
                toArrayHard(f0)
            }
            case _ => toArrayHard(x)
        }
    }
}

这是我得到的错误:

'=>' 应为,但找到了 'forSome'。

由于这是一项教育活动,因此非常感谢任何反馈。具体来说,如果对我的 Java 外观代码、现有的做同样事情的 scala 函数或对构建这些数组的替代方法的建议提出任何代码批评,我将不胜感激。

【问题讨论】:

  • 您应该避免将 { 放在下一行。它可能会导致分号推断出现问题。
  • @KingCub:你什么意思?能举个例子吗?

标签: arrays scala maps generic-programming scala-collections


【解决方案1】:

这是没有意义的:

case t:Map[Int, Map[Int, Y]] forSome { type Y }

由于已擦除,您所能检查的只有以下内容:

case t: Map[_, _]

事实上,这是您不会收到有关擦除的警告的唯一方法。此外,存在类型几乎总是不必要的,而且,就我个人而言,我总是发现它的语法有点难以正确。尽管如此,这是使用_ 表示存在类型就足够的一种情况。但是请注意,在我自己的代码(第一个版本)中,我不能使用它,因为如果我这样做,类型将不匹配。

接下来,

任意深度多维 数组

这不是一个特别好的主意。你必须知道你将返回什么类型来声明它——如果深度是“任意的”,那么类型也是如此。你可以使用Array[AnyRef],但使用起来会很痛苦。

不过,这是一种方法。我取消了所有循环,用 map 替换它们。请注意,我通过调用map m 来利用Map 也是Function1 这一事实。另请注意,我只是在数组(使用toArray 创建)上使用map,以避免必须管理地图创建。

def deMap(m: Map[Int, AnyRef]): Array[AnyRef] = {
  def translate(x: AnyRef): AnyRef = x match {
    case map: Map[Int, AnyRef] => deMap(map)
    case s: String => s
    case other => throw new IllegalArgumentException("Expected Map or String, got "+other.getClass.toString)
  }
  m.keys.toArray.sorted map m map translate
}

有两个问题。首先,如果键不连续(例如Map(0 -&gt; "a", 2 -&gt; "b")),则元素将错位。这是一种解决方法,将最后一行代码替换为:

  Array.range(0, m.keys.max + 1) map (m getOrElse (_, "")) map translate

这里我用一个空字符串来代表数组中的任何孔。

另一个问题是我假设所有地图的类型都是Map[Int, AnyRef]。为了解决这个问题,可能必须像这样重写:

def deMap(m: Map[Int, AnyRef]): Array[AnyRef] = {
  def translate(x: AnyRef): AnyRef = x match {
    case map: Map[_, _] => 
      val validMap = map collect { case (key: Int, value: AnyRef) => (key -> value) }
      deMap(validMap)
    case s: String => s
    case other => throw new IllegalArgumentException("Expected Map or String, got "+other.getClass.toString)
  }
  Array.range(0, m.keys.max + 1) map (m getOrElse (_, "")) map translate
}

在这里,我丢弃所有键不是Int 且值不是AnyRef 的对。可以进一步安全检查代码以测试是否缺少某些内容并在这种情况下报告和错误:

def deMap(m: Map[Int, AnyRef]): Array[AnyRef] = {
  def translate(x: AnyRef): AnyRef = x match {
    case map: Map[_, _] => 
      val validMap = map collect { case (key: Int, value: AnyRef) => (key -> value) }
      if (map.size > validMap.size) {
        val wrongPairs = map.toSeq diff validMap.toSeq
        throw new IllegalArgumentException("Invalid key/value pairs found: "+wrongPairs)
      }
      deMap(validMap)
    case s: String => s
    case other => throw new IllegalArgumentException("Expected Map or String, got "+other.getClass.toString)
  }
  Array.range(0, m.keys.max + 1) map (m getOrElse (_, "")) map translate
}

最后,关于性能。像我一样使用map 意味着将为每个map 分配一个新数组。有些人认为这意味着我必须迭代三次而不是一次,但由于每次我执行不同的任务,它并没有真正做更多的工作。但是,我每次都分配一个新数组这一事实肯定会对性能产生影响——既因为分配损失(Java 预先初始化所有数组),也因为缓存局部性问题。

避免这种情况的一种方法是使用view。当您在view 上执行mapflatMapfilter 时,您将获得一个新视图,该操作存储 以供将来使用(其他方法也可能以这种方式工作 - - 检查文档,或在有疑问时进行测试)。当您最终对view 对象执行apply 时,它将应用所有必要的操作来获取您所要求的特定元素。每次您为该元素apply 时它都会这样做,因此性能可能会更好或更差。

这里我将从Range 视图开始,以避免数组分配,然后在最后将视图转换为Array。尽管如此,keys 仍会创建一个集合,从而增加一些开销。在此之后,我将解释如何避免这种情况。

def deMap(m: Map[Int, AnyRef]): Array[AnyRef] = {
  def translate(x: AnyRef): AnyRef = x match {
    case map: Map[_, _] => 
      val validMap = map collect { case (key: Int, value: AnyRef) => (key -> value) }
      if (map.size > validMap.size) {
        val wrongPairs = map.toSeq diff validMap.toSeq
        throw new IllegalArgumentException("Invalid key/value pairs found: "+wrongPairs)
      }
      deMap(validMap)
    case s: String => s
    case other => throw new IllegalArgumentException("Expected Map or String, got "+other.getClass.toString)
  }
  (0 to m.keys.max view) map (m getOrElse (_, "")) map translate toArray
}

但是,您应该将 view 留给必要的优化,而不是主动使用它们。它不一定比普通集合快,而且它的非严格性可能很棘手。使用view 的另一种替代方法是使用StreamStream 很像 List,除了它只按需计算它的元素。这意味着在必要之前不会应用任何map 操作。要使用它,只需将倒数第二行替换为:

  Stream.range(0, m.keys.max + 1) map (m getOrElse (_, "")) map translate toArray

Stream 相对于view 的主要优势在于,一旦计算了Stream 中的值,它就会保持计算状态。这也是它相对于view 的主要缺点,具有讽刺意味的是。在这种特殊情况下,我认为view 更快。

最后,关于在不计算Setkeys 的结果)的情况下执行max。一个Map 也是一个Iterable,并且所有Iterable 都有一个max 方法,所以你可以简单地做m.max。然而,这将计算Tuple2[Int, AnyRef] 的最大值,这是一个没有Ordering 的类型。但是,max 接收到一个隐式参数,告诉它使用什么 Ordering,因此可以提供:

m.max(Ordering by ((_: (Int, AnyRef))._1))

这有点拗口,所以我们可以定义我们需要的隐式并使用它,错误,隐式。 :-)

def deMap(m: Map[Int, AnyRef]): Array[AnyRef] = {
  implicit val myOrdering = Ordering by ((_: (Int, AnyRef))._1)
  def translate(x: AnyRef): AnyRef = x match {
    case map: Map[_, _] => 
      val validMap = map collect { case (key: Int, value: AnyRef) => (key -> value) }
      if (map.size > validMap.size) {
        val wrongPairs = map.toSeq diff validMap.toSeq
        throw new IllegalArgumentException("Invalid key/value pairs found: "+wrongPairs)
      }
      deMap(validMap)
    case s: String => s
    case other => throw new IllegalArgumentException("Expected Map or String, got "+other.getClass.toString)
  }
  (0 to m.max._1 view) map (m getOrElse (_, "")) map translate toArray
}

注意max返回一个元组keyvalue,所以我们需要_1来获取key。另外,请注意,我们在deMap 的每个嵌套处创建一个Ordering 对象。不错,但可以通过在别处定义它来做得更好。

一旦你完成了所有这些,while 循环仍然会更快,尽管我不知道快多少。如果有足够的 JVM 优化,它们可能会非常接近。另一方面,如果您真的想要速度,则可以一直使用汇编代码。它归结为在编写代码的难易程度、维护难易程度以及运行速度之间取得平衡。

【讨论】:

  • 谢谢!关于擦除的有趣点,你的代码比我的优雅得多。 “toArray” + “map” 在速度上与 while 循环相比如何?另外,你说:“你必须知道你会返回什么类型来声明它。”我希望通过使用高阶类型(目前这对我来说似乎很神奇),我可以避免为每个深度创建一个新的“deMap”函数;但是在 scala API 中看到 Array 的伴随对象中的“ofDim”方法让我信服了。
  • 哇,这个答案太棒了!学到了很多,非常感谢。
猜你喜欢
  • 2019-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-06-10
  • 1970-01-01
  • 2013-01-24
  • 1970-01-01
  • 2023-02-02
相关资源
最近更新 更多