Есть ли в Java HashMap с обратным поиском?

98

У меня есть данные, которые организованы по типу «ключ-ключ», а не «ключ-значение». Это похоже на HashMap, но мне понадобится поиск O (1) в обоих направлениях. Есть ли название для этого типа структуры данных и включено ли что-нибудь подобное в стандартные библиотеки Java? (или, может быть, Apache Commons?)

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

Кип
источник

Ответы:

106

В Java API такого класса нет. Нужный вам класс Apache Commons будет одной из реализаций BidiMap .

Как математик я бы назвал такую ​​структуру биекцией.

Uckelman
источник
83
как не математик, я бы назвал такую ​​структуру «картой, которая позволяет вам искать значения по ключу или наоборот»
Дональ
4
Жаль, что в Guava нет поддержки дженериков.
Эран Медан,
2
На github.com/megamattron/collections-generic есть BidiMap с поддержкой Generics
Кенстон Чой,
1
@Don "Bidi" -> "Bi-Directional"
ryvantage
3
@ Dónal: Да, но все ЭТО основано на математике,
Alex
76

В дополнение к Apache Commons у Guava также есть BiMap .

ColinD
источник
Спасибо за информацию! Я пока придерживаюсь apache (если нет веских причин не делать этого?)
Кип
Я не могу предложить хорошее сравнение с коллекциями apache, но в коллекциях Google действительно есть много хороших вещей, которые, как мне кажется, стоили бы взглянуть на них.
ColinD
16
Одним из преимуществ Google Collections является то, что у него есть общие шаблоны, а у Commons Collections нет.
Марк
3
Для сравнения двух библиотек см. Цитаты в этом ответе: stackoverflow.com/questions/787446/… (и исходное интервью). Это предвзятое отношение к Google по очевидным причинам, но даже в этом случае я думаю, что сейчас можно с уверенностью сказать, что вам лучше работать с Google Collections.
Jonik 05
1
Ссылка на BiMap не работает. Пожалуйста, используйте это .
Mahsa2
20

Вот простой класс, который я использовал для этого (я не хотел иметь еще одну стороннюю зависимость). Он не предлагает все функции, доступные в Картах, но это хорошее начало.

    public class BidirectionalMap<KeyType, ValueType>{
        private Map<KeyType, ValueType> keyToValueMap = new ConcurrentHashMap<KeyType, ValueType>();
        private Map<ValueType, KeyType> valueToKeyMap = new ConcurrentHashMap<ValueType, KeyType>();

        synchronized public void put(KeyType key, ValueType value){
            keyToValueMap.put(key, value);
            valueToKeyMap.put(value, key);
        }

        synchronized public ValueType removeByKey(KeyType key){
            ValueType removedValue = keyToValueMap.remove(key);
            valueToKeyMap.remove(removedValue);
            return removedValue;
        }

        synchronized public KeyType removeByValue(ValueType value){
            KeyType removedKey = valueToKeyMap.remove(value);
            keyToValueMap.remove(removedKey);
            return removedKey;
        }

        public boolean containsKey(KeyType key){
            return keyToValueMap.containsKey(key);
        }

        public boolean containsValue(ValueType value){
            return keyToValueMap.containsValue(value);
        }

        public KeyType getKey(ValueType value){
            return valueToKeyMap.get(value);
        }

        public ValueType get(KeyType key){
            return keyToValueMap.get(key);
        }
    }
GETah
источник
5
Вы значительно улучшите производительность containsValue (), изменив его так, чтобы он возвращал valueToKeyMap.containsKey (value)
JT.
Я бы не стал использовать эту карту в нынешнем виде, потому что двунаправленность нарушается, если ключ (или значение) повторно добавляется с другим значением (или ключом), что было бы допустимым использованием для обновления ключа IMO.
Qw3ry
11

Если коллизий не происходит, вы всегда можете добавить оба направления в одну и ту же HashMap :-)

rsp
источник
6
@ Кип: Почему? В некоторых случаях это вполне законное решение. Так было бы иметь две хэш-карты.
Лоуренс Дол
7
нет, это уродливая, хрупкая уловка. он требует поддержания свойства двунаправленности для каждого метода get () и put (), и его можно передать другим методам, которые изменяют карту, даже не зная о свойстве двунаправленности. возможно, это было бы нормально в качестве локальной переменной внутри метода, которая нигде не передается, или если она была сделана неизменной сразу после создания. но даже в этом случае он хрупок (кто-то приходит и настраивает эту функцию и нарушает двунаправленность таким образом, что не всегда сразу проявляется проблема)
Кип
1
@Kip, я согласен, что такое использование должно быть внутренним для класса, использующего эту карту, но ваше последнее замечание справедливо только в том случае, если соответствующие тесты JUnit неаккуратны :-)
rsp
Если я могу представить очень правильное использование такой реализации, представьте, что вам нужна карта для декодирования / кодирования кодов операций инструкций на языке ассемблера, здесь вы также никогда не измените состояние карты, в одном направлении ключи представляют собой строки инструкций, другие двоичные значения. Так что конфликтов быть не должно.
MK
Для небольшого поиска этот хак решает мою проблему.
milkersarac
5

Вот мои 2 цента.

Или вы можете использовать простой метод с дженериками. Кусок пирога.

public static <K,V> Map<V, K> invertMap(Map<K, V> toInvert) {
    Map<V, K> result = new HashMap<V, K>();
    for(K k: toInvert.keySet()){
        result.put(toInvert.get(k), k);
    }
    return result;
}

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

Фульвий
источник
1

Вдохновленный ответом GETah, я решил сам написать нечто подобное с некоторыми улучшениями:

  • Класс реализует Map<K,V>-Interface
  • Двунаправленность действительно гарантируется, если об этом позаботиться при изменении значения на put(по крайней мере, я надеюсь гарантировать это настоящим)

Использование похоже на обычную карту, чтобы получить обратный вид на вызов сопоставления getReverseView(). Контент не копируется, возвращается только представление.

Я не уверен, что это полностью защищено от дурака (на самом деле, вероятно, это не так), поэтому не стесняйтесь комментировать, если вы заметите какие-либо недостатки, и я обновлю ответ.

public class BidirectionalMap<Key, Value> implements Map<Key, Value> {

    private final Map<Key, Value> map;
    private final Map<Value, Key> revMap;

    public BidirectionalMap() {
        this(16, 0.75f);
    }

    public BidirectionalMap(int initialCapacity) {
        this(initialCapacity, 0.75f);
    }

    public BidirectionalMap(int initialCapacity, float loadFactor) {
        this.map = new HashMap<>(initialCapacity, loadFactor);
        this.revMap = new HashMap<>(initialCapacity, loadFactor);
    }

    private BidirectionalMap(Map<Key, Value> map, Map<Value, Key> reverseMap) {
        this.map = map;
        this.revMap = reverseMap;
    }

    @Override
    public void clear() {
        map.clear();
        revMap.clear();
    }

    @Override
    public boolean containsKey(Object key) {
        return map.containsKey(key);
    }

    @Override
    public boolean containsValue(Object value) {
        return revMap.containsKey(value);
    }

    @Override
    public Set<java.util.Map.Entry<Key, Value>> entrySet() {
        return Collections.unmodifiableSet(map.entrySet());
    }

    @Override
    public boolean isEmpty() {
        return map.isEmpty();
    }

    @Override
    public Set<Key> keySet() {
        return Collections.unmodifiableSet(map.keySet());
    }

    @Override
    public void putAll(Map<? extends Key, ? extends Value> m) {
        m.entrySet().forEach(e -> put(e.getKey(), e.getValue()));
    }

    @Override
    public int size() {
        return map.size();
    }

    @Override
    public Collection<Value> values() {
        return Collections.unmodifiableCollection(map.values());
    }

    @Override
    public Value get(Object key) {
        return map.get(key);
    }

    @Override
    public Value put(Key key, Value value) {
        Value v = remove(key);
        getReverseView().remove(value);
        map.put(key, value);
        revMap.put(value, key);
        return v;
    }

    public Map<Value, Key> getReverseView() {
        return new BidirectionalMap<>(revMap, map);
    }

    @Override
    public Value remove(Object key) {
        if (containsKey(key)) {
            Value v = map.remove(key);
            revMap.remove(v);
            return v;
        } else {
            return null;
        }
    }

}
Qw3ry
источник
обратите внимание, что так же, как BiMap и BidiMap, это биекция, которая не позволяет иметь несколько ключей с одинаковым значением. (getReverseView (). get (v) всегда будет возвращать только один ключ).
Донателло
Верно, но OTOH - это именно то, что просил OP
Qw3ry
Я не уверен, что он сказал, что его данные соответствуют этому ограничению, но в любом случае это может помочь кому-то лучше понять!
Донателло
0

Довольно старый вопрос, но если у кого-то еще есть блокировка мозга, как у меня, и он наткнется на это, надеюсь, это поможет.

Я тоже искал двунаправленную HashMap, иногда самые простые ответы оказываются наиболее полезными.

Если вы не хотите изобретать колесо заново и предпочитаете не добавлять в проект другие библиотеки или проекты, как насчет простой реализации параллельных массивов (или ArrayLists, если этого требует ваш дизайн).

SomeType[] keys1 = new SomeType[NUM_PAIRS];
OtherType[] keys2 = new OtherType[NUM_PAIRS];

Как только вы узнаете индекс одного из двух ключей, вы можете легко запросить другой. Итак, ваши методы поиска могут выглядеть примерно так:

SomeType getKey1(OtherType ot);
SomeType getKey1ByIndex(int key2Idx);
OtherType getKey2(SomeType st); 
OtherType getKey2ByIndex(int key2Idx);

Это предполагает, что вы используете правильные объектно-ориентированные структуры, где только методы изменяют эти массивы / ArrayLists, было бы очень просто сохранить их параллельность. Еще проще для ArrayList, так как вам не придется перестраивать, если размер массивов изменится, если вы добавляете / удаляете в тандеме.

ThatOneGuy
источник
3
Вы теряете важную функцию HashMaps, а именно поиск O (1). Подобная реализация потребует сканирования одного из массивов до тех пор, пока вы не найдете индекс искомого элемента, то есть O (n)
Кип
Да, это правда, и это один довольно большой недостаток. Однако в моей личной ситуации я на самом деле имел дело с необходимостью составления трехстороннего списка ключей, и я всегда знал хотя бы один из ключей заранее, поэтому лично для меня это не было проблемой. Спасибо, что указали на это, но я, кажется, пропустил этот важный факт в своем исходном посте.
ThatOneGuy 03