La respuesta de Joe es extremadamente buena y le brinda todas las palabras clave importantes.
Debe tener en cuenta que la investigación sucinta de la estructura de datos aún se encuentra en una etapa temprana, y muchos de los resultados son en gran medida teóricos. Muchas de las estructuras de datos propuestas son bastante complejas de implementar, pero la mayor parte de la complejidad se debe al hecho de que necesita mantener la complejidad asintótica tanto en el tamaño del universo como en la cantidad de elementos almacenados. Si cualquiera de estos es relativamente constante, entonces gran parte de la complejidad desaparece.
Si la colección es semiestática (es decir, las inserciones son raras, o al menos de bajo volumen), entonces vale la pena considerar una estructura de datos estáticos fácil de implementar (el sdarray de Sadakane es una buena opción) junto con una actualización cache. Básicamente, registra las actualizaciones en una estructura de datos tradicional (por ejemplo, B-tree, trie, tabla hash) y actualiza periódicamente la estructura de datos "principal". Esta es una técnica muy popular en la recuperación de información, ya que los índices invertidos tienen muchas ventajas para la búsqueda, pero son difíciles de actualizar en el lugar. Si este es el caso, hágamelo saber en un comentario y enmendaré esta respuesta para darle algunos consejos.
Si las inserciones son más frecuentes, entonces sugiero un hash sucinto. La idea básica es lo suficientemente sencilla como para explicar aquí, así que lo haré.
Entonces, el resultado teórico de la información básica es que si está almacenando elementos de un universo de elementos , y no hay otra información (por ejemplo, ninguna correlación entre los elementos), entonces necesita bits para almacenarlo. (Todos los logaritmos son de base 2 a menos que se especifique lo contrario). Necesita esta cantidad de bits. No hay manera de evitarlo. log ( unortetuIniciar sesión( unorte) +O(1)
Ahora algo de terminología:
- Si tiene una estructura de datos que puede almacenar los datos y respaldar sus operaciones en bits de espacio, llamaremos a esto una estructura de datos implícita .Iniciar sesión( unorte) +O(1)
- Si tiene una estructura de datos que puede almacenar los datos y respaldar sus operaciones en bits de espacio, llamamos a esto una estructura de datos compacta . Tenga en cuenta que en la práctica esto significa que la sobrecarga relativa (relativa al mínimo teórico) está dentro de una constante. Podría ser un 5% de gastos generales, o un 10% de gastos generales, o 10 veces de gastos generales.Iniciar sesión( unorte) +O(log( unorte) )=(1+O(1))log( unorte)
- Si tiene una estructura de datos que puede almacenar los datos y respaldar sus operaciones en bits de espacio, llamamos a esto una estructura de datos sucinta .Iniciar sesión( unorte) +o(log( unorte) )=(1+o(1))log( unorte)
La diferencia entre sucinto y compacto es la diferencia entre little-oh y big-oh. Ignorando el valor absoluto por un momento ...
- c n 0 n > n 0 g ( n ) < c ⋅ f ( n )sol( n ) = O ( f( n ) ) significa que existe una constante y un número tal que para todos , .Cnorte0 0n > n0 0sol( n ) < c ⋅ f( n )
- c n 0 n > n 0 g ( n ) < c ⋅ f ( n )sol( n ) = o ( f( n ) ) significa que para todas las constantes existe un número tal que para todas , .Cnorte0 0n > n0 0sol( n ) < c ⋅ f( n )
Informalmente, big-oh y little-oh están "dentro de un factor constante", pero con big-oh la constante es elegida por usted (por el diseñador del algoritmo, el fabricante de la CPU, las leyes de la física o lo que sea), pero con -oh, tú eliges la constante y puede ser tan pequeña como quieras . En otras palabras, con estructuras de datos sucintas, la sobrecarga relativa se reduce arbitrariamente a medida que aumenta el tamaño del problema.
Por supuesto, el tamaño del problema puede ser enorme para darse cuenta de la sobrecarga relativa que desea, pero no puede tenerlo todo.
Bien, con eso bajo nuestros cinturones, pongamos algunos números en el problema. Supongamos que las claves son enteros de bits (por lo que el tamaño del universo es ), y queremos almacenar de estos enteros. Supongamos que podemos organizar mágicamente una tabla hash idealizada con ocupación total y sin desperdicio, por lo que necesitamos exactamente ranuras hash.2 n 2 m 2 mnorte2norte2metro2metro
Una operación de búsqueda hash la clave de bits , enmascara bits para encontrar las ranuras de hash y luego verifica si el valor en la tabla coincide con la clave. Hasta aquí todo bien.mnortemetro
Tal tabla hash usa bits. ¿Podemos hacerlo mejor que esto?n 2metro
Suponga que la función hash es invertible. Entonces no tenemos que almacenar la clave completa en cada ranura de hash. La ubicación de la ranura de hash le da bits del valor de hash, por lo que si solo almacenó los restantes, puede reconstruir la clave a partir de esas dos piezas de información (la ubicación de la ranura de hash y el valor almacenado allí). Por lo tanto, solo necesitaría bits de almacenamiento.m n - m ( n - m ) 2 mhmetron - m( n - m ) 2metro
Si es pequeño en comparación con , la aproximación de Stirling y una pequeña aritmética (¡la prueba es un ejercicio!) Revela que:2 n2metro2norte
( n - m ) 2metro= log( 2norte2metro) +o(log( 2norte2metro) )
Entonces esta estructura de datos es sucinta.
Sin embargo, hay dos capturas.
La primera trampa es construir funciones hash invertibles "buenas". Afortunadamente, esto es mucho más fácil de lo que parece; los criptógrafos hacen funciones invertibles todo el tiempo, solo que los llaman "cifrados". Podría, por ejemplo, basar una función hash en una red Feistel, que es una forma sencilla de construir funciones hash invertibles a partir de funciones hash no invertibles.
El segundo inconveniente es que las tablas hash reales no son ideales, gracias a la paradoja del cumpleaños. Por lo tanto, querrá utilizar un tipo de tabla hash más sofisticado que lo acerque a la ocupación total sin derrames. El hash de cuco es perfecto para esto, ya que te permite acercarte arbitrariamente al ideal en teoría, y bastante cerca en la práctica.
El hash de Cuckoo requiere múltiples funciones hash, y requiere que los valores en las ranuras hash se etiqueten con la función hash utilizada. Entonces, si usa cuatro funciones hash, por ejemplo, necesita almacenar dos bits adicionales en cada ranura hash. Esto sigue siendo sucinto a medida que crece, por lo que no es un problema en la práctica, y aún supera el almacenamiento de claves completas.metro
Ah, también es posible que desee mirar los árboles de van Emde Boas.
MÁS PENSAMIENTOS
Si está en algún lugar alrededor de , entonces es aproximadamente , entonces (una vez más) suponiendo que no hay más correlación entre los valores, básicamente no puede hacer nada mejor que un vector de bits Notará que la solución de hash anterior degenera efectivamente en ese caso (termina almacenando un bit por ranura de hash), pero es más barato usar la clave como la dirección en lugar de usar una función de hash.un log ( uu2 ulog(un)u
Si está muy cerca de , toda la literatura de estructuras de datos sucintas le aconseja que invierta el sentido del diccionario. Almacene los valores que no ocurren en el conjunto. Sin embargo, ahora debe admitir efectivamente la operación de eliminación, y para mantener un comportamiento breve también debe poder reducir la estructura de datos a medida que se "agregan" más elementos. Expandir una tabla hash es una operación bien entendida, pero contraerla no lo es.unu