Deques y buffers circulares sin desplazar datos
Una cola doble opera en ambos extremos y un buffer circular reutiliza un arreglo fijo. Juntos permiten colas rápidas y memoria acotada.
Para este tema conviene tener claro:Pilas y colas
Cuando una cola llega al final de un arreglo, no hace falta mover todo hacia el principio. Podemos volver a la primera posición libre y tratar el arreglo como un círculo. Esa idea convierte espacio fijo en una cola eficiente.
La deque: los dos extremos
Una deque, o cola doble, permite agregar y retirar por ambos extremos. Sus cuatro operaciones
básicas son pushFront, pushBack, popFront y popBack.
Puede comportarse como cola usando un extremo para entrar y el otro para salir, o como pila usando el mismo extremo para ambas cosas. También sirve en algoritmos de ventana deslizante, donde elementos viejos salen por delante mientras nuevos candidatos entran por detrás.
El buffer circular: un arreglo que da la vuelta
Un buffer circular guarda posiciones de cabeza y cola dentro de un arreglo de capacidad fija. Después de la última celda, el índice siguiente vuelve a cero:
donde es la capacidad. Avanzar índices cuesta y ningún elemento debe desplazarse. El arreglo físico es lineal; lo circular es la forma de interpretar sus posiciones.
Buffer de capacidad 4, vacío. Cabeza y cola arrancan en la misma celda y la cantidad es cero.
Vacío y lleno se ven iguales
Si cabeza y cola tienen el mismo índice, el buffer podría estar vacío o lleno. Para evitar la ambigüedad podemos guardar además la cantidad de elementos. Entonces las invariantes son , la cabeza señala el próximo valor que sale y la cola la próxima celda libre.
Otra estrategia reserva siempre una celda vacía, pero reduce la capacidad útil. Las dos son válidas; lo importante es que la representación permita distinguir todos sus estados.
Antes de seguir, predecí
Una cola circular mínima
Una cola circular mínima puede mantener esos tres datos:
class CircularQueue<T> {
private readonly slots: Array<T | undefined>;
private head = 0;
private tail = 0;
private count = 0;
constructor(private readonly capacity: number) {
if (capacity <= 0) throw new Error('La capacidad debe ser positiva.');
this.slots = new Array<T | undefined>(capacity);
}
enqueue(value: T): boolean {
if (this.count === this.capacity) return false;
this.slots[this.tail] = value;
this.tail = (this.tail + 1) % this.capacity;
this.count += 1;
return true;
}
dequeue(): T | undefined {
if (this.count === 0) return undefined;
const value = this.slots[this.head];
this.slots[this.head] = undefined;
this.head = (this.head + 1) % this.capacity;
this.count -= 1;
return value;
}
}Qué hacer cuando se llena
Cuando el buffer se llena, la aplicación debe elegir una política. Una cola de trabajos puede rechazar el nuevo elemento o bloquear al productor. Un historial de métricas puede sobrescribir el dato más viejo porque interesa conservar sólo la ventana reciente.
No hay una respuesta universal. La capacidad fija vuelve explícito un límite que de otro modo aparecería tarde como consumo creciente de memoria.
El costo y el precio de la capacidad fija
Las operaciones en los extremos cuestan y el espacio queda acotado por . El precio es elegir la capacidad y manejar correctamente los estados vacío y lleno.
Una deque basada en bloques puede crecer dinámicamente; un buffer circular prioriza memoria predecible. Para audio, redes, telemetría o comunicación entre productores y consumidores, esa previsibilidad suele ser más importante que crecer sin límite.
Dónde aparecen de verdad
| Deque | Buffer circular | |
|---|---|---|
| Tamaño | crece | fijo, decidido de antemano |
| Al llenarse | reserva más | sobrescribe, rechaza o bloquea |
| Memoria | variable | constante y conocida |
| Reserva en tiempo de ejecución | sí | no: se reserva una vez |
| Dónde se usa | algoritmos: ventana deslizante, BFS 0-1 | sistemas: audio, red, registros, telemetría |
Más a fondo · nivel seniorEl buffer circular entre dos hilos
Con un solo productor y un solo consumidor, un buffer circular se puede usar sin ningún bloqueo, y es una de las estructuras concurrentes más usadas por ese motivo. La clave es que el productor sólo escribe el índice de cola y el consumidor sólo el de cabeza: nadie escribe la misma variable, así que no hay carrera.
Lo que sí hace falta son barreras de memoria, porque el procesador y el compilador pueden reordenar la escritura del dato y la del índice. Si el índice se publica antes que el dato, el consumidor puede leer basura. Escribir el dato, poner una barrera, y recién después avanzar el índice es el orden correcto, y es exactamente la parte que se olvida y produce errores que sólo aparecen bajo carga.
Con varios productores la cosa se complica bastante y hay que usar operaciones atómicas de comparar e intercambiar sobre el índice. Por eso, cuando se puede, la arquitectura preferida es un buffer por productor en vez de uno compartido.
Cierre
Una deque generaliza pilas y colas al permitir operaciones en ambos extremos. Un buffer circular implementa esas operaciones sobre espacio reutilizable, sin desplazamientos. La clave no es el módulo: son las invariantes de cabeza, cola, cantidad y política de capacidad.
Autoevaluación
¿Lo entendiste?
Práctica