Является ли хеширование подходящим решением? Я слишком усложняю?

Я пишу 2D платформер, где мне нужны комнаты с (максимум 4) дверями. Я пишу это на Java, но язык не имеет значения.

Каждая комната может иметь 4 двери, сверху, снизу и по бокам. Я называю их NORTH, SOUTH, EAST и WEST. Когда я строю комнату, я даю ей только целое число, где каждый бит целого числа представляет дверь.

Например, если мне нужна комната с 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;
}

Что тоже, кажется, работает нормально, но я чувствую, что есть более простое решение этой проблемы или решение с менее расточительными хеш-таблицами. Я чувствую, что это не так асимптотически гибко, как должно быть, или я слишком усложняю вещи. Что было бы лучшим методом?

Спасибо за ваше время!


person David Tóth    schedule 08.10.2013    source источник
comment
Комната без Двери — это не комната, поэтому 0000 — недопустимое значение.   -  person Grim    schedule 08.10.2013
comment
Таким образом, массив может тратить место на неиспользуемые элементы - если у вас нет миллионов комнат, об этом не стоит беспокоиться. В вашем массиве будут храниться ссылки на объекты Door, а не сами объекты (т. е. указатель в терминах C++), поэтому размер класса Door не имеет значения для нулевого значения без двери.   -  person Rup    schedule 08.10.2013
comment
Почему ты так поступаешь, в этом есть ощущение безумия. Что не так с 4 логическими значениями?   -  person Richard Tingle    schedule 08.10.2013
comment
В дополнение к комментарию Rups: вы пытаетесь оптимизировать возможность потери места в классе Door, и это привело к этому вопросу. Это может указывать на то, что вы слишком много думаете об этом - преждевременная оптимизация, корень всех зол, бла-бла-бла.   -  person simont    schedule 08.10.2013
comment
@PeterRader: Нет, если вы используете телепортацию :-)   -  person Gyro Gearless    schedule 08.10.2013
comment
Спасибо, это немного избавило меня от беспокойства, но проблема, я думаю, все еще актуальна. Мне просто любопытно, есть ли оптимальное решение для этого.   -  person David Tóth    schedule 08.10.2013
comment
Я думаю, что разумным решением для этого было бы: класс Room содержит 4 логических значения для дверей в качестве элементов. Все хранится в какой-то расширяемой коллекции (например, классы комнат, хранящиеся в ArrayList‹Room›), если вы заранее не знаете, сколько комнат у вас будет   -  person Richard Tingle    schedule 08.10.2013
comment
Так что хеширование может быть слишком сложным, да? У позднего кодирования есть свои чудеса.   -  person David Tóth    schedule 08.10.2013


Ответы (7)


Перечисления — ваш друг:

// 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, который чрезвычайно эффективен.

person OldCurmudgeon    schedule 08.10.2013
comment
Намного лучше, чем принятый ответ, потому что он расширяемый. - person Michael Pankov; 08.10.2013
comment
Суть в том, что реализация должна быть скрыта за подходящим интерфейсом, если только мы не делаем что-то до смешного переоптимизированное, возможно, какую-нибудь мобильную игру в реальном времени с 5-мерной стрельбой для миллионов пользователей... - person Sergey Orshanskiy; 11.10.2013

Ваше решение, вероятно, является наиболее эффективным по пространству, однако, вероятно, будет неэффективным по времени и, безусловно, наименее понятным для написания.

Простой объектно-ориентированный подход состоял бы в том, чтобы иметь класс 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));

}

Или в 2D-массиве, соответствующем вашему плану этажа.

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));

}

Весь важный вопрос заключается в том, заботитесь ли вы об экономии нескольких килобайт за счет чистого кода и, возможно, скорости выполнения? В ситуации нехватки памяти вы, вероятно, делаете, в противном случае вы этого не сделаете.

person Richard Tingle    schedule 08.10.2013
comment
В этой ситуации удобочитаемость имеет приоритет над несколькими килобайтами. Благодарю вас! - person David Tóth; 08.10.2013
comment
Однако передача аргументов boolean далеко не читабельна. Может быть, лучше передать массив значений enum, таких как new Room({ NORTH, SOUTH }); или что-то в этом роде. - person TC1; 08.10.2013
comment
@TC1 Конечно, так было бы лучше. Однако я часто нахожу перечисления сложной темой и не хочу путать 90% лучше, пытаясь получить последние 10%. Я включил ENUM для бонусных баллов в код в качестве подсказки, но это то, на что я действительно хочу пойти. - person Richard Tingle; 08.10.2013
comment
@ TC1 Тем не менее, ответ OldCurmudgeon является хорошим примером подхода, основанного на перечислении, и я рад, что он здесь как конкурирующий ответ. - person Richard Tingle; 08.10.2013

Вы определенно переусердствуете с этим. Это может быть разрешено, например, с помощью символов и использования массива char[4] для хранения (максимум) 4 разных символов n, s, w и e.

Вы можете использовать String для сбора информации о дверях, и каждая char будет одной дверью (тогда у вас может быть что-то вроде "nnse", означающее, что есть 2 двери на северной стене, 1 южная дверь и одна восточная дверь).

Вы также можете использовать ArrayList символов и преобразовать их в массив, если хотите. Зависит от того, что вы хотите с этим делать, но есть много способов добиться того, что вы пытаетесь сделать.

И помни, что

Преждевременная оптимизация — корень всех зол

person Eel Lee    schedule 08.10.2013
comment
Это все еще похоже на инженера, с хорошим объектно-ориентированным решением было бы проще всего работать. - person Richard Tingle; 08.10.2013
comment
Кроме того, преждевременная оптимизация с жонглированием типами символов... В такой ситуации я бы использовал перечисления и EnumSets... - person ppeterka; 08.10.2013
comment
@RichardTingle Не совсем, если он хочет быстрого и простого решения. Я просто указал первое решение, которое пришло мне в голову. Это все еще не самое лучшее и оптимизированное решение, но: см. цитату. И я уже говорил, что есть много способов добиться этого. Это действительно простая вещь, и лично я считаю, что вместо того, чтобы два часа думать о том, как это лучше сделать, просто сделайте это легко, а затем проверьте, не вызовет ли это каких-либо проблем с производительностью. И я сомневаюсь, если вы используете его (одно из решений) нормально (правильно). - person Eel Lee; 08.10.2013
comment
@ppeterka66 Да, Enums тоже был моей первой мыслью, но джентльмен явно хочет использовать как можно меньше памяти ;-) - person Eel Lee; 08.10.2013

Хорошо, основываясь на комментариях, я решил не усложнять ситуацию дальше. Я буду использовать первое решение, в котором массив 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;
}

Я просто подумал, что это хороший теоретический вопрос, вот и все. Спасибо за ответы!

person David Tóth    schedule 08.10.2013
comment
Проверка существования двери кажется излишней; если вы просто вернете соответствующий элемент в массиве, вы получите нуль, если двери не было! - person Ernest Friedman-Hill; 10.10.2013
comment
Просто позвольте NORTH==0, EAST==1, SOUTH==2, WEST==3. (В этом порядке, так что вы можете добавить/вычесть 1 по модулю 4, чтобы повернуть). Тогда вы можете просто return[doors[direction]]. В других местах, где вам нужна степень двойки, просто используйте bitmask & (1 << direction) или что-то в этом роде. - person Sergey Orshanskiy; 11.10.2013
comment
но это стоило бы некоторых проблем с читабельностью в моем текущем конструкторе. В настоящее время мой конструктор выглядит следующим образом: door = new Door(Door.NORTH | Door.SOUTH | Door.East), где Door.SOUTH(..etc..) — это константы для направления дверей, которые я хочу видеть в этой конкретной комнате. - person David Tóth; 11.10.2013

Хорошо, я согласен на 100%, что Enums — это то, что нужно. Единственное, что я бы посоветовал, тем более, что это логика, применяемая к игре, и вам, вероятно, потребуется где-то хранить информацию, определяющую комнату, — это использовать систему двоичных идентификаторов. Используя такую ​​систему, вы можете хранить бинарное представление того, какие двери доступны в комнате. Эта система хорошо работает, когда вы имеете дело с предметами, где у вас может быть максимум один предмет. В вашем случае в комнате может быть только 1 дверь для каждого направления. Если вы суммируете все двоичные значения для каждой двери в комнате, вы можете сохранить это значение, а затем преобразовать это значение обратно в соответствующие перечисления.

Пример:

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;
    }

}
person Joe    schedule 10.10.2013

Я бы даже смоделировал двери в виде перечисления, чтобы в будущем можно было использовать другие типы дверей. Например:

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);
    }
}
person Tobias    schedule 08.10.2013

Вы можете преобразовать это:

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];
}
person Red Alert    schedule 08.10.2013
comment
Если говорить об оптимизации, то я бы сказал, что while следует избегать. В конце концов, вы можете предварительно вычислить массив дискретных логарифмов, чтобы записать doors[reverse[index]]. - person Sergey Orshanskiy; 11.10.2013