Java a-t-il un HashMap avec recherche inversée?


98

J'ai des données qui sont organisées dans une sorte de format "clé-clé", plutôt que "clé-valeur". C'est comme un HashMap, mais j'aurai besoin d'une recherche O (1) dans les deux sens. Existe-t-il un nom pour ce type de structure de données, et est-ce que quelque chose de similaire est inclus dans les bibliothèques standard de Java? (ou peut-être Apache Commons?)

Je pourrais écrire ma propre classe qui utilise essentiellement deux cartes en miroir, mais je préfère ne pas réinventer la roue (si cela existe déjà mais je ne cherche tout simplement pas le bon terme).

Réponses:


106

Il n'existe pas de classe de ce type dans l'API Java. La classe Apache Commons que vous voulez sera l'une des implémentations de BidiMap .

En tant que mathématicien, j'appellerais ce type de structure une bijection.


83
en tant que non-mathématicien, j'appellerais ce genre de structure "une carte qui vous permet de rechercher des valeurs par clé ou l'inverse"
Dónal

4
Dommage qu'il ne supporte pas les génériques, semble le faire Guava.
Eran Medan

2
github.com/megamattron/collections-generic a BidiMap avec le support Generics
Kenston Choi

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

3
@ Dónal Oui, mais tout l'informatique est basé sur les mathématiques
Alex

76

En plus d'Apache Commons, Guava dispose également d'une BiMap .


Merci pour l'info! Je reste avec Apache pour le moment (sauf s'il y a de bonnes raisons de ne pas le faire?)
Kip

Je ne peux pas offrir une bonne comparaison avec les collections Apache, mais les collections Google contiennent beaucoup de choses intéressantes qui, à mon avis, mériteraient d'être examinées.
ColinD

16
Un avantage de Google Collections est qu'il contient des génériques, contrairement à Commons Collections.
Mark

3
Pour la comparaison des deux bibliothèques, voir les citations dans cette réponse: stackoverflow.com/questions/787446/... (et l'interview originale). C'est biaisé en faveur de Google, pour des raisons évidentes, mais même ainsi, je pense qu'il est prudent de dire que vous êtes mieux avec Google Collections de nos jours.
Jonik

1
Le lien BiMap est rompu. Veuillez utiliser celui-ci .
Mahsa2

20

Voici une classe simple que j'ai utilisée pour faire cela (je ne voulais pas avoir encore une autre dépendance tierce). Il n'offre pas toutes les fonctionnalités disponibles dans Maps mais c'est un bon début.

    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
Vous améliorerez considérablement les performances de containsValue () en le modifiant pour renvoyer valueToKeyMap.containsKey (value)
JT.

Je n'utiliserais pas cette carte telle qu'elle est actuellement car la bidirectionnalité se rompt si une clé (ou une valeur) est ré-ajoutée avec une valeur (ou clé) différente, ce qui serait un usage valide pour mettre à jour une clé IMO.
Qw3ry

11

Si aucune collision ne se produit, vous pouvez toujours ajouter les deux directions au même HashMap :-)


6
@Kip: Pourquoi? Dans certains contextes, c'est une solution parfaitement légitime. Ce serait donc avoir deux cartes de hachage.
Lawrence Dol

7
non, c'est un hack affreux et fragile. il nécessite la maintenance de la propriété bidirectionnelle sur chaque get () et put (), et il pourrait être passé à d'autres méthodes qui modifient la carte sans même connaître la propriété bidirectionnelle. peut-être que ce serait acceptable en tant que variable locale dans une méthode qui n'est passée nulle part, ou si elle a été rendue non modifiable immédiatement après sa création. mais même dans ce cas, c'est fragile (quelqu'un arrive et peaufine cette fonction et rompt la bidirectionnalité d'une manière qui ne se montrera pas toujours immédiatement un problème)
Kip

1
@Kip, je suis d'accord qu'une telle utilisation devrait être maintenue interne à la classe utilisant cette carte, mais votre dernière remarque n'est vraie que si les tests JUnit correspondants sont bâclés :-)
rsp

Si je peux poser une utilisation très valide d'une telle implémentation, imaginez avoir besoin d'une carte pour le décodage / encodage des opcodes des instructions en langage d'assemblage ici, vous ne changerez jamais l'état de la carte également, dans un sens les clés sont des chaînes d'instructions les autres valeurs binaires. Vous ne devriez donc jamais avoir de conflits.
MK

Dans un but de recherche à petite échelle, ce hack résout mon problème.
milkersarac

5

Voici mes 2 cents.

Ou vous pouvez utiliser une méthode simple avec des génériques. Part de gâteau.

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

Bien sûr, vous devez avoir une carte avec des valeurs uniques. Sinon, l'un d'entre eux sera remplacé.


1

Inspiré par la réponse de GETah, j'ai décidé d'écrire quelque chose de similaire par moi-même avec quelques améliorations:

  • La classe implémente Map<K,V>-Interface
  • La bidirectionnalité est vraiment garantie en en prenant soin lors du changement d'une valeur par un put(du moins j'espère le garantir par la présente)

L'utilisation est comme une carte normale, pour obtenir une vue inversée de l'appel de cartographie getReverseView(). Le contenu n'est pas copié, seule une vue est renvoyée.

Je ne suis pas sûr que ce soit totalement infaillible (en fait, ce n'est probablement pas le cas), alors n'hésitez pas à commenter si vous remarquez des défauts et je mettrai à jour la réponse.

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

}

notez que tout comme BiMap et BidiMap, il s'agit d'une bijection qui ne permet pas d'avoir plusieurs clés de même valeur. (getReverseView (). get (v) ne retournera toujours qu'une seule clé).
Donatello le

C'est vrai, mais OTOH c'est exactement ce que l'OP a demandé
Qw3ry

Je ne suis pas sûr qu'il ait exprimé que ses données correspondent à cette contrainte, mais de toute façon, cela pourrait aider quelqu'un d'autre à mieux comprendre!
Donatello le

0

Une question assez ancienne ici, mais si quelqu'un d'autre a un bloc cérébral comme je viens de le faire et tombe dessus, j'espère que cela aidera.

Moi aussi je cherchais un HashMap bidirectionnel, parfois c'est la plus simple des réponses qui est la plus utile.

Si vous ne souhaitez pas réinventer la roue et préférez ne pas ajouter d'autres bibliothèques ou projets à votre projet, que diriez-vous d'une simple implémentation de tableaux parallèles (ou ArrayLists si votre conception l'exige).

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

Dès que vous connaissez l'index de l'une des deux clés, vous pouvez facilement demander l'autre. Ainsi, vos méthodes de recherche pourraient ressembler à quelque chose comme:

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

Cela suppose que vous utilisez des structures orientées objet appropriées, où seules les méthodes modifient ces tableaux / ArrayLists, il serait très simple de les garder parallèles. Encore plus facile pour une ArrayList puisque vous n'auriez pas à reconstruire si la taille des tableaux change, tant que vous ajoutez / supprimez en tandem.


3
Vous perdez une fonctionnalité importante de HashMaps, à savoir la recherche O (1). Une implémentation comme celle-ci nécessiterait de parcourir l'un des tableaux jusqu'à ce que vous trouviez l'index de l'élément que vous recherchez, qui est O (n)
Kip le

Oui, c'est très vrai et c'est un inconvénient assez important. Cependant, dans ma situation personnelle, je faisais face à un besoin d'une liste de clés tri-directionnelle et je saurais toujours au moins une des clés à l'avance, donc pour moi personnellement, ce n'était pas un problème. Merci d'avoir souligné cela, il me semble avoir ignoré ce fait important dans mon message d'origine.
ThatOneGuy
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.