Java ha una HashMap con ricerca inversa?


98

Dispongo di dati organizzati in una sorta di formato "chiave-chiave", piuttosto che "valore-chiave". È come una HashMap, ma avrò bisogno di una ricerca O (1) in entrambe le direzioni. Esiste un nome per questo tipo di struttura dati e qualcosa di simile è incluso nelle librerie standard di Java? (o forse Apache Commons?)

Potrei scrivere la mia classe che fondamentalmente utilizza due mappe speculari, ma preferirei non reinventare la ruota (se esiste già ma non sto cercando il termine giusto).

Risposte:


106

Non esiste una classe di questo tipo nell'API Java. La classe Apache Commons che desideri sarà una delle implementazioni di BidiMap .

Come matematico, definirei questo tipo di struttura una biiezione.


83
come non matematico chiamerei questo tipo di struttura "una mappa che ti consente di cercare i valori per chiave o viceversa"
Dónal

4
Peccato che non abbia supporto per i generici, sembra che Guava lo faccia.
Eran Medan

2
github.com/megamattron/collections-generic ha BidiMap con supporto Generics
Kenston Choi

1
@Don "Bidi" -> "Bi-Directional"
ryvantage

3
@ Dónal Sì, ma l'intero IT è basato sulla matematica
Alex

75

Oltre ad Apache Commons, Guava ha anche una BiMap .


grazie per le informazioni! per il momento sto attaccando con apache (a meno che non ci siano dei buoni motivi per non farlo?)
Kip

Non posso offrire un buon confronto con le raccolte di Apache, ma le raccolte di Google hanno molte cose carine che penso varrebbe la pena cercare.
ColinD

16
Un vantaggio di Google Collections è che ha generici mentre Commons Collections no.
Mark

3
Per confrontare le due librerie, vedere le citazioni in questa risposta: stackoverflow.com/questions/787446/… (e l'intervista originale). Questo è di parte nei confronti di Google, per ovvie ragioni, ma anche così penso che sia sicuro dire che stai meglio con le raccolte di Google al giorno d'oggi.
Jonik

1
Il collegamento BiMap è interrotto. Per favore usa questo .
Mahsa2

20

Ecco una semplice classe che ho usato per ottenere questo risultato (non volevo avere ancora un'altra dipendenza da terze parti). Non offre tutte le funzionalità disponibili in Maps ma è un buon inizio.

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

5
Migliorerai notevolmente le prestazioni di containsValue () modificandolo per restituire valueToKeyMap.containsKey (value)
JT.

Non userei questa mappa come è attualmente perché la bidirezionalità si interrompe se una chiave (o un valore) viene aggiunto di nuovo con un valore (o chiave) diverso, che sarebbe un utilizzo valido per aggiornare un IMO chiave.
Qw3ry

11

Se non si verificano collisioni, puoi sempre aggiungere entrambe le direzioni alla stessa HashMap :-)


6
@Kip: Perché? In alcuni contesti questa è una soluzione perfettamente legittima. Quindi sarebbe avere due mappe hash.
Lawrence Dol

7
no, è un trucco brutto e fragile. richiede il mantenimento della proprietà bidirezionale su ogni get () e put (), e potrebbe essere passato ad altri metodi che modificano la mappa senza nemmeno conoscere la proprietà bidirezionale. forse andrebbe bene come variabile locale all'interno di un metodo che non viene passato da nessuna parte, o se è stata resa immodificabile immediatamente dopo la creazione. ma anche allora, è fragile (qualcuno arriva e modifica quella funzione e rompe la bidirezionalità in un modo che non si rivelerà sempre immediatamente un problema)
Kip

1
@Kip, sono d'accordo che tale utilizzo debba essere mantenuto interno alla classe che utilizza quella mappa, ma la tua ultima osservazione è vera solo se i corrispondenti test JUnit sono sciatti :-)
rsp

Se posso rappresentare un uso molto valido di tale implementazione, immagina di aver bisogno di una mappa per decodificare / codificare gli opcode delle istruzioni in linguaggio assembly qui non cambierai mai lo stato della mappa, in una direzione le chiavi sono stringhe di istruzioni gli altri valori binari. Quindi non dovresti mai avere conflitti.
MK

Per uno scopo di ricerca su piccola scala, quell'hack risolve il mio problema.
Milkersarac

5

Ecco i miei 2 centesimi.

Oppure puoi usare un metodo semplice con i generici. Pezzo di torta.

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

Ovviamente devi avere una mappa con valori univoci. In caso contrario, uno di essi verrà sostituito.


1

Ispirato dalla risposta di GETah ho deciso di scrivere qualcosa di simile da solo con alcuni miglioramenti:

  • La classe sta implementando Map<K,V>-Interface
  • La bidirezionalità è davvero garantita curandone la modifica di un valore con a put(almeno spero di garantirlo qui)

L'utilizzo è proprio come una normale mappa, per ottenere una vista inversa sulla chiamata di mappatura getReverseView(). Il contenuto non viene copiato, viene restituita solo una visualizzazione.

Non sono sicuro che sia totalmente infallibile (in realtà, probabilmente non lo è), quindi sentiti libero di commentare se noti qualche difetto e aggiornerò la risposta.

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

}

notare che proprio come BiMap e BidiMap, questa è una biiezione che non permette di avere più chiavi con lo stesso valore. (getReverseView (). get (v) restituirà sempre una sola chiave).
Donatello

Vero, ma OTOH è esattamente ciò che l'OP ha chiesto
Qw3ry

Non sono sicuro che abbia espresso che i suoi dati corrispondano a questo vincolo, ma comunque potrebbe aiutare qualcun altro a capire meglio!
Donatello

0

Una domanda piuttosto vecchia qui, ma se qualcun altro ha un blocco cerebrale come ho appena fatto e si imbatte in questo, si spera che questo possa aiutare.

Anch'io stavo cercando una HashMap bidirezionale, a volte è la più semplice delle risposte quella più utile.

Se non desideri reinventare la ruota e preferisci non aggiungere altre librerie o progetti al tuo progetto, che ne dici di una semplice implementazione di array paralleli (o ArrayList se il tuo progetto lo richiede).

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

Non appena conosci l'indice di 1 delle due chiavi puoi facilmente richiedere l'altra. Quindi i tuoi metodi di ricerca potrebbero assomigliare a:

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

Questo presuppone che tu stia utilizzando strutture orientate agli oggetti appropriate, dove solo i metodi stanno modificando questi array / ArrayList, sarebbe molto semplice mantenerli paralleli. Ancora più facile per un ArrayList poiché non dovresti ricostruire se la dimensione degli array cambia, a patto che tu aggiunga / rimuovi in ​​tandem.


3
Stai perdendo una caratteristica importante di HashMaps, ovvero la ricerca O (1). Un'implementazione come questa richiederebbe la scansione di uno degli array fino a trovare l'indice dell'elemento che stai cercando, ovvero O (n)
Kip

Sì, è molto vero ed è uno svantaggio piuttosto grande. Tuttavia, nella mia situazione personale, avevo effettivamente a che fare con la necessità di un elenco di chiavi tri-direzionale e avrei sempre saputo almeno 1 delle chiavi in ​​anticipo, quindi per me personalmente non era un problema. Grazie per averlo sottolineato, però, mi sembra di aver saltato questo fatto importante nel mio post originale.
ThatOneGuy
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.