Saltar al contenido principal

Cola de prioridad limitada

Consideremos el siguiente esquema. Usaremos la mitad de la memoria, M2\frac{M}{2}, para mantener una cola de prioridad clásica HH. Todas las inserciones ocurrirán en HH, gratis. Mientras esta cola no se desborde, no usaremos el disco.

En el momento en que se inserte un nuevo elemento y HH esté llena, ésta se ordenará completamente en memoria (gratis) y se almacenará en un archivo en disco, F1F_1, lo que requerirá M2B\frac{M}{2B} escrituras. Inmediatamente crearemos un buffer de tamaño BB en memoria para F1F_1, donde leeremos su primer bloque. HH quedará vacía de nuevo para aceptar nuevas inserciones.

De ahora en adelante, cada vez que extraigamos el mínimo, tendremos que elegir entre el mínimo de HH y el primer elemento del buffer de F1F_1. Una vez que leímos todo el buffer de F1F_1, lo volvemos a llenar leyendo el siguiente bloque de BB elementos.

Como HH sigue recibiendo inserciones, puede volverse a llenar. En este caso lo ordenamos nuevamente y lo escribimos en un nuevo archivo, F2F_2. En general, tendremos kk archivos ordenados F1,,FkF_1, \ldots, F_k, y las extracciones de mínimo tendrán que considerar el mínimo entre el mínimo de HH y los mínimos de cada FiF_i. Esto se hace fácilmente en tiempo de CPU O(logk)O(\log k) con una pequeña cola de prioridad que mantiene los primeros elementos de cada FiF_i y los reemplaza por el siguiente de su buffer cuando éstos son extraídos.

Note que en todo momento los archivos FiF_i pueden estar a medio leer. Podríamos pensar en un mecanismo más sofisticado que eliminara los archivos leídos, o los uniera cuando se hicieran pequeños, pero aquí mantendremos la simplicidad: los archivos FiF_i se crean y se van leyendo, y nunca se eliminan o unen.

Cola de Prioridad limitada

Considerando que tenemos M2\frac{M}{2} espacio de memoria para los buffers, tenemos un límite de kM2Bk \le \frac{M}{2B}. Esto significa que tenemos un límite de NkM2+M2M2(M2B+1)=O(M2B)N \le k\cdot\frac{M}{2} + \frac{M}{2} \le \frac{M}{2}(\frac{M}{2B}+1) = O(\frac{M^2}{B}) al total de elementos que pueden ser insertados en esta estructura (en el peor caso; en la práctica muchos podrían eliminarse antes de pasar a un archivo FiF_i). Con una memoria de GBs y un BB de KBs, esto equivale a PBs (petabytes).

Para analizar el costo de las operaciones, consideremos lo que nos puede costar un elemento desde que es insertado hasta que es extraído. La inserción es gratis, pero el elemento puede finalmente ser enviado a un archivo FiF_i, donde es escrito junto con otros B1B-1 elementos, por lo que podemos cobrarle 1B\frac{1}{B} escrituras. Luego, puede ser leído de este archivo a su buffer en memoria, junto con otros BB elementos, por lo que podemos cobrarle 1B\frac{1}{B} lecturas. En total, cada operación cuesta O(1B)O(\frac{1}{B}) I/Os. Esto, por supuesto, es en un sentido amortizado: muchas operaciones son gratis, y de repente una inserción provoca un costo de O(MB)O(\frac{M}{B}) para escribir un archivo FiF_i completo. En un esquema más sofisticado, podemos "deamortizar" el costo mediante escribir este archivo poco a poco, dividiendo HH en dos colas de tamaño M/4M/4, de manera que cuando una se llena empezamos a usar la otra y vamos escribiendo la que se llenó poco a poco a disco, a lo largo de las sucesivas inserciones que siguen. Debemos asegurar que, para cuando la segunda cola se llene, la primera ya se habrá vaciado y puedan intercambiar sus roles.

Aún en sentido amortizado, esta complejidad parece violar la cota inferior: podríamos ordenar en disco en tiempo O(NB)O(\frac{N}{B}) mediante insertar los NN elementos y luego extraerlos de esta cola de prioridad. Esto es efectivamente cierto, pero dentro de la limitación de N=O(M2B)N = O(\frac{M^2}{B}). Bajo este supuesto, la complejidad Θ(nlogmnm)\Theta(n\log_m \frac{n}{m}) de ordenar es efectivamente Θ(n)\Theta(n).