将任意大小的矩阵/ 2d阵列合并为一个大的2d阵列

时间:2014-09-21 15:51:44

标签: arrays scala

我是Scala的新手,这是一个非常愚蠢的问题,但我被困在这里。 我有多个Int的2D阵列,我想合并成一个大的2D阵列。

到目前为止的代码:

object Generator
{
  // Use a HashMap for greate flexibility in the future. We could more easily add additional checks for biome placement like if(x, y-1) ...
  val biomes:mutable.HashMap[Vector2, Biome] = new mutable.HashMap[Vector2, Biome]

  def create(width:Int, height:Int)
  {
    for(x <- 0 until width; y <- 0 until height)
    {
      if(y < height / 3 * 2)
        biomes.put(new Vector2(x, y), new GrasslandBiome(128, 64))
      else
        biomes.put(new Vector2(x, y), new SkyBiome(128, 64))
    }

    terrain
  }

  def terrain
  {
    var tileMap:ArrayBuffer[Array[Array[Int]]] = new ArrayBuffer[Array[Array[Int]]]
    for((position, biome) <- biomes)
    {
      tileMap += biome.tileMap
    }

    Predef.println(tileMap.toString) // TODO remove
  }
}

Biome只不过是2D数组的生成器:var data:Array [Array [Int]](是的,这将是一个相当简单的游戏,可以更好地了解Scala! :))

我的问题是ArrayBuffer似乎是错误的选择,因为我没有得到预期的结果,这是一个大的2D阵列。事实上我得到了:

ArrayBuffer([[I@1ed8f33e, [[I@78c86c9a, [[I@46c4ace3, [[I@4de0dedb)

然后是NullPointerException的{​​{1}} :(

我非常感谢任何帮助解决我在推理(和编程)中的错误,并希望祝你有个美好的一天! :)

编辑:当然相应的Biome数据不是未初始化/ null!如果我手动打印,一切都按预期进行。

edit2:根据要求:

println

和一个例子:

abstract class Biome(width:Int, height:Int)
{
  var tileMap: Array[Array[Int]]

  def create();

  def printTileMap()
  {
    printTileMap(tileMap, width, height)
  }

  def printTileMap(map: Array[Array[Int]], mapWidth:Int, mapHeight:Int)
  {
    var x = mapHeight - 1
    while(x >= 0)
    {
      for (y <- 0 until mapWidth) Predef.print(map(y)(x) + " ")
      println()
      x -= 1
    }
  }
}

另外,一个丑陋的[b]解决方案[/ b]将迭代整个HashMap,总结每个生物群系的宽度和高度,用该信息初始化tileMap并简单地将所有内容复制到正确的位置。但这似乎是一个令人讨厌的解决方案。

1 个答案:

答案 0 :(得分:1)

首先,我找到了各种数组类型之间差异的解释: scala - array vs arrayseq

现在关于如何从二维数组中获取一维数组的问题:但是让我们假设,而不是Array[Array[T]]我们有List[List[T]](因为我喜欢列表)更多;但你应该没有问题应用于数组)。

foldLeft[A](initial: A)(function: (A, B) => A)做什么?它通过在每个元素上应用函数A来迭代集合并聚合类型(A, B) => A的值。这里,B是列表的通用类型。在我们的示例中,我们通过将中间值B连接起来(A)来将整数列表(++)的列表折叠为整数列表(a)。列表中的下一个元素。


编辑2:

将此应用于Biome示例时,我们可以创建从位置到生物群系的地图,如下所示:

def create(width:Int, height:Int): Map[Vector2, Biome] =
{
  // we can define functions within functions
  def isGrasslandBiome(x: Int, y: Int): Boolean =
    y < height / 3 * 2

  // A for-comprehension returns a sequence of all computed elements
  val positionWithBiomes: Seq[(Vector2, Biome)] =
    for{
      x <- 0 until width
      y <- 0 until height
    } yield
    {
      val biome =
        if (isGrasslandBiome(x, y))
          GrasslandBiome(DefaultWidth, DefaultHeight)
        else
          SkyBiome(DefaultWidth, DefaultHeight)
      ((x, y), biome)
    }
  // We fold each element of the sequence into a map
  val biomes: Map[Vector2, Biome] =
    positionWithBiomes.foldLeft[Map[Vector2, Biome]](Map.empty){
      case (map, ((x, y), biom)) => map + ((x, y) -> biom)
    }
  biomes
}

我们迭代((Int, Int), Biome) s的序列并将它们聚合在Map中。我更喜欢不可变集合(大多数标准Scala集合)到可变集合。在调试代码以获得可能的性能优势时,可变性可能会给您带来困难。此外,不变性符合函数式,因为函数(在数学意义上)总是纯粹的,即不会改变状态。

尽管如此,为了示例,并且因为tileMap的内部表示可能实际上对性能至关重要,让我们定义一个类TileMap,它本质上是一个两个包装器 - 维数组:(注:type TileMapInternal = mutable.ArraySeq[mutable.ArraySeq[Int]]

/** A tile map is our internal, mutable representation of a Biome
  *
  * Note that this makes Biome mutable
  */
/*mutable*/ class TileMap private(private val value: TileMapInternal, private val width: Int, private val height: Int) {

def print()
{
  value foreach { row =>
    row foreach { tile =>
      Predef.print(s"$tile ")
    }
    println()
  }
}

def setAllTiles(tileValue: Int) {
  for{
    x <- 0 until width
    y <- 0 until height
  }{
    setTile(tileValue)(x, y)
  }
}

def setTile(tileValue: Int)(x: Int, y: Int) {
  value(y)(x) = tileValue
}

def addRows(other: TileMap): TileMap =
  new TileMap(value ++ other.value,
    width, // we add rows -> width stays the same
    height + other.height
  )

def addColumns(other: TileMap): TileMap =
  new TileMap(
    value zip other.value map {case (first, second) => first ++ second},
    width + other.width,
    height // we add columns -> height stays the same
  )

}

这个类有一个私有构造函数,因为我们不希望内部表示泄露出来。在Scala中,您可以定义一个可以访问所有私有成员的协同对象。基本上,伴随对象包含您在Java中声明静态的所有方法。

// companion object
object TileMap {
  def empty(width: Int, height: Int): TileMap = {
    val rows = new mutable.ArraySeq[mutable.ArraySeq[Int]](height)
    for (row <- 0 until height) {
      rows(row) = new mutable.ArraySeq[Int](width)
      for (cell <- 0 until width) {
        rows(row)(cell) = 0
      }
    }
    new TileMap(rows, width, height)
  }
}

伴侣对象将为我们初始化一个空的TileMap。这很可能写得更优雅,但它完成了它应该做的事情:它创建一个宽度为x的数组,并将所有元素设置为零。您的Nullpointer异常可能是因为您没有正确初始化元素。当您尝试手动打印时,由于自动装箱,他们可能只打印0而不是null

修改

如何在单个2d数组中累积地图:

方法addRowsaddColumns通过组合内部或外部数组来创建新的TileMap(即您的二维数组)。组合外部数组很简单:只需使用在所有scala集合上定义的方法++添加它们。要组合内部数组,我们可以先将zip两个数组放在一起,这样就可以得到一个数组元组的数组(Array[(Array[Int], Array[Int])])。然后,我们可以通过在每个元组上应用map将此数组++转换为新的常规二维数组。

我们现在可以使用这两个函数将二维数组的tile map(基本上就是我们使用val biomes: Map[Vector2, Biome]存储的内容)减少到一个tile map中。

我应该提一下,按照设计,这看起来非常脆弱:一旦你的瓷砖地图尺寸不同,我想象要破坏的东西。另外,搞乱行/列组合的顺序几乎不可能调试,如果你的生物群系地图还没有完成,计算地形可能会崩溃。然而,这就是我如何通过当前设计解决问题:

def terrain(biomes: Map[Vector2, Biome]): TileMap = {
  val (rows, _) = biomes.keys.unzip
  rows.toList.sortBy{id => id} map {
    row => terrainForRow(row, biomes)
  } reduceLeft {
      (accumulator: TileMap, nexTileMap: TileMap) => accumulator addColumns nexTileMap
  }
}

def terrainForRow(column: Int, biomes: Map[Vector2, Biome]): TileMap =
  biomes.filter{
    case ((x, y), _) => x == column
  }.toList.sortBy{
    case ((_, y), _) => y
  }.map{
    case ((_, _), biome) => biome.tileMap
  }.reduceLeft[TileMap]{
    (accumulator, nextTileMap) => accumulator addRows nextTileMap
  }

这当然需要一些解释:terrainForRow将占用一行(即地图中的所有值与x具有相同的值)并以正确的顺序连接它们。订单由y确定,因为我们希望保留列的顺序。

  • 首先,我们filter所有具有x

  • 相同值的生物群落
  • 其次,我们通过将其转换为列表来为集合添加排序,并根据y的值

  • 对列表进行排序
  • 第三,我们丢弃位置(xy的值),并提取平铺贴图。如果我们想要使用reduceLeft,则需要这样做,因为它的类型签名。

  • 最后,我们累积了瓷砖地图,调用我们之前定义的addRows。请注意,reduceLeft仅针对大小为2且更大的

  • 的集合定义

在方法terrain中,我们为每一行调用terrainForRow(也就是说,在我们按行对图块进行排序之后)。每个结果都是TileMap,因此我们可以对结果序列调用reduceLeft,并使用我们定义的函数TileMap将它们缩减为单个addColumns

我已将更新的代码放在 http://pastie.org/9598459


我想在scala中添加一些你可以做的事情,因为你想学习这门语言:

结束和部分应用功能

查看方法setTile的签名:它有两个参数列表。这称为currying,允许我们部分应用函数。例如,我们可以创建一个这样的函数:

// let us partially apply the setTile function
val setGrassTileAt: (Int, Int) => Unit =
  biomesTileMap.setTile(GrassTileValue)
// now tileBuilder will set the value of GrassTile at each position we provide
setGrassTileAt(0, 0)
setGrassTileAt(3, 1)
setGrassTileAt(0, 1)

函数setGrassTile(存储在变量中,因为在scala中,函数是一等公民),获取两个整数值(坐标)并将该坐标处的生物群系的值设置为{{ 1}}。

统一访问原则和案例类

GrassTileValue

在Scala中,客户端在访问对象成员时不应区分变量和方法。例如,abstract class Biome { // Scala supports the uniform access princple // A client is indifferent whether this is def, val or var // A subclass can implement width and height as val def width: Int def height: Int def tileMap: TileMap } // Case classes automatically define the methods equals, hashCode and toString // They also have an implicit companion object defining apply and unapply // Note that equals is now defined on width and height, which is probably not what // we want in this case. case class SkyBiome(width: Int, height: Int) extends Biome { val tileMap: TileMap = TileMap.empty(width, height) } case class GrasslandBiome(width: Int, height: Int) extends Biome { val tileMap: TileMap = TileMap.empty(width, height) } 是超类型中的width还是子类型中的def无关紧要。

案例类是覆盖valequalshashCode的类。此外,它们还提供了一个提取器toString(可以在unapply时使用)和match方法(当您将对象视为函数时应用它)。案例类对价值对象最有利。

我希望我能帮助你一点。 修改:我更新的答案前的代码:http://pastie.org/9586136