En informática , una estructura de datos sucinta es una estructura de datos que utiliza una cantidad de espacio que está "cerca" del límite inferior de la teoría de la información , pero (a diferencia de otras representaciones comprimidas) aún permite operaciones de consulta eficientes. El concepto fue introducido originalmente por Jacobson [1] para codificar vectores de bits , árboles (sin etiquetar) y gráficos planos . A diferencia de los algoritmos generales de compresión de datos sin pérdidas , las estructuras de datos concisas conservan la capacidad de utilizarlas in situ, sin descomprimirlas primero. Una noción relacionada es la de estructura de datos comprimidos , en la medida en que el tamaño de los datos almacenados o codificados depende de manera similar del contenido específico de los datos mismos.
Supongamos que ese es el número óptimo de bits en teoría de la información necesarios para almacenar algunos datos. Una representación de estos datos se llama:
Por ejemplo, una estructura de datos que utiliza bits de almacenamiento es compacta, los bits son concisos, los bits también son concisos y los bits están implícitos.
Por tanto, las estructuras implícitas suelen reducirse a almacenar información utilizando alguna permutación de los datos de entrada; el ejemplo más conocido de esto es el montón .
Los diccionarios indexables sucintos, también llamados diccionarios de clasificación/selección , forman la base de una serie de técnicas de representación sucintas, incluidos árboles binarios , árboles -arios y conjuntos múltiples , [2] así como árboles y matrices de sufijos . [3] El problema básico es almacenar un subconjunto de un universo , generalmente representado como una matriz de bits donde iff. Un diccionario indexable admite los métodos habituales en diccionarios (consultas e inserciones/eliminaciones en el caso dinámico), así como las siguientes operaciones. :
para .
En otras palabras, devuelve el número de elementos igual a hasta la posición mientras que devuelve la posición de la -ésima aparición de .
Hay una representación simple [4] que utiliza bits de espacio de almacenamiento (la matriz de bits original y una estructura auxiliar) y admite clasificación y selección en tiempo constante. Utiliza una idea similar a la de las consultas de rango mínimo ; hay un número constante de recursiones antes de detenerse en un subproblema de tamaño limitado. La matriz de bits se divide en bloques grandes de tamaño de bits y pequeños bloques de tamaño de bits. Para cada bloque grande, el rango de su primer bit se almacena en una tabla separada ; cada una de estas entradas toma bits para un total de bits de almacenamiento. Dentro de un bloque grande, otro directorio almacena el rango de cada uno de los bloques pequeños que contiene. La diferencia aquí es que solo necesita bits para cada entrada, ya que solo es necesario almacenar las diferencias con respecto al rango del primer bit en el bloque grande que lo contiene. Por tanto, esta tabla ocupa un total de bits. Luego se puede utilizar una tabla de búsqueda que almacene la respuesta a cada consulta de clasificación posible en una cadena de bits de longitud para ; esto requiere poco espacio de almacenamiento. Por lo tanto, dado que cada una de estas tablas auxiliares ocupa espacio, esta estructura de datos admite consultas de clasificación en tiempo y bits de espacio.
Para responder una consulta en tiempo constante, un algoritmo de tiempo constante calcula:
En la práctica, la tabla de búsqueda se puede reemplazar por operaciones bit a bit y tablas más pequeñas que se pueden usar para encontrar el número de bits establecidos en los bloques pequeños. Esto suele ser beneficioso, ya que las estructuras de datos concisas encuentran su uso en grandes conjuntos de datos, en cuyo caso los errores de caché se vuelven mucho más frecuentes y aumentan las posibilidades de que la tabla de búsqueda sea expulsada de los cachés de CPU más cercanos. [5] Las consultas de selección se pueden admitir fácilmente realizando una búsqueda binaria en la misma estructura auxiliar utilizada para la clasificación ; sin embargo, esto lleva tiempo en el peor de los casos. Se puede utilizar una estructura más complicada que utiliza bits de almacenamiento adicional para admitir la selección en tiempo constante. [6] En la práctica, muchas de estas soluciones tienen constantes ocultas en la notación que dominan antes de que cualquier ventaja asintótica se haga evidente; Las implementaciones que utilizan operaciones de palabras anchas y bloques alineados con palabras a menudo funcionan mejor en la práctica. [7]
El enfoque espacial se puede mejorar observando que hay distintos subconjuntos de (o cadenas binarias de longitud con exactamente unos) y, por lo tanto, existe un límite inferior teórico de la información para el número de bits necesarios para almacenar . Existe un diccionario sucinto (estático) que alcanza este límite, es decir, utilizando el espacio. [8] Esta estructura se puede ampliar para admitir consultas de clasificación y selección y ocupa espacio. [2] Sin embargo, las consultas de clasificación correcta en esta estructura se limitan a los elementos contenidos en el conjunto, de forma análoga a cómo funcionan las funciones hash perfectas mínimas. Este límite se puede reducir a una compensación de espacio/tiempo reduciendo el espacio de almacenamiento del diccionario para que las consultas tomen tiempo. [9]
También es posible construir un diccionario indexable que admita clasificación (pero no selección) que utilice menos de bits. Un diccionario de este tipo se denomina función hash perfecta mínima monótona y se puede implementar utilizando tan solo bits. [10] [11]
Una tabla hash sucinta, también conocida como diccionario desordenado sucinto, es una estructura de datos que almacena claves de un universo utilizando bits de espacio y al mismo tiempo admite consultas de membresía en un tiempo esperado constante. Si una tabla hash sucinta también admite inserciones y eliminaciones en un tiempo esperado constante, entonces se la denomina dinámica y, en caso contrario, se la denomina estática.
La primera tabla hash dinámica y sucinta se debió a Raman y Rao en 2003. [12] En el caso de , su solución utiliza bits de espacio. Posteriormente, se demostró que este límite espacial podía mejorarse a bits para cualquier número constante de logaritmos [13] y poco después este límite también era óptimo. [14] [15] La última solución admite todas las operaciones en el peor de los casos en tiempo constante con alta probabilidad.
La primera tabla hash estática y sucinta se debió a Pagh en 1999. [16] [17] En el caso de , su solución utiliza bits de espacio y admite consultas de tiempo constante en el peor de los casos . Este límite se mejoró posteriormente a bits, [18] y luego a bits. [19] Mientras que las dos primeras soluciones [17] [18] admiten consultas de tiempo constante en el peor de los casos, la última admite consultas de tiempo esperado constante. [19] La solución final también requiere acceso a una tabla de búsqueda de tamaño , pero esta tabla de búsqueda es independiente del conjunto de elementos que se almacenan. [19]
Una cadena con una longitud arbitraria ( cadena Pascal ) ocupa el espacio Z + log( Z ) y, por lo tanto, es concisa. Si hay una longitud máxima (que es el caso en la práctica, ya que 2 32 = 4 GiB de datos es una cadena muy larga, y 2 64 = 16 EiB de datos es más grande que cualquier cadena en la práctica) entonces una cadena con una longitud También está implícito, tomando el espacio Z + k , donde k es el número de datos para representar la longitud máxima (por ejemplo, 64 bits).
Cuando es necesario codificar una secuencia de elementos de longitud variable (como cadenas), existen varias posibilidades. Un enfoque directo es almacenar una longitud y un elemento en cada registro; luego, estos se pueden colocar uno tras otro. Esto permite ser eficiente a continuación, pero no encontrar el késimo elemento. Una alternativa es colocar los elementos en orden con un delimitador (por ejemplo, una cadena terminada en nulo ). Esto utiliza un delimitador en lugar de una longitud y es sustancialmente más lento, ya que se debe escanear toda la secuencia en busca de delimitadores. Ambos ahorran espacio. Un enfoque alternativo es la separación fuera de banda: los elementos pueden simplemente colocarse uno tras otro, sin delimitadores. Luego, los límites de los elementos se pueden almacenar como una secuencia de longitud, o mejor, compensaciones en esta secuencia. Alternativamente, se codifica junto con él una cadena binaria separada que consta de 1 en las posiciones donde comienza un elemento y 0 en el resto. Dada esta cadena, la función puede determinar rápidamente dónde comienza cada elemento, dado su índice. [20] Esto es compacto pero no conciso, ya que requiere 2 espacios Z , que es O( Z ).
Otro ejemplo es la representación de un árbol binario : un árbol binario arbitrario en nodos se puede representar en bits y al mismo tiempo admite una variedad de operaciones en cualquier nodo, lo que incluye encontrar su padre, su hijo izquierdo y derecho, y devolver el tamaño de su subárbol. , cada uno en tiempo constante. El número de árboles binarios diferentes en los nodos es . En el caso de las grandes , se trata de ; por lo tanto, necesitamos al menos unos bits para codificarlo. Por lo tanto, un árbol binario sucinto ocuparía sólo bits por nodo.