¿Java tiene un HashMap con búsqueda inversa?

98

Tengo datos que están organizados en una especie de formato de "clave-clave", en lugar de "clave-valor". Es como un HashMap, pero necesitaré una búsqueda O (1) en ambas direcciones. ¿Existe un nombre para este tipo de estructura de datos y se incluye algo como esto en las bibliotecas estándar de Java? (¿O quizás Apache Commons?)

Podría escribir mi propia clase que básicamente usa dos mapas reflejados, pero prefiero no reinventar la rueda (si esto ya existe pero no estoy buscando el término correcto).

Dormir
fuente

Respuestas:

106

No existe tal clase en la API de Java. La clase de Apache Commons que desea será una de las implementaciones de BidiMap .

Como matemático, llamaría biyección a este tipo de estructura.

uckelman
fuente
83
como no matemático, llamaría a este tipo de estructura "un mapa que te permite buscar valores por clave o al revés"
Dónal
4
Lástima que no tenga soporte para genéricos, parece que Guava sí.
Eran Medan
2
github.com/megamattron/collections-generic tiene BidiMap con soporte genérico
Kenston Choi
1
@Don "Bidi" -> "Bi-Directional"
ryvantage
3
@ Dónal Sí, pero todo está basado en matemáticas
Alex
75

Además de Apache Commons, Guava también tiene un BiMap .

ColinD
fuente
¡Gracias por la info! Sin embargo, me quedo con apache por el momento (a menos que haya buenas razones para no hacerlo)
Kip
No puedo ofrecer una buena comparación con las colecciones de apache, pero las colecciones de Google tienen muchas cosas interesantes que creo que valdrían la pena.
ColinD
16
Una ventaja de las colecciones de Google es que tiene genéricos, mientras que las colecciones comunes no.
Mark
3
Para comparar las dos bibliotecas, consulte las citas en esta respuesta: stackoverflow.com/questions/787446/… (y la entrevista original). Eso está sesgado hacia Google, por razones obvias, pero aun así creo que es seguro decir que estás mejor con las Colecciones de Google hoy en día.
Jonik
1
El enlace BiMap está roto. Por favor use este .
Mahsa2
20

Aquí hay una clase simple que usé para hacer esto (no quería tener otra dependencia de terceros). No ofrece todas las funciones disponibles en Maps, pero es un buen comienzo.

    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
fuente
5
Mejorará significativamente el rendimiento de containsValue () cambiándolo para devolver valueToKeyMap.containsKey (value)
JT.
No usaría este mapa como está actualmente porque la bidireccionalidad se rompe si una clave (o valor) se vuelve a agregar con un valor (o clave) diferente, que sería un uso válido para actualizar una clave IMO.
Qw3ry
11

Si no se producen colisiones, siempre puede agregar ambas direcciones al mismo HashMap :-)

rsp
fuente
6
@Kip: ¿Por qué? En algunos contextos, esta es una solución perfectamente legítima. Entonces tendría dos mapas hash.
Lawrence Dol
7
no, es un truco feo y frágil. requiere el mantenimiento de la propiedad bidireccional en cada get () y put (), y podría pasarse a otros métodos que modifiquen el mapa sin siquiera conocer la propiedad bidireccional. tal vez estaría bien como una variable local dentro de un método que no se pasa a ningún lado, o si se hizo no modificable inmediatamente después de la creación. pero incluso entonces, es frágil (alguien viene y modifica esa función y rompe la bidireccionalidad de una manera que no siempre se mostrará de inmediato como un problema)
Kip
1
@Kip, estoy de acuerdo en que dicho uso debe mantenerse interno a la clase que usa ese mapa, pero su último comentario solo es cierto si las pruebas JUnit correspondientes son descuidadas :-)
rsp
Si puedo plantear un uso muy válido de tal implementación, imagine que necesita un mapa para decodificar / codificar códigos de operación de instrucciones en lenguaje ensamblador aquí, nunca cambiará el estado del mapa también, en una dirección las claves son cadenas de instrucciones y el otro valores binarios. Así que nunca deberías tener conflictos.
MK
Para un propósito de búsqueda a pequeña escala, ese truco resuelve mi problema.
milkersarac
5

Aquí mis 2 centavos.

O puede usar un método simple con genéricos. Pedazo de pastel.

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

Por supuesto, debe tener un mapa con valores únicos. De lo contrario, uno de ellos será reemplazado.

Fulvio
fuente
1

Inspirado por la respuesta de GETah, decidí escribir algo similar yo mismo con algunas mejoras:

  • La clase está implementando la Map<K,V>-Interface
  • La bidireccionalidad está realmente garantizada al cuidarla al cambiar un valor por un put(al menos espero garantizarlo por este medio)

El uso es como un mapa normal, para obtener una vista inversa de la llamada de mapeo getReverseView(). El contenido no se copia, solo se devuelve una vista.

No estoy seguro de que esto sea totalmente infalible (en realidad, probablemente no lo sea), así que siéntete libre de comentar si notas algún defecto y actualizaré la respuesta.

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
fuente
tenga en cuenta que al igual que BiMap y BidiMap, esta es una biyección que no permite tener varias claves con el mismo valor. (getReverseView (). get (v) siempre devolverá solo una clave).
Donatello
Es cierto, pero OTOH eso es exactamente lo que pidió el OP
Qw3ry
No estoy seguro de que haya expresado que sus datos coincidan con esta restricción, pero de todos modos, ¡podría ayudar a otra persona a comprender mejor!
Donatello
0

Es una pregunta bastante vieja aquí, pero si alguien más tiene un bloqueo cerebral como yo y se tropieza con esto, espero que esto ayude.

Yo también estaba buscando un HashMap bidireccional, a veces las respuestas más simples son las más útiles.

Si no desea reinventar la rueda y prefiere no agregar otras bibliotecas o proyectos a su proyecto, ¿qué tal una implementación simple de matrices paralelas (o ArrayLists si su diseño lo exige)?

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

Tan pronto como conozca el índice de una de las dos claves, podrá solicitar fácilmente la otra. Entonces, sus métodos de búsqueda podrían verse algo como:

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

Esto es asumiendo que está utilizando estructuras orientadas a objetos adecuadas, donde solo los métodos están modificando estas matrices / ArrayLists, sería muy simple mantenerlas en paralelo. Incluso más fácil para ArrayList, ya que no tendría que reconstruir si cambia el tamaño de las matrices, siempre que agregue / elimine en conjunto.

ThatOneGuy
fuente
3
Está perdiendo una característica importante de HashMaps, a saber, la búsqueda O (1). Una implementación como esta requeriría escanear a través de una de las matrices hasta que encuentre el índice del elemento que está buscando, que es O (n)
Kip
Sí, eso es muy cierto y es un gran inconveniente. Sin embargo, en mi situación personal, en realidad estaba lidiando con la necesidad de una lista de claves tridireccionales y siempre sabría al menos una de las claves con anticipación, por lo que para mí personalmente eso no fue un problema. Sin embargo, gracias por señalar eso, parece que me he saltado ese hecho importante en mi publicación original.
ThatOneGuy