Un deque es una cola doblemente terminada que permite insertar y extraer elementos tanto por el frente como por el final. Mantiene el orden de llegada, pero admite patrones de acceso más flexibles que una cola FIFO estricta.
Es la base de planificadores, caches LRU y estructuras de datos avanzadas como los algoritmos de ventana deslizante.
La interfaz mínima se compone de cuatro operaciones de modificación y dos accesos de solo lectura:
push_front / push_back: insertan un nuevo nodo en cada extremo.pop_front / pop_back: retiran nodos y retornan el valor asociado.front / back: leen sin extraer, devolviendo -1 o un código de error si la estructura está vacía.Para evitar condiciones de carrera se recomienda empaquetar cada operación en funciones autónomas y documentar si son atómicas.
Hay dos enfoques predominantes:
Elegir uno u otro depende de si prima la velocidad o la elasticidad. En sistemas embebidos se prefiere el array, mientras que en servidores con cargas variables manda la lista.
En un deque circular, los índices frente y final se desplazan en sentidos opuestos y aprovechan el operador módulo para reutilizar el buffer.
#define CAP_DEQUE 64
typedef struct {
int datos[CAP_DEQUE];
int frente;
int final;
int cantidad;
} DequeArray;
void inicializarDeque(DequeArray *d) {
d->frente = 0;
d->final = 0;
d->cantidad = 0;
}
static int anteriorIndice(int idx) {
return (idx - 1 + CAP_DEQUE) % CAP_DEQUE;
}
static int siguienteIndice(int idx) {
return (idx + 1) % CAP_DEQUE;
}
int pushFront(DequeArray *d, int valor) {
if (d->cantidad == CAP_DEQUE) {
return 0;
}
d->frente = anteriorIndice(d->frente);
d->datos[d->frente] = valor;
d->cantidad++;
return 1;
}
int pushBack(DequeArray *d, int valor) {
if (d->cantidad == CAP_DEQUE) {
return 0;
}
d->datos[d->final] = valor;
d->final = siguienteIndice(d->final);
d->cantidad++;
return 1;
}
Los pop simétricos aplican el mismo principio. Registrar cantidad permite distinguir entre lleno y vacío aun cuando los índices coincidan.
Una lista doblemente enlazada simplifica las operaciones de extremos, ya que cada nodo conoce a sus vecinos. Se recomienda mantener punteros a ambos extremos y un contador de nodos.
typedef struct Nodo {
int dato;
struct Nodo *anterior;
struct Nodo *siguiente;
} Nodo;
typedef struct {
Nodo *frente;
Nodo *final;
int cantidad;
} DequeLista;
void inicializarLista(DequeLista *d) {
d->frente = NULL;
d->final = NULL;
d->cantidad = 0;
}
int push_front_lista(DequeLista *d, int valor) {
Nodo *nuevo = malloc(sizeof(Nodo));
if (!nuevo) {
return 0;
}
nuevo->dato = valor;
nuevo->anterior = NULL;
nuevo->siguiente = d->frente;
if (d->frente) {
d->frente->anterior = nuevo;
} else {
d->final = nuevo;
}
d->frente = nuevo;
d->cantidad++;
return 1;
}
int pop_back_lista(DequeLista *d, int *valor) {
if (!d->final) {
return 0;
}
Nodo *nodo = d->final;
*valor = nodo->dato;
d->final = nodo->anterior;
if (d->final) {
d->final->siguiente = NULL;
} else {
d->frente = NULL;
}
free(nodo);
d->cantidad--;
return 1;
}
Este estilo evita reasignar arrays y mantiene la complejidad constante aun cuando el deque crece o se vacía repetidamente.
Siempre documenta si el deque es seguro para hilos o si requiere protección externa (mutex, spinlocks) antes de exponerlo a un scheduler.
El siguiente programa implementa un deque mediante lista doble y expone todas las operaciones, además de una función imprimir para depurar el estado.
#include <stdio.h>
#include <stdlib.h>
typedef struct Nodo {
int dato;
struct Nodo *anterior;
struct Nodo *siguiente;
} Nodo;
typedef struct {
Nodo *frente;
Nodo *final;
int cantidad;
} Deque;
void inicializar(Deque *d) {
d->frente = NULL;
d->final = NULL;
d->cantidad = 0;
}
int push_front(Deque *d, int valor) {
Nodo *nuevo = malloc(sizeof(Nodo));
if (!nuevo) {
return 0;
}
nuevo->dato = valor;
nuevo->anterior = NULL;
nuevo->siguiente = d->frente;
if (d->frente) {
d->frente->anterior = nuevo;
} else {
d->final = nuevo;
}
d->frente = nuevo;
d->cantidad++;
return 1;
}
int push_back(Deque *d, int valor) {
Nodo *nuevo = malloc(sizeof(Nodo));
if (!nuevo) {
return 0;
}
nuevo->dato = valor;
nuevo->siguiente = NULL;
nuevo->anterior = d->final;
if (d->final) {
d->final->siguiente = nuevo;
} else {
d->frente = nuevo;
}
d->final = nuevo;
d->cantidad++;
return 1;
}
int pop_front(Deque *d, int *valor) {
if (!d->frente) {
return 0;
}
Nodo *nodo = d->frente;
*valor = nodo->dato;
d->frente = nodo->siguiente;
if (d->frente) {
d->frente->anterior = NULL;
} else {
d->final = NULL;
}
free(nodo);
d->cantidad--;
return 1;
}
int pop_back(Deque *d, int *valor) {
if (!d->final) {
return 0;
}
Nodo *nodo = d->final;
*valor = nodo->dato;
d->final = nodo->anterior;
if (d->final) {
d->final->siguiente = NULL;
} else {
d->frente = NULL;
}
free(nodo);
d->cantidad--;
return 1;
}
void imprimir(const Deque *d) {
printf("[deque] cantidad=%d -> ", d->cantidad);
for (Nodo *n = d->frente; n; n = n->siguiente) {
printf("%d ", n->dato);
}
puts("");
}
void vaciar(Deque *d) {
int descartado;
while (pop_front(d, &descartado)) {
}
}
int main(void) {
Deque deque;
inicializar(&deque);
push_back(&deque, 10);
push_back(&deque, 20);
push_front(&deque, 5);
imprimir(&deque);
int valor;
pop_front(&deque, &valor);
printf("pop_front = %d\n", valor);
imprimir(&deque);
push_front(&deque, 1);
push_back(&deque, 99);
imprimir(&deque);
pop_back(&deque, &valor);
printf("pop_back = %d\n", valor);
imprimir(&deque);
vaciar(&deque);
imprimir(&deque);
return 0;
}
Ejecuta el programa y observa cómo las operaciones afectan al contador y a los nodos interiores para validar que no quedan referencias colgantes.