【问题标题】:Is Hashing a suitable solution? Am I over-complicating it?散列是一个合适的解决方案吗?我是否过于复杂了?
【发布时间】:2013-11-07 08:00:08
【问题描述】:

我编写了一个 2D 平台游戏,我需要有(最多 4 个)门的房间。我用 Java 写的,但语言无关紧要。

每个房间可以有 4 扇门,分别位于顶部、底部和侧面。我称他们为NORTHSOUTHEASTWEST。当我建造一个房间时,我只给它一个整数,整数中的每一位代表一扇门。

例如,如果我想要一个有 3 个门的房间 (北一个,东一个,西一个) 我给房间编号:11(二进制1011)。

因此,每扇门都有一个整数,用来识别它。

NORTH = 8;//1000
SOUTH = 4;//0100
EAST =  2;//0010
WEST =  1;//0001

如果我生成一个房间,我会给它这些标识符的组合。

例如:前面提到的房间会得到标识符

doorStock = NORTH | EAST | WEST;

我将这些门存储在一个简单的数组中:

Door doors[] = new Door[4];

我的问题是:我需要一个可以将标识符映射到数组中正确索引的函数。我并不总是需要 4 扇门。

起初我所做的似乎是最简单的:门数组总是有 4 个元素,而我不会使用的索引将保持为空。

public Door getDoor(int doorID){
    switch(doorID){
        case NORTH:{
            return doors[0];
        }
        case SOUTH:{
            return doors[1];
        }
        case EAST:{
            return doors[2];
        }
        case WEST:{
            return doors[3];
        }
    }
    return null;
}

为了安全起见,我需要确定我请求的门是否真的存在于房间中。

private boolean doorExists(int doorID){
    return (doorID & doorStock) != 0
}

因此,查询函数如下所示:

public Door getDoor(int doorID){
    switch(doorID){
        case NORTH:{
            if(doorExists(NORTH))return doors[0];
            else return null;
        }
        case SOUTH:{
            if(doorExists(NORTH))return doors[1];
            else return null;
        }
        case EAST:{
            if(doorExists(NORTH))return doors[2];
            else return null;
        }
        case WEST:{
            if(doorExists(NORTH))return doors[3];
            else return null;
        }
    }
    return null;
}

哪个有效。但!这样,数组可能会因未使用的元素而浪费空间。此外,Door 类可能是任意大小,这会增加内存浪费。

更不用说我可能需要更多的门“槽”(例如,如果我尝试在 3D 中实现它),所以我决定尝试根据门的标识符制作门数组的大小:

Door doors = new Door[Integer.bitCount(doorStock)];

这很快就给出了IndexOutOfBounds 错误。我并不感到惊讶,因为门数组可以是 0 到 4 之间的任意大小,所以我需要一种新的散列方法。

我想出了两个哈希表,一个用于数组索引:

private final int[][] doorhash = {
    /* NORTH  SOUTH   EAST    WEST doorStock*/
    { -1,     -1,     -1,     -1} /*0000*/,
    { -1,     -1,     -1,      0} /*0001*/,
    { -1,     -1,      0,     -1} /*0010*/,
    { -1,     -1,      0,      1} /*0011*/,
    { -1,      0,     -1,     -1} /*0100*/,
    { -1,      0,     -1,      1} /*0101*/,
    { -1,      0,      1,     -1} /*0110*/,
    { -1,      0,      1,      2} /*0111*/,
    {  0,     -1,     -1,     -1} /*1000*/,
    {  0,     -1,     -1,      1} /*1001*/,
    {  0,     -1,      1,     -1} /*1010*/,
    {  0,     -1,      1,      2} /*1011*/,
    {  0,      1,     -1,     -1} /*1100*/,
    {  0,      1,     -1,      2} /*1101*/,
    {  0,      1,      2,     -1} /*1110*/,
    {  0,      1,      2,      3} /*1111*/
};

还有一个,它有助于上一张表的映射:

private final int[] directionHash = {
    -1, /*0000*/
     3, /*0001 - WEST*/
     2, /*0010 - EAST*/
    -1, /*0011*/
     1, /*0100 - SOUTH*/
    -1, /*0101*/
    -1, /*0110*/
    -1, /*0111*/
     0, /*1000 - NORTH*/
};

所以我当前的映射函数如下所示:

public Door getDoor(int doorID){
    switch(doorID){
        case NORTH:{
            if(doorExists(NORTH))return doors[doorhash[doorStock][directionHash[NORTH]]];
            else return null;
        }
        case SOUTH:{
            if(doorExists(NORTH))return doors[doorhash[doorStock][directionHash[SOUTH]]];
            else return null;
        }
        case EAST:{
            if(doorExists(NORTH))return doors[doorhash[doorStock][directionHash[EAST]]];
            else return null;
        }
        case WEST:{
            if(doorExists(NORTH))return doors[doorhash[doorStock][directionHash[WEST]]];
            else return null;
        }
    }
    return null;
}

这似乎也可以正常工作,但我觉得有一个更简单的解决方案可以解决这个问题,或者使用较少浪费的哈希表。我觉得这不像它应该的那样渐近灵活,或者我把事情复杂化了。什么是更好的方法?

感谢您的宝贵时间!

【问题讨论】:

  • 没有门的房间不是房间,所以0000 是非法值。
  • “这样阵列可能会因为未使用的元素而浪费空间”——除非您拥有数百万个房间,否则无需担心。您的数组将存储对 Door 对象的引用,而不是对象本身(即 C++ 术语中的指针),因此 Door 类的大小与无门 null 无关。
  • 你为什么要这样做,它有一种疯狂的感觉。 4 个布尔值有什么问题?
  • 除了 Rups 评论:您正在尝试优化 Door 类中浪费空间的可能性,这导致了这个问题。这可能表明你想多了 - 过早的优化,万恶之源,yadda yadda。
  • @PeterRader:如果您使用传送,则不会:-)

标签: java arrays hash concept


【解决方案1】:

枚举是你的朋友:

// Each possible direction - with Up/Down added to show extendability.
public enum Dir {
  North,
  South,
  East,
  West,
  Up,
  Down;
}

class Room {
  // The doors.
  EnumSet doors = EnumSet.noneOf(Dir.class);

  // Many other possible constructors.
  public Room ( Dir ... doors) {
    this.doors.addAll(Arrays.asList(doors));
  }

  public boolean doorExists (Dir dir) {
    return doors.contains(dir);
  }
}

让枚举为您完成繁重的工作在这里很自然。他们还提供了一个现成的EnumSet,非常高效。

【讨论】:

  • 比公认的答案要好得多,因为它是可扩展的。
  • 底线是,实现应该隐藏在合适的界面后面,除非我们正在做一些可笑的过度优化的事情,也许是一些面向数百万用户的 5 维动作射击的实时手机游戏...
【解决方案2】:

您的解决方案可能是最节省空间的,但可能是时间效率低下的,而且绝对是最不清晰的写法。

一个简单的面向对象的方法是有一个Room 类,有4 个booleans,可以在一个数组中,也可以根据你的需要独立;

public class Room {

    //Consider an ENUM for bonus points
    public static int NORTH=0;
    public static int SOUTH=1;
    public static int EAST=2;
    public static int WEST=3;   

    private boolean[] hasDoor=new boolean[4];

    public Room(boolean northDoor,boolean southDoor,boolean eastDoor,boolean westDoor) {
        setHasDoor(northDoor,NORTH);
        setHasDoor(southDoor,SOUTH);
        setHasDoor(eastDoor,EAST);
        setHasDoor(westDoor,WEST);
    }

    public final  void setHasDoor(boolean directionhasDoor, int direction){
        hasDoor[direction]=directionhasDoor;
    }
    public final boolean  getHasDoor(int direction){
        return hasDoor[direction];
    }


}

不阅读文档或方法的任何人都清楚它的作用,而这始终是您首先应该瞄准的目标。

然后可以按如下方式使用

public static void main(String[] args){
    ArrayList<Room> rooms=new ArrayList<Room>();

    rooms.add(new Room(true,false,true,false));
    rooms.add(new Room(true,true,true,false));

    System.out.println("Room 0 has door on north side:"+rooms.get(0).getHasDoor(NORTH));

}

或者按照您的平面图放置在二维数组中

public static void main(String[] args){
    Room[][] rooms=new  Room[10][10]; 

    rooms[0][0]=new Room(true,false,true,false);
    rooms[0][1]=new Room(true,false,true,false);
    //........
    //........
    //other rooms
    //........
    //........

    System.out.println("Room 0,0 has door on north side:"+rooms[0][0].getHasDoor(NORTH));

}

最重要的问题是您是否关心以清晰的代码和可能的执行速度为代价节省几千字节?在内存不足的情况下,您可能会这样做,否则您不会。

【讨论】:

  • 在这种情况下,可读性优先于几千字节。谢谢!
  • 传递 boolean 参数远非可读。可能更好地传递enum 值的数组,例如new Room({ NORTH, SOUTH }); 或其他东西。
  • @TC1 当然是这样,这样会更好。 但是我经常发现枚举是一个高级主题,并且不想通过尝试获得最后 10% 来更好地混淆 90%。我在代码中包含了“考虑使用 ENUM 作为奖励积分”作为提示,但这就是我真正想去的地方
  • @TC1 也就是说,OldCurmudgeon 的答案是基于枚举的方法的一个很好的例子,我很高兴它在这里作为一个竞争答案
【解决方案3】:

你肯定过度设计了这个。例如,它可以通过字符解决,并使用char[4] 数组来存储(最多)4 个不同的字符nswe

您可以使用String 收集有关门的信息,每个char 将是一扇门(然后您可以使用"nnse" 之类的东西,这意味着北墙上有两扇门,一扇南门和一扇东门)。

您还可以使用字符的ArrayList,并且可以根据需要将其转换为数组。 取决于你想用这个做什么,但是有很多方法可以实现你想要做的。

记住这一点

过早的优化是万恶之源

【讨论】:

  • 这仍然感觉像是一个过度工程师,一个好的面向对象的解决方案将是最容易使用的
  • 另外,过早的优化 char 类型...在这种情况下,我会使用 enums 和 EnumSets...
  • @RichardTingle 如果他想要一个快速而简单的解决方案,那真的不是。我只是指出了我想到的第一个解决方案。它仍然不是最好和最优化的解决方案,但是:请参阅报价。我已经说过有很多方法可以实现这一点。这是非常简单的事情,我个人认为与其花 2 小时思考如何做到这一点的 最佳 方法,不如轻松地做到这一点,然后检查它是否会导致任何性能问题。如果您正常(正确)使用它(解决方案之一),我对此表示怀疑。
  • @ppeterka66 是的,Enums 也是我的第一个想法,但这里的绅士显然希望尽可能少地使用内存 ;-)
【解决方案4】:

好的,基于 cmets,我决定不再使事情复杂化。我将使用第一个解决方案,其中doors 数组将有 4 个元素,映射函数为:

public Door getDoor(int doorID){
    switch(doorID){
        case NORTH:{
            if(doorExists(NORTH))return doors[0];
            else return null;
        }
        case SOUTH:{
            if(doorExists(NORTH))return doors[1];
            else return null;
        }
        case EAST:{
            if(doorExists(NORTH))return doors[2];
            else return null;
        }
        case WEST:{
            if(doorExists(NORTH))return doors[3];
            else return null;
        }
    }
    return null;
}

我只是认为这是一个很好的理论问题,仅此而已。感谢您的回复!

【讨论】:

  • 门存在检查似乎是多余的;如果你只是简单地返回数组中的相应元素,如果没有门,你会得到 null!
  • NORTH==0EAST==1SOUTH==2WEST==3。 (按此顺序,您可以加/减 1 模 4 转)。然后你可以简单地return[doors[direction]]。在其他地方,如果您需要 2 的幂,只需使用 bitmask &amp; (1 &lt;&lt; direction) 或类似的东西。
  • 但这会在我当前的构造函数中造成一些可读性问题。目前我的构造函数是这样的:door = new Door(Door.NORTH | Door.SOUTH | Door.East),其中 Door.SOUTH(..etc..) 是我想要在那个特定房间里的门方向的常量。
【解决方案5】:

好的,我 100% 同意 Enums 是这里的方法。我建议的唯一一件事是使用二进制 ID 系统,特别是因为这是应用于游戏的逻辑,并且您可能需要将定义房间的信息存储在某处。使用这种系统,您可以存储房间可用门的二进制表示。当您处理最多只能拥有一个的项目时,该系统运行良好。在您的情况下,一个房间的每个方向只能有一扇门。如果将房间中每个门的所有二进制值相加,则可以存储该值,然后将该值转换回正确的枚举。

例子:

public enum Direction {

    NONE (0, 0),
    NORTH (1, 1),
    SOUTH (2, 2),
    EAST (3, 4),
    WEST (4, 8),
    NORTHEAST (5, 16),
    NORTHWEST (6, 32),
    SOUTHEAST (7, 64),
    SOUTHWEST (8, 128),
    UP (9, 256),
    DOWN (10, 512);

    private Integer id;
    private Integer binaryId;

    private Direction(Integer id, Integer binaryId) {
        this.id = id;
        this.binaryId = binaryId;
    }

    public Integer getId() {
        return id;
    }

    public Integer getBinaryId() {
        return binaryId;
    }

    public static List<Direction> getDirectionsByBinaryIdTotal(Integer binaryIdTotal) {
        List<Direction> directions = new ArrayList<Direction>();

        if (binaryIdTotal >= 512) {
            directions.add(Direction.DOWN);
            binaryIdTotal -= 512;
        }
        if (binaryIdTotal >= 256) {
            directions.add(Direction.UP);
            binaryIdTotal -= 256;
        }
        if (binaryIdTotal >= 128) {
            directions.add(Direction.SOUTHWEST);
            binaryIdTotal -= 128;
        }
        if (binaryIdTotal >= 64) {
            directions.add(Direction.SOUTHEAST);
            binaryIdTotal -= 64;
        }
        if (binaryIdTotal >= 32) {
            directions.add(Direction.NORTHWEST);
            binaryIdTotal -= 32;
        }
        if (binaryIdTotal >= 16) {
            directions.add(Direction.NORTHEAST);
            binaryIdTotal -= 16;
        }
        if (binaryIdTotal >= 8) {
            directions.add(Direction.WEST);
            binaryIdTotal -= 8;
        }
        if (binaryIdTotal >= 4) {
            directions.add(Direction.EAST);
            binaryIdTotal -= 4;
        }
        if (binaryIdTotal >= 2) {
            directions.add(Direction.SOUTH);
            binaryIdTotal -= 2;
        }
        if (binaryIdTotal >= 1) {
            directions.add(Direction.NORTH);
            binaryIdTotal -= 1;
        }

        return directions;
    }

}

【讨论】:

    【解决方案6】:

    我什至会将门建模为枚举,以便将来使用其他类型的门。例如:

    public enum Door { TOP, BOTTOM, LEFT, RIGHT };
    
    public class Room {
        private Set<Door> doors = new HashSet<Door>;
        public Room(Door... doors) {
            //Store doors.
            this.doors = doors;
        }
    
        public boolean hasDoor(Door door) {
            return this.doors.contains(door);
        }
    }
    

    【讨论】:

      【解决方案7】:

      你可以这样转换:

      public Door getDoor(int doorID){
      switch(doorID){
          case NORTH:{
              return doors[0];
          }
          case SOUTH:{
              return doors[1];
          }
          case EAST:{
              return doors[2];
          }
          case WEST:{
              return doors[3];
          }
      }
      return null;
      }
      

      到这里:

      public Door getDoor(int doorID){
          int index = 0;
          int id = doorID;
          while(id > 1){
              if(id & 1 == 1)
                  return null;
              id = id >>> 1;
              index++;
          }
      return doors[index];
      }
      

      【讨论】:

      • 如果我们谈论优化,我会说应该避免使用while。毕竟,你可以预先计算一个离散对数数组来写doors[reverse[index]]
      猜你喜欢
      • 1970-01-01
      • 2014-05-24
      • 1970-01-01
      • 2014-03-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多