Atlasingeniería

Estructuras de datosEstructuras linealesTema 3

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:

siguiente(i)=(i+1)modC,\operatorname{siguiente}(i)=(i+1)\bmod C,

donde CC es la capacidad. Avanzar índices cuesta O(1)O(1) 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.

1 / 5
El arreglo nunca se mueve: lo que se mueven son los índices, y al pasarse del final vuelven a 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 qq de elementos. Entonces las invariantes son 0qC0\leq q\leq C, 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í

En el último paso del buffer de arriba, cabeza y cola quedaron las dos en la celda 1. ¿Qué significa?

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 O(1)O(1) y el espacio queda acotado por O(C)O(C). 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

DequeBuffer circular
Tamañocrecefijo, decidido de antemano
Al llenarsereserva mássobrescribe, rechaza o bloquea
Memoriavariableconstante y conocida
Reserva en tiempo de ejecuciónno: se reserva una vez
Dónde se usaalgoritmos: ventana deslizante, BFS 0-1sistemas: audio, red, registros, telemetría
La diferencia práctica no es la estructura sino la decisión: el buffer circular obliga a decidir de antemano qué pasa cuando se llena, y esa obligación es la ventaja.
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?

En un buffer circular, cabeza y cola quedan en el mismo índice. ¿Qué significa?
¿Para qué sirve el módulo en siguiente(i) = (i + 1) mod C?
Un historial de métricas con buffer lleno. ¿Qué política suele corresponder?
¿Qué gana una deque frente a una pila y una cola?