¿Cómo implementar una tabla hash bidireccional eficiente?

82

Python dictes una estructura de datos muy útil:

d = {'a': 1, 'b': 2}

d['a'] # get 1

A veces también le gustaría indexar por valores.

d[1] # get 'a'

¿Cuál es la forma más eficiente de implementar esta estructura de datos? ¿Alguna forma oficial que recomiende hacerlo?

Juanjo Conti
fuente
Si lo prefiere, podemos asumir que los valores son inmutables al igual que las claves.
Juanjo Conti
3
¿Qué devolverías por este dict: {'a': 1, 'b': 2, 'A': 1}
PaulMcG
2
@PaulMcGuire: Volvería {1: ['a', 'A'], 2: 'b'}. Vea mi respuesta para saber cómo hacerlo.
Basj
4
Nota para el moderador: esto no es un duplicado de stackoverflow.com/questions/1456373/two-way-reverse-map . Este último tiene 1) una redacción muy vaga 2) no MCVE 3) solo se ocupa del caso del mapa biyectivo (ver el primer comentario en esta pregunta), que es mucho más restrictivo que esta pregunta real, que es más general. Así que creo que marcarlo como duplicado es, en este caso particular, engañoso. Si realmente uno debería ser un duplicado de otro, debería ser lo contrario, ya que este aquí cubre el caso general, mientras que el otro (ver respuestas) no cubre el caso no biyectivo.
Basj

Respuestas:

65

Aquí hay una clase para un bidireccional dict, inspirada en Finding key from value en el diccionario Python y modificada para permitir los siguientes 2) y 3).

Tenga en cuenta que :

  • 1) El directorio inverso se bd.inverse actualiza automáticamente cuando bdse modifica el diccionario estándar .
  • 2) El directorio inverso bd.inverse[value] es siempre una lista de keytal que bd[key] == value.
  • 3) A diferencia del bidictmódulo de https://pypi.python.org/pypi/bidict , aquí podemos tener 2 claves con el mismo valor, esto es muy importante .

Código:

class bidict(dict):
    def __init__(self, *args, **kwargs):
        super(bidict, self).__init__(*args, **kwargs)
        self.inverse = {}
        for key, value in self.items():
            self.inverse.setdefault(value,[]).append(key) 

    def __setitem__(self, key, value):
        if key in self:
            self.inverse[self[key]].remove(key) 
        super(bidict, self).__setitem__(key, value)
        self.inverse.setdefault(value,[]).append(key)        

    def __delitem__(self, key):
        self.inverse.setdefault(self[key],[]).remove(key)
        if self[key] in self.inverse and not self.inverse[self[key]]: 
            del self.inverse[self[key]]
        super(bidict, self).__delitem__(key)

Ejemplo de uso:

bd = bidict({'a': 1, 'b': 2})  
print(bd)                     # {'a': 1, 'b': 2}                 
print(bd.inverse)             # {1: ['a'], 2: ['b']}
bd['c'] = 1                   # Now two keys have the same value (= 1)
print(bd)                     # {'a': 1, 'c': 1, 'b': 2}
print(bd.inverse)             # {1: ['a', 'c'], 2: ['b']}
del bd['c']
print(bd)                     # {'a': 1, 'b': 2}
print(bd.inverse)             # {1: ['a'], 2: ['b']}
del bd['a']
print(bd)                     # {'b': 2}
print(bd.inverse)             # {2: ['b']}
bd['b'] = 3
print(bd)                     # {'b': 3}
print(bd.inverse)             # {2: [], 3: ['b']}
Basj
fuente
2
¡Solución muy ordenada del caso ambiguo!
Tobias Kienzler
2
Creo que esta estructura de datos es muy útil en muchos problemas prácticos.
0xc0de
5
Esto es fenomenal. Es sucinto; es autodocumentado; es razonablemente eficiente; simplemente funciona. Mi única objeción sería optimizar las búsquedas repetidas de self[key]in __delitem__()con una sola value = self[key]asignación reutilizada para tales búsquedas. Pero ... si. Eso es insignificante. ¡Gracias por lo increíble, Basj !
Cecil Curry
1
¿Qué tal una versión de Python 3?
zelusp
1
Me gusta esta respuesta para el ejemplo. La respuesta aceptada es correcta aún y creo que la respuesta aceptada debería permanecer como la respuesta aceptada, pero esto es un poco más explícito para definirlo usted mismo, simplemente porque establece claramente que para revertir el diccionario debe colocar el reverso valores en una lista, ya que no puede haber una asignación de uno a uno porque un diccionario tiene una relación de uno a varios con los valores clave.
motor
41

Puede usar el mismo dictado agregando clave, par de valores en orden inverso.

d = {'a': 1, 'b': 2}
revd = dict ([invertido (i) para i en d.items ()])
d.update (revd)
Emil
fuente
5
+1 Una solución agradable y práctica. Otra forma de escribir que: d.update( dict((d[k], k) for k in d) ).
FMc
4
+1 Para un uso ordenado de invertido (). Estoy indeciso si es más legible que el explícito dict((v, k) for (k, v) in d.items()). En cualquier caso, puede pasar directamente a pares .Update: d.update(reversed(i) for i in d.items()).
Beni Cherniavsky-Paskin
22
Tenga en cuenta esto falla por ejemplo, parad={'a':1, 'b':2, 1: 'b'}
Tobias Kienzler
3
Ligera modificación: dict(map(reversed, a_dict.items())).
0xc0de
13
Agregar asignaciones inversas al diccionario original es una idea terrible. Como demuestran los comentarios anteriores, hacerlo no es seguro en el caso general. Solo mantenga dos diccionarios separados. d.update(revd)Sin embargo, dado que las dos primeras líneas de esta respuesta ignorando el final son geniales, todavía estoy contemplando un voto a favor. Pensemos un poco en esto.
Cecil Curry
34

Una tabla hash bidireccional de un hombre pobre sería usar solo dos diccionarios (estas ya son estructuras de datos altamente ajustadas).

También hay un paquete bidict en el índice:

La fuente de bidict se puede encontrar en github:

miku
fuente
1
2 dictados requieren inserciones y eliminaciones dobles.
Juanjo Conti
12
@Juanjo: casi cualquier tabla hash bidireccional / reversible implicará "inserciones y eliminaciones dobles", ya sea como parte de la implementación de la estructura o como parte de su uso. Mantener dos índices es realmente la única forma rápida de hacerlo, AFAIK.
Walter Mundt
7
Por supuesto; Quise decir que cuidar el índice 2 a mano es el problema.
Juanjo Conti
1
@Basj Creo que es correcto que no se acepte, ya que tener más de un valor significa que ya no es una biyección y es ambiguo para la búsqueda inversa.
user193130
1
@Basj Bueno, puedo entender que habría casos de uso que serían útiles para tener más de un valor por clave, por lo que tal vez este tipo de estructura de datos debería existir como una subclase de bidict. Sin embargo, dado que un dict normal se asigna a un solo objeto, creo que tiene mucho más sentido que lo contrario también sea igual. (Solo para aclarar, aunque el valor también puede ser una colección, quise decir que la clave del primer dict debe ser del mismo tipo que el valor del dict inverso)
user193130
4

El siguiente fragmento de código implementa un mapa invertible (biyectiva):

class BijectionError(Exception):
    """Must set a unique value in a BijectiveMap."""

    def __init__(self, value):
        self.value = value
        msg = 'The value "{}" is already in the mapping.'
        super().__init__(msg.format(value))


class BijectiveMap(dict):
    """Invertible map."""

    def __init__(self, inverse=None):
        if inverse is None:
            inverse = self.__class__(inverse=self)
        self.inverse = inverse

    def __setitem__(self, key, value):
        if value in self.inverse:
            raise BijectionError(value)

        self.inverse._set_item(value, key)
        self._set_item(key, value)

    def __delitem__(self, key):
        self.inverse._del_item(self[key])
        self._del_item(key)

    def _del_item(self, key):
        super().__delitem__(key)

    def _set_item(self, key, value):
        super().__setitem__(key, value)

La ventaja de esta implementación es que el inverseatributo de a BijectiveMapes nuevamente a BijectiveMap. Por lo tanto, puede hacer cosas como:

>>> foo = BijectiveMap()
>>> foo['steve'] = 42
>>> foo.inverse
{42: 'steve'}
>>> foo.inverse.inverse
{'steve': 42}
>>> foo.inverse.inverse is foo
True
jme
fuente
1

Algo como esto, tal vez:

import itertools

class BidirDict(dict):
    def __init__(self, iterable=(), **kwargs):
        self.update(iterable, **kwargs)
    def update(self, iterable=(), **kwargs):
        if hasattr(iterable, 'iteritems'):
            iterable = iterable.iteritems()
        for (key, value) in itertools.chain(iterable, kwargs.iteritems()):
            self[key] = value
    def __setitem__(self, key, value):
        if key in self:
            del self[key]
        if value in self:
            del self[value]
        dict.__setitem__(self, key, value)
        dict.__setitem__(self, value, key)
    def __delitem__(self, key):
        value = self[key]
        dict.__delitem__(self, key)
        dict.__delitem__(self, value)
    def __repr__(self):
        return '%s(%s)' % (type(self).__name__, dict.__repr__(self))

Tienes que decidir qué quieres que suceda si más de una clave tiene un valor determinado; la bidireccionalidad de un par dado podría ser fácilmente superada por algún par posterior que inserte. Implementé una opción posible.


Ejemplo:

bd = BidirDict({'a': 'myvalue1', 'b': 'myvalue2', 'c': 'myvalue2'})
print bd['myvalue1']   # a
print bd['myvalue2']   # b        
Matt Anderson
fuente
1
No estoy seguro si esto es un problema, pero usando la implementación anterior, ¿no habría problemas si las claves y los valores se superpusieran? Entonces dict([('a', 'b'), ('b', 'c')]); dict['b']-> en 'c'lugar de la clave 'a'.
tgray
1
No es un problema para el ejemplo del OP, pero podría ser un buen descargo de responsabilidad para incluirlo.
tgray
¿Cómo podemos hacer esas print bd['myvalue2']respuestas b, c(o [b, c], o (b, c), o cualquier otra cosa)?
Basj
0

Primero, debe asegurarse de que la clave para el mapeo de valores sea uno a uno; de lo contrario, no es posible construir un mapa bidireccional.

En segundo lugar, ¿qué tamaño tiene el conjunto de datos? Si no hay muchos datos, solo use 2 mapas separados y actualice ambos cuando actualice. O mejor, use una solución existente como Bidict , que es solo una envoltura de 2 dictados, con actualización / eliminación incorporada.

Pero si el conjunto de datos es grande y no es deseable mantener 2 dictados:

  • Si tanto la clave como el valor son numéricos, considere la posibilidad de utilizar la interpolación para aproximar la asignación. Si la función de mapeo (y su
    función inversa) puede cubrir la gran mayoría de los pares clave-valor , entonces solo necesita registrar los valores atípicos en los mapas.

  • Si la mayor parte del acceso es unidireccional (clave-> valor), entonces está totalmente bien construir el mapa inverso de forma incremental, para cambiar tiempo por
    espacio.

Código:

d = {1: "one", 2: "two" }
reverse = {}

def get_key_by_value(v):
    if v not in reverse:
        for _k, _v in d.items():
           if _v == v:
               reverse[_v] = _k
               break
    return reverse[v]
NeoWang
fuente
0

Desafortunadamente, la respuesta mejor calificada bidictno funciona.

Hay tres opciones:

  1. Dict de subclase : puede crear una subclase de dict, pero tenga cuidado. Usted tiene que escribir implementaciones personalizadas de update, pop, initializer, setdefault. Las dictimplementaciones no llaman __setitem__. Es por eso que la respuesta mejor calificada tiene problemas.

  2. Heredar de UserDict : esto es como un dict, excepto que todas las rutinas están hechas para llamar correctamente. Utiliza un dict debajo del capó, en un elemento llamado data. Puede leer la documentación de Python o usar una implementación simple de una lista direccional que funciona en Python 3 . Perdón por no incluirlo literalmente: no estoy seguro de sus derechos de autor.

  3. Heredar de clases base abstractas : Heredar de collections.abc lo ayudará a obtener todos los protocolos e implementaciones correctos para una nueva clase. Esto es excesivo para un diccionario bidireccional, a menos que también pueda cifrar y almacenar en caché una base de datos.

TL; DR: use esto para su código. Leer Trey Hunner 's artículo para más detalles.

Charles Merriam
fuente