2009-09-29 21 views
6

A veces tiene sentido tener un diccionario ordenado por teclado. En C++, esto a menudo se implementa con un árbol Rojo-negro. Pero cualquier árbol de búsqueda binaria autoequilibrante servirá (sin embargo, Knuth es particularmente claro en este tema). La mejor solución que he podido encontrar hasta ahora es tomar R. McGraw's AVL-tree type y crear una clase contenedora que básicamente implemente la interfaz de mapa STL (también contando con el práctico orden de pares (dos elementos tuplas) en Python). Tal tupla corresponde básicamente a std :: map :: value_type.Mapping std :: map to Python

Sí, hay un módulo bisectrónico de Python, y aunque eso es logarítmico en el momento de la inserción de la misma manera que los árboles binarios autoequilibrantes son logarítmicos en el momento de la inserción (¿verdad?), Francamente solo quiero un objeto. Llamado OrderedDict o algo así (y no, el Python 3.1 OrderedDict no califica, eso es para pedidos de "tiempo de inserción" y, francamente, lo que el orden de inserción tiene que ver con ordenar no es del todo obvio).

Nota, un diccionario ordenado por clave es muy útil en muchas industrias (en finanzas, por ejemplo, es común hacer un seguimiento de los precios de los libros de datos, que son básicamente diccionarios ordenados de precio - cantidad, información de orden agregada, etc. .).

Si alguien tiene alguna otra idea, es genial. Todo lo que sé es que acabo de obtener cinco millones de veces más inteligente con las "respuestas" de Alex Martelli. Así que pensé en preguntar.

+0

gracias a todos! No tengo suficientes puntos o algo para recomendar a todos, pero encuentro todos los comentarios muy útiles. –

Respuesta

-1

Como usted ha dicho, puede rodar su propia aplicación con bisect:

class OrderedDict: 
    def __init__(self, keyvalues_iter): 
    self.__srtlst__ = sorted(keyvalues_iter) 
    def __len__(self): 
    return len(self.__srtlst__) 
    def __contains__(self, key): 
    index = bisect(self.__srtlst__, key) 
    if index<len(self.__srtlst__) and self.__srtlst__[index][0] == key: 
     return True 
    else: 
     return False  
    def __getitem__(self, key): 
    index = bisect(self.__srtlst__, key) 
    if index<len(self.__srtlst__) and self.__srtlst__[index][0] == key: 
     return self.__srtlst__[index][1] 
    else: 
     raise KeyError(key) 
    def __setitem__(sekf, key, value): 
    index = bisect(self.__srtlst__, key) 
    if index<len(self.__srtlst__) and self.__srtlst__[index][0] == key: 
     self.__srtlst__[index][1] = value 
    else: 
     self.__srtlst__[index]=(key, value) 
    def __delitem__(sekf, key, value): 
    index = bisect(self.__srtlst__, key) 
    if index<len(self.__srtlst__) and self.__srtlst__[index][0] == key: 
     del __srtlst__[index] 
    else: 
     raise KeyError(key) 
    def __iter__(self): 
    return (v for k,v in self.__srtlst__) 
    def clear(self): 
    self.__srtlst__ = [] 
    def get(self, key, default=None): 
    index = bisect(self.__srtlst__, key) 
    if index<len(self.__srtlst__) and self.__srtlst__[index][0] == key: 
     return self.__srtlst__[index][1] 
    else: 
     return default 
    def items(self): 
    return self.__srtlst__[:] 
    def iteritems(self): 
    return iter(self.__srtlst__) 
    def iterkeys(self): 
    return (k for k,v in self.__srtlst__) 
    def itervalues(self): 
    return (v for k,v in self.__srtlst__) 
    def keys(self): 
    return [k for k,v in self.__srtlst__] 
    def values(self): 
    return [v for k,v in self.__srtlst__] 
    def setdefault(self, key, default): 
    index = bisect(self.__srtlst__, key) 
    if index<len(self.__srtlst__) and self.__srtlst__[index][0] == key: 
     return self.__srtlst__[index][1] 
    else: 
     self.__srtlst__[index]=(key, default) 
     return default 
    def update(self, other): 
    #a more efficient implementation could be done merging the sorted lists 
    for k, v in other.iteritems(): 
     self[k] = v 

hmmm ... parece que ya lo hice por ti, D'oh!

+0

La única desventaja de esta implementación, afaik, es esa inserción y la eliminación es potencialmente O (n) (aunque puede encontrar el índice en O (log n), si tiene que mover elementos para dar paso a un nuevo elemento o eliminar un elemento existente, es posible que deba desplazar la lista ordenada subyacente en O (n) tiempo). Versión ligeramente modificada disponible aquí: http://pastebin.com/f1e721bdb (pero aún no hay método de inserción) –

+0

Sí, por supuesto, hacer crecer la lista puede llevar tiempo, pero debe ser amortizada asintóticamente ... sobre el cambio, algunos trucos podrían hacerse para aliviarlo (como por ejemplo marcar posiciones vacías y tener una lista auxiliar más pequeña para las costosas inserciones al principio y fusionar y compactar de vez en cuando), pero al final debe optimizar para sus necesidades: iterar (la lista gana), mutando (gana de árbol) o acceso directo por clave (hashmap gana). – fortran

+3

La "única desventaja" es O (n) modificaciones en lugar de O (log n) u O (1)? Esa no es una desventaja menor, es la pérdida de un beneficio fundamental de un árbol. Si todo lo que necesitas en este momento es lo que puedes obtener de esto, genial, pero este no es un mapa estándar. –

2

Obtuve exactamente la misma necesidad, y la respuesta de Alex Martelli me ha convencido por completo: lo mejor es mantener un diccionario y una lista de claves parcialmente ordenadas, luego ordenar cuando sea necesario. Esto es eficiente debido al comportamiento muy particular del algoritmo de ordenación de pitón (AKA Timsort). Key-ordered dict in Python

me puso a prueba su aplicación y la mía y la suya era mejor (porque no se inserta en el medio de la lista)

(Te aconsejo que lea el documento vinculado en el comentario de AM sobre la timsort , que es una perla).

+0

Gracias. Finalmente leí la publicación completa. Muy útil. Con respecto a Alex Martelli y los comentarios de todos, creo que podría intentar el enfoque py-avl wrapper (como se mencionó primero en su publicación). Es un caso algo inusual: pero básicamente necesito encontrar una subsecuencia (algo así como lower_bound y upper_bound en std :: map). Aunque el timsort parece genial (y particularmente bueno para evitar comparaciones, aunque es más agresivo con el intercambio), algo así es lo que creo que se resuelve de forma más natural con un árbol. O (log n) insert/delete también es bueno ... –

+0

Comentario final, y luego lo dejaré descansar un rato. Pero a fin de aclarar lo que estaba diciendo en lo anterior. No es tanto que quiera encontrar subsecuencias con lower_bound y upper_bound en log (n) time (bisect también puede hacerlo). Es posible que posteriormente quiera insertar un elemento en algún lugar de la subsecuencia (por ejemplo, una colección ordenada de widgets en un diseño, ordenados por distancia de un widget de referencia). Si tiene un iterador en un árbol (o una lista vinculada para ese asunto, pero un límite inferior/superior en una lista vinculada es, por supuesto, O (n)), ya está. –

+0

fwiw, aquí hay una solución que usa esa py-avl (con un pequeño parche: http://pastebin.com/d80a3296): http://pastebin.com/f66ca441c (tenga en cuenta que esta solución es probablemente incorrecta en todos los sentidos que son importante, el método iter_range es un intento de emular low_bound/upper_bound, aunque esta es la primera vez que exploro iteradores personalizados, así que como digo, probablemente sea incorrecto) –

1

Las listas son un miserable sustituto de un árbol.

Las inserciones necesitan mover toda la lista para hacer espacio; las eliminaciones necesitan mover la lista hacia abajo. Agregar o eliminar cosas en lote está bien cuando es posible, pero a menudo no lo es, o toma contorsiones no naturales para organizarlo. Un atributo fundamental de un árbol es que las inserciones y eliminaciones son O (log n); ninguna cantidad de movimiento manual convertirá O (n) en O (log n).

Insertar un elemento en un árbol cuando ya se sabe a dónde irá es O (1). De forma equivalente, eliminar un elemento de un árbol en función de su nodo también es O (1). std :: map es compatible con ambos. Estos son ambos O (n) con una lista.

Otra propiedad fundamental de un árbol es que la iteración sobre un rango de valores es O (1) por iteración. La combinación de lista y dict pierde esto, porque cada iteración necesita hacer una búsqueda de dict. (El enfoque de lista de tuplas no tiene este problema.)

Los árboles se encuentran entre los tipos de datos más básicos. La falta de Python de un tipo de contenedor de árbol es una verruga. Tal vez haya una biblioteca de terceros implementando una (por ejemplo, la vinculada por el Sr. "Desconocido", que no he probado, por lo que no puedo responder), pero no hay un tipo de Python estándar para ello.

+1

Debo admitir que encuentro la falta de una tabla hash (estandarizada) en C++ más problemática en mi trabajo diario que la falta de un contenedor basado en árbol en Python. Pero nunca he encontrado un caso de uso como este en el que la estructura no fuera lo suficientemente pequeña como para ordenarlo. – Kylotan

+1

O (1) inserción del elemento x: 'thelist.append (x)'; O (1) eliminación del índice i: 'thelist [i] = thelist [-1]; dellist [-1] '. Esto perturba el orden, pero para pequeñas perturbaciones, thelist.sort() es O (N) (gracias, timsort! -) y solo necesita realizarse cuando realmente se necesita (por lo que para la mayoría de los patrones de acceso se obtiene un buen rendimiento amortizado). Por lo tanto, como p. La falta de optimización de recursividad de cola de Python, la falta de contenedores de árbol es muy rara vez un problema en la práctica (exploración informal de las fuentes de C++ de Google: std :: hashmap _many_ órdenes de magnitud más frecuentes que std :: map! -). –

+0

Son para resolver diferentes tipos de problemas. De hecho, te refieres a la implementación del vector/matriz cultivable de una lista, no de la lista vinculada. Además, un árbol es solo una lista de listas, y ya las tienes en Python. – fortran

0

Para obtener una lista que permanece ordenada, puede probar el módulo heapq.

+1

no completamente ordenado, un montón simplemente impone ordenar entre niveles de un árbol (padre con hijos) pero no en el mismo nivel (hermanos), por ejemplo en un montón válido de menor a mayor, h [0] fortran

1

Me encontré con esta pregunta que necesitaba un OrderedMap y descubrí con horror que la respuesta aceptada es basura completa. Así Rodé mi cuenta, en caso de que alguien le resulta útil:

from bisect import * 

class OrderedMap: 
    """Much less efficient than a dict, 
    but keys don't need to be hashable.""" 
    __default_arg = object() 
    def __init__(self, keyvalues_iter = None): 
     self.clear() 
     if keyvalues_iter is not None: 
      self.update(keyvalues_iter) 
    def clear(self): 
     self.__keys = [] 
     self.__values = [] 
    def __index(self, key): 
     if self.__keys: 
      index = bisect(self.__keys, key)-1 
      if self.__keys[index] == key: 
       return index 
     raise KeyError(key) 
    def __len__(self): 
     return len(self.__keys) 
    def __contains__(self, key): 
     try: 
      self.__index(key) 
      return True 
     except KeyError: 
      return False 
    def __getitem__(self, key): 
     index = self.__index(key) 
     return self.__values[index] 
    def __setitem__(self, key, value): 
     try: 
      index = self.__index(key) 
      # exists 
      self.__values[index] = value 
     except KeyError: 
      # new 
      index = bisect(self.__keys, key) 
      self.__keys.insert(index, key) 
      self.__values.insert(index, value) 
    def __delitem__(self, key): 
     index = self.__index(key) 
     self.__keys.pop(index) 
     self.__values.pop(index) 
    def __iter__(self): 
     return iter(self.__keys) 
    def get(self, key, default=__default_arg): 
     try: 
      return self[key] 
     except KeyError: 
      if default != OrderedMap.__default_arg: 
       return default 
      raise 
    def setdefault(self, key, default = None): 
     try: 
      return self[key] 
     except KeyError: 
      if default != OrderedMap.__default_arg: 
       self[key] = default 
       return default 
      raise 
    def items(self): 
     return zip(self.__keys, self.__values) 
    def iteritems(self): 
     return iter((self.__keys[x], self.__values[x]) 
        for x in xrange(len(self))) 
    def keys(self): 
     return self.__keys[:] 
    def iterkeys(self): 
     return iter(self.__keys) 
    def values(self): 
     return self.__values[:] 
    def itervalues(self): 
     return iter(self.__values) 
    def update(self, other): 
     for k, v in other.iteritems(): 
      self[k] = v 
    def __repr__(self): 
     s = ", ".join("%s: %s" % (repr(self.__keys[x]), 
            repr(self.__values[x])) 
         for x in xrange(len(self))) 
     return "OrderedMap{%s}" % (s,) 
+0

No estoy seguro de la utilidad de tener un método 'clear()' ya que las instancias son inmutables una vez creadas - a diferencia de la respuesta aceptada. – martineau

0

El módulo de Python sortedcontainers ofrece un tipo de datos SortedDict por exactamente estos fines. Utiliza una estructura de datos de tipo árbol B modificado y está escrito en Python puro. El módulo tiene una cobertura de prueba del 100% y horas de estrés. Aunque Python puro, es más rápido que las implementaciones C y tiene un performance comparison para respaldarlo.

Debido a que es puro en Python, la instalación es una brisa con pip:

pip install sortedcontainers 

Entonces sólo tiene que:

from sortedcontainers import SortedDict 
help(SortedDict) 

El performance comparison incluye una lista bastante completa de implementaciones alternativas. Vale la pena echarle un vistazo.