【问题标题】:Optimisation advice (HashMap)优化建议(HashMap)
【发布时间】:2017-05-23 03:49:28
【问题描述】:

我有一个很大的坐标列表(这个表格):

    if(x == 1055 && y == 63 && z == 1117)
        return blackwood;
    if(x == 1053 && y == 63 && z == 1117)
        return blackwood;
    if(x == 1049 && y == 64 && z == 1113)
        return blackwood;
    if(x == 1054 && y == 63 && z == 1112)
        return blackwood;
    if(x == 1058 && y == 63 && z == 1112)
        return blackwood;
    if(x == 1062 && y == 64 && z == 1117)
        return blackwood;
    if(x == 1050 && y == 64 && z == 1117)
        return blackwood;
    if(x == 1062 && y == 64 && z == 1118)
        return glass;
    if(x == 1050 && y == 64 && z == 1118)
        return andesite;

(比那个长得多)

但是,当我调用执行这些指令的方法时,我有一个延迟(不是很长,但足以在游戏中产生冻结印象)。

所以,我的问题是,我该如何优化?

我正在考虑将这些存储在HashMap 中并使用HashMap.get(key),但是,HashMap.get(key) 是否会迭代列表以找出它?

【问题讨论】:

  • 没有。 HashMaps 基本上 在恒定时间内返回 afaik。这就是人们使用它们的原因。请注意,尽管要将坐标放入 HashMap 中,您需要将数字分组到向量或其他东西中,并且重复对容器进行哈希处理也可能会很慢;虽然可能比您当前的 t 方法更快。
  • HashMap.get() 是常数时间(加上冲突解决 - 取决于列表大小与列表中项目之间的比率)
  • 对什么是哈希表进行(自己的)研究。这里有一些东西可以帮助您入门:How does a hash table work
  • 也许你需要一个游戏引擎来绘制地图或检查碰撞。
  • 您似乎有空间相关信息。您是否考虑过将实现更改为KD-Tree

标签: java list optimization hashmap


【解决方案1】:

您确实可以使用带有自定义类作为键的 Map,该自定义类将这 3 个数据用作组件值:xyz

通过公平实现hashCode() 方法,它应该是一个常数时间 [O(1)] 或非常接近。

如果您必须根据需要经常重新创建地图,那么使用地图可能会无能为力,因为从一侧您可能会失去从另一侧获得的收益。

因此,创建这个自定义类并通过考虑这 3 个字段来覆盖 hashCode()equals()

public class Coordinate {

    private int x;
    private int y;
    private int z;

    public Coordinate(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + x;
        result = prime * result + y;
        result = prime * result + z;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (!(obj instanceof Coordinate))
            return false;

        Coordinate other = (Coordinate) obj;
        if (x == other.x && y == other.y && z == other.z)
            return true;

        return false;
    }


}

然后您可以使用预期的键值对地图进行初始化:

Map<Coordinate, MyValue> mapValuesByCoord = new HashMap<>();
mapValuesByCoord.put(new Coordinate(1055,63,1117), blackwood);
mapValuesByCoord.put(new Coordinate(1053,63,1117), blackwood);
mapValuesByCoord.put(new Coordinate(1062, 64, 1118), glass);

...

并以这种方式使用地图:

MyValue value = mapValuesByCoord.get(new Coordinate(1055,63,1117));

【讨论】:

    【解决方案2】:

    这里存在一个问题,即“地图”一词的常用用法,例如在一张纸中显示一块土地的微型表示,而程序员或数学家使用“地图”,其中值相关联与其他值。

    如果几乎​​每个 3D 坐标都有相应的返回值,那么最好有一个直接查找表。您可以使用 3D 数组来执行此操作,即 array-of-arrays-of-arrays:

     Rock[][][] rockMap =  ... // Google for guidance on initialising a 3D array
    
     ...
     rockMap[1054][63][1112] = blackwood;
     // etc.
    

    这里的“rockMap”更接近于“map”这个词的常见用法。如果你画了那个 3D 数组,它就是你的“世界地图”。

    那么你的查找是:

     return rockMap[x][y][z];
    

    或者,您可以使用一维数组并根据 x、y、z 计算索引:

     Rock[] rockMap = new Rock[SIZE_X * SIZE_Y * SIZE_Z];
    
     rockMap[1054 * SIZE_Y * SIZE_Z + 63 * SIZE_Z + 1112] = blackwood;
    

    查找类似于赋值:

     return rockMap[x * SIZE_Y * SIZE_Z + y * SIZE_Z + z];
    

    如果您没有为每个坐标设置一块石头,那么这种方法仍然有效(您只需在所有空槽中都有一个空值或其他一些缺席标记)。但是这样会有点浪费空间。

    在这种情况下,创建Coordinate 类型并构造Map&lt;Coordinate&gt; 可能更有效。

     Map<Coordinate> rockMap = ...;
     rockMap.put(new Coordinate(x,y,z), blackwood);
    
     ...
    
     return rockMap.get(new Coordinate(x,y,z));
    

    您应该进行自己的测试以找出哪种类型的Map 最适合您的程序——HashMapTreeMap?测试它们。

    为了使这些工作,Coordinate 需要实现某些方法——检查 Javadoc 并确保它确实如此。

    【讨论】:

      【解决方案3】:

      HashMap 很少通过迭代来从其结构中获取元素。正确的实现很少迭代,以至于人们通常不会费心提及该部分的存在。因此 O(1) 复杂度。

      想象一下你的元素表(每个元素都有一个键)。理想情况下,HashMap 会将您的每个元素放在一个唯一的行中(因此每行只有一个元素)。当给定一个键来获取一个元素时,HashMap 将计算该键的哈希(int)并找出其适当的值位于哪一行。一行中可能出现多个元素,因为有时两个不同的键可能具有相同的哈希值,然后您迭代该行以找到您的元素。

      我认为您可以在问题上使用 HashMap 来加快速度(其中 x、y 和 z 应该是唯一键)。

      【讨论】:

        【解决方案4】:

        我会创建具有 check(x, y, z) 方法的黑木、玻璃和安山岩对象。

        编辑:

        也许我还应该补充一点,为了提高效率,您可以像这样实现 blackwood.check(x, y, z):

          public boolean check(int x, int y, int z){
              if ( y == 63 || y == 64){
                  if (z == 1117 || z == 1113 || z == 1112){
                      if (x == 1055 || x == 1053 || x == 1049 || x = 1054 || 
                          x == 1058 || x == 1062 || x == 1050){
                          return true;
                      }
                  }
              }
            return false;
        }
        

        你会从中得到什么:

        1) 每种类型的逻辑都封装在单独的类中,您不会有一个带有长“ifs”的巨大方法。并且很容易添加新类型。

        2) 通过将“y”检查移到顶部,如果它不是 63 或 64,您将快速退出。同样适用于 z。由于 Java 中的“或”检查不会检查其余部分,因此如果 x 为 1055,则不检查所有“x”值会购买一些性能。

        3) 使用 Map 或 Set 方法,您必须在每次调用您的方法时创建 X、Y、Z 包装键对象。由于该方法被非常频繁地调用,您可能会遇到垃圾收集器无法足够快地清理它们的问题。

        【讨论】: