Czy Java ma HashMap z wyszukiwaniem wstecznym?

98

Mam dane zorganizowane w rodzaju „klucz-klucz”, a nie „klucz-wartość”. To jest jak HashMap, ale będę potrzebować wyszukiwania O (1) w obu kierunkach. Czy istnieje nazwa tego typu struktury danych i czy coś takiego jest zawarte w standardowych bibliotekach Javy? (a może Apache Commons?)

Mógłbym napisać własną klasę, która zasadniczo używa dwóch lustrzanych map, ale wolałbym nie wymyślać koła na nowo (jeśli to już istnieje, ale po prostu nie szukam odpowiedniego terminu).

Wyrko
źródło

Odpowiedzi:

106

W Java API nie ma takiej klasy. Klasa Apache Commons, którą chcesz, będzie jedną z implementacji BidiMap .

Jako matematyk nazwałbym ten rodzaj struktury bijecją.

uckelman
źródło
83
jako nie-matematyk nazwałbym ten rodzaj struktury „mapą, która pozwala wyszukiwać wartości według klucza lub na odwrót”
Dónal,
4
Szkoda, że ​​Guava nie obsługuje leków generycznych.
Eran Medan
2
github.com/megamattron/collections-generic ma BidiMap z obsługą Generics
Kenston Choi
1
@Don „Bidi” -> „
Dwukierunkowy
3
@ Dónal Tak, ale całe IT jest oparte na matematyce
Alex
76

Oprócz Apache Commons, Guava ma również BiMap .

ColinD
źródło
dzięki za informację! Jednak na razie trzymam się apache (chyba, że ​​istnieją dobre powody, aby tego nie
robić
Nie mogę zaoferować dobrego porównania do kolekcji Apache, ale kolekcje Google mają wiele fajnych rzeczy, które moim zdaniem sprawiłyby, że warto je obejrzeć.
ColinD
16
Jedną z zalet Google Collections jest to, że ma generics, podczas gdy Commons Collections ich nie ma.
Mark
3
Aby porównać obie biblioteki, zobacz cytaty w tej odpowiedzi: stackoverflow.com/questions/787446/ ... (i oryginalny wywiad). Jest to nastawione na Google z oczywistych powodów, ale mimo to myślę, że można śmiało powiedzieć, że w dzisiejszych czasach lepiej jest z Google Collections.
Jonik
1
Link BiMap jest uszkodzony. Użyj tego .
Mahsa2
20

Oto prosta klasa, której użyłem, aby to zrobić (nie chciałem mieć kolejnej zależności od strony trzeciej). Nie oferuje wszystkich funkcji dostępnych w Mapach, ale to dobry początek.

    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
źródło
5
Znacząco poprawisz wydajność funkcji zawieraValue (), zmieniając ją na zwracającą wartość valueToKeyMap.containsKey (value)
JT.
Nie użyłbym tej mapy tak, jak jest obecnie, ponieważ dwukierunkowość przerywa się, jeśli klucz (lub wartość) jest ponownie dodawany z inną wartością (lub kluczem), co byłoby prawidłowym użyciem do aktualizacji klucza IMO.
Qw3ry,
11

Jeśli nie wystąpią żadne kolizje, zawsze możesz dodać oba kierunki do tej samej HashMap :-)

rsp
źródło
6
@Kip: Dlaczego? W niektórych kontekstach jest to całkowicie uzasadnione rozwiązanie. Więc miałoby dwie mapy mieszania.
Lawrence Dol
7
nie, to brzydki, delikatny hack. wymaga utrzymywania właściwości dwukierunkowej w każdym get () i put () i może być przekazana do innych metod, które modyfikują mapę, nawet nie wiedząc o właściwości dwukierunkowej. może byłaby w porządku jako zmienna lokalna wewnątrz metody, która nie jest nigdzie przekazywana, lub gdyby stała się niemodyfikowalna natychmiast po utworzeniu. ale nawet wtedy jest kruchy (ktoś przychodzi i modyfikuje tę funkcję i łamie dwukierunkowość w sposób, który nie zawsze od razu okaże się problemem)
Kip
1
@Kip, zgadzam się, że takie użycie powinno być utrzymywane wewnątrz klasy używającej tej mapy, ale twoja ostatnia uwaga jest prawdziwa tylko wtedy, gdy odpowiednie testy JUnit są niechlujne :-)
rsp
Jeśli mogę przedstawić bardzo poprawne użycie takiej implementacji, wyobraź sobie, że potrzebuję mapy do dekodowania / kodowania rozkazów instrukcji asemblera tutaj, nigdy nie zmienisz również stanu mapy, w jednym kierunku klucze są ciągami instrukcji, a inne wartości binarne. Więc nigdy nie powinieneś mieć konfliktów.
MK
W przypadku wyszukiwania na małą skalę ten hack rozwiązuje mój problem.
milkersarac
5

Tutaj moje 2 centy.

Lub możesz użyć prostej metody z rodzajami. Bułka z masłem.

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

Oczywiście musisz mieć mapę z unikalnymi wartościami. W przeciwnym razie jeden z nich zostanie wymieniony.

Fulviusa
źródło
1

Zainspirowany odpowiedzią GETah, zdecydowałem się napisać coś podobnego z kilkoma ulepszeniami:

  • Klasa implementuje Map<K,V> -Interface
  • Dwukierunkowość jest naprawdę gwarantowana, dbając o to podczas zmiany wartości o put(przynajmniej mam nadzieję, że to tutaj gwarantuję)

Użycie jest jak normalna mapa, aby uzyskać odwrotny widok wywołania mapowania getReverseView() . Treść nie jest kopiowana, zwracany jest tylko widok.

Nie jestem pewien, czy jest to całkowicie niezawodne (w rzeczywistości prawdopodobnie nie jest), więc nie krępuj się komentować, jeśli zauważysz jakiekolwiek wady, a zaktualizuję odpowiedź.

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
źródło
Zauważ, że tak jak BiMap i BiMap, jest to bijection, który nie pozwala na posiadanie wielu kluczy o tej samej wartości. (getReverseView (). get (v) zawsze zwróci tylko jeden klucz).
Donatello
To prawda, ale OTOH właśnie o to prosił OP
Qw3ry
Nie jestem pewien, czy powiedział, że jego dane pasują do tego ograniczenia, ale tak czy inaczej, mogłoby to pomóc komuś innemu lepiej zrozumieć!
Donatello
0

Dość stare pytanie, ale jeśli ktoś inny ma blokadę mózgu, tak jak ja, i natknie się na to, mam nadzieję, że to pomoże.

Ja też szukałem dwukierunkowej HashMapy, czasami jest to najprostsza z odpowiedzi, która jest najbardziej przydatna.

Jeśli nie chcesz ponownie wymyślać koła i wolisz nie dodawać innych bibliotek lub projektów do swojego projektu, co powiesz na prostą implementację tablic równoległych (lub ArrayLists, jeśli wymaga tego projekt).

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

Gdy tylko poznasz indeks jednego z dwóch kluczy, możesz łatwo zażądać drugiego. Twoje metody wyszukiwania mogą więc wyglądać następująco:

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

Zakłada się, że używasz odpowiednich struktur zorientowanych obiektowo, w których tylko metody modyfikują te tablice / ArrayLists, bardzo łatwo byłoby zachować ich równoległość. Jeszcze łatwiejsze dla ArrayList, ponieważ nie musiałbyś przebudowywać, jeśli rozmiar tablic ulegnie zmianie, o ile dodasz / usuniesz w tandemie.

ThatOneGuy
źródło
3
Tracisz ważną cechę HashMaps, a mianowicie wyszukiwanie O (1). Taka implementacja wymagałaby przeszukania jednej z tablic do momentu znalezienia indeksu szukanego elementu, czyli O (n)
Kip
Tak, to prawda i jest to raczej duża wada. Jednak w mojej sytuacji osobistej miałem do czynienia z potrzebą trójkierunkowego zestawienia kluczy i zawsze z wyprzedzeniem znałem przynajmniej jeden z kluczy, więc dla mnie osobiście nie było to problemem. Dziękuję za zwrócenie na to uwagi, wydaje mi się, że pominąłem ten ważny fakt w moim oryginalnym poście.
ThatOneGuy