0% encontró este documento útil (0 votos)
3 vistas13 páginas

Colas Dsa Java

Las colas son estructuras de datos esenciales que determinan el orden de procesamiento de elementos y son fundamentales en algoritmos y sistemas concurrentes. Existen diferentes tipos de colas, como FIFO, LIFO, Deque y Priority Queue, cada una con características y aplicaciones específicas. Comprender las colas es crucial para el diseño eficiente de sistemas y algoritmos, ya que su elección impacta directamente en la complejidad temporal y el rendimiento.

Cargado por

Maria Castillo
Derechos de autor
© All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
3 vistas13 páginas

Colas Dsa Java

Las colas son estructuras de datos esenciales que determinan el orden de procesamiento de elementos y son fundamentales en algoritmos y sistemas concurrentes. Existen diferentes tipos de colas, como FIFO, LIFO, Deque y Priority Queue, cada una con características y aplicaciones específicas. Comprender las colas es crucial para el diseño eficiente de sistemas y algoritmos, ya que su elección impacta directamente en la complejidad temporal y el rendimiento.

Cargado por

Maria Castillo
Derechos de autor
© All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd

Colas en Data Structures & Algorithms — Guía

Completa en Java
Basado en Goodrich, Tamassia & Goldwasser — Data Structures & Algorithms in Java

Resumen Ejecutivo
Las colas (queues) son estructuras de datos fundamentales en el análisis de algoritmos. Definen
el orden en que los elementos se procesan y, dependiendo del tipo de cola utilizado, permiten
resolver problemas que van desde la planificación de procesos en un sistema operativo hasta la
implementación de algoritmos de búsqueda en grafos. Comprender las colas es comprender
cómo fluye la información a través de un sistema.

1. Importancia de las Colas en DSA


Una cola es una colección ordenada de elementos en la que la posición o prioridad de cada
elemento determina cuándo será procesado. No son solo un contenedor: son una política de
decisión.

¿Por qué son críticas?


Modelan el mundo real: colas de impresión, atención al cliente, solicitudes HTTP,
planificación de tareas en la CPU.
Son la base de algoritmos esenciales: BFS (Breadth-First Search) usa una cola FIFO; los
algoritmos de Dijkstra y A* usan una Priority Queue; la evaluación de expresiones usa una
pila (Stack).
Definen la complejidad temporal de los algoritmos: la elección del tipo de cola impacta
directamente en el tiempo de ejecución vs de operaciones críticas.
Son el pilar del diseño de sistemas concurrentes: los BlockingQueue de Java son
fundamentales en arquitecturas productor-consumidor.

Regla de Oro para el Análisis


Antes de elegir una cola, pregunta : ¿qué elemento debe procesarse primero y bajo qué
criterio?

Criterio de prioridad Tipo de cola recomendado

Orden de llegada Cola FIFO simple

Último en llegar Pila (Stack / LIFO)

Ambos extremos Deque (doble extremo)


Criterio de prioridad Tipo de cola recomendado

Mayor/menor valor numérico Priority Queue (Heap)

Tiempo real de ejecución Delay Queue o TimerQueue

2. Clases de Colas — Taxonomía Completa

2.1 Cola Simple (FIFO — First In, First Out)


El primer elemento en entrar es el primero en salir. Es el modelo más intuitivo: como la fila de un
supermercado.

Operaciones fundamentales:

Operación Descripción Complejidad

enqueue(e) Agrega e al final de la cola

dequeue() Extrae y retorna el frente

first() Consulta el frente sin extraer

size() Cantidad de elementos

isEmpty() Verifica si está vacía

Qué analizar en una Cola FIFO:

¿La implementación es con array circular o con lista enlazada?


¿Existe riesgo de desbordamiento (overflow) si se usa array de tamaño fijo?
¿El orden de llegada es justo para todos los elementos (fairness)?

// Cola FIFO con ArrayDeque (implementación preferida en Java)


import [Link];
import [Link];

public class ColaFIFO {


public static void main(String[] args) {
Queue<String> cola = new ArrayDeque<>();

// Simulación de llegada de clientes


[Link]("Cliente-A"); // enqueue
[Link]("Cliente-B");
[Link]("Cliente-C");

[Link]("Atendiendo: " + [Link]()); // Cliente-A


[Link]("Siguiente: " + [Link]()); // Cliente-B (sin extraer)
[Link]("Atendiendo: " + [Link]()); // Cliente-B

// Cola: [Cliente-C]
}
}

2.2 Pila (Stack — LIFO: Last In, First Out)


El último elemento en entrar es el primero en salir. Como una pila de platos: solo se accede al
plato del tope.

Operaciones fundamentales:

Operación Descripción Complejidad

push(e) Coloca e en el tope amortizado

pop() Extrae y retorna el tope amortizado

top() / peek() Consulta el tope sin extraer

isEmpty() Verifica si está vacía

Qué analizar en una Pila :

¿Existe riesgo de StackOverflowError en recursión profunda?


¿Se necesita simular recursión de forma iterativa con una pila explícita?
¿El comportamiento LIFO modela correctamente el problema (ej. paréntesis balanceados,
historial de navegación)?

import [Link];
import [Link];

public class ValidadorParentesis {


// EJEMPLO DE MEDIANA COMPLEJIDAD
// Verifica si una expresión tiene paréntesis, corchetes y llaves balanceados.
// Usa una pila: cada apertura se "recuerda" y cada cierre debe coincidir con el últ
public static boolean esValida(String expresion) {
Deque<Character> pila = new ArrayDeque<>();

for (char c : [Link]()) {


if (c == '(' || c == '[' || c == '{') {
[Link](c); // Apila la apertura
} else if (c == ')' || c == ']' || c == '}') {
if ([Link]()) return false; // Cierre sin apertura previa
char tope = [Link]();
// Verifica que el cierre corresponda a la última apertura
if ((c == ')' && tope != '(') ||
(c == ']' && tope != '[') ||
(c == '}' && tope != '{')) {
return false;
}
}
}
return [Link](); // Debe estar vacía si todo está balanceado
}
public static void main(String[] args) {
[Link](esValida("{[()]}")); // true
[Link](esValida("{[(])}")); // false — cierre incorrecto
[Link](esValida("((())")); // false — falta cerrar
}
}

2.3 Deque (Double-Ended Queue — Cola de Doble Extremo)


Permite insertar y extraer por ambos extremos (frente y final). Es la más flexible de las colas
básicas.

Operaciones fundamentales:

Operación Descripción Complejidad

addFirst(e) Inserta al frente

addLast(e) Inserta al final

removeFirst() Extrae del frente

removeLast() Extrae del final

peekFirst() Consulta el frente

peekLast() Consulta el final

Qué analizar en un Deque:

¿El problema requiere acceso por ambos extremos simultáneamente?


¿Se puede usar como pila (solo addFirst/removeFirst) o cola (solo addLast/removeFirst)?
¿La implementación subyacente es con array circular o lista doblemente enlazada?

// EJEMPLO DE MEDIANA COMPLEJIDAD — Ventana deslizante con Deque


// Dado un arreglo y un tamaño k, encuentra el máximo en cada ventana de k elementos.
// Complejidad: O(n) — cada elemento se agrega y elimina del deque a lo sumo una vez.
import [Link];
import [Link];

public class MaxVentanaDeslizante {


public static int[] maxVentana(int[] nums, int k) {
int n = [Link];
int[] resultado = new int[n - k + 1];
// El deque guarda ÍNDICES, no valores.
// Invariante: los índices en el deque corresponden a elementos en orden DECRECI
Deque<Integer> deque = new ArrayDeque<>();

for (int i = 0; i < n; i++) {


// 1. Elimina índices que ya no están en la ventana actual [i-k+1, i]
while (![Link]() && [Link]() < i - k + 1) {
[Link]();
}
// 2. Elimina índices cuyo valor es menor que el actual (ya no pueden ser máx
while (![Link]() && nums[[Link]()] < nums[i]) {
[Link]();
}
[Link](i); // Agrega el índice actual al final

// 3. El frente del deque siempre tiene el índice del máximo de la ventana


if (i >= k - 1) {
resultado[i - k + 1] = nums[[Link]()];
}
}
return resultado;
}

public static void main(String[] args) {


int[] nums = {1, 3, -1, -3, 5, 3, 6, 7};
int k = 3;
int[] maximos = maxVentana(nums, k);
// Ventanas: [1,3,-1]=3, [3,-1,-3]=3, [-1,-3,5]=5, [-3,5,3]=5, [5,3,6]=6, [3,6,7]=7
for (int m : maximos) [Link](m + " "); // 3 3 5 5 6 7
}
}

2.4 Priority Queue (Cola de Prioridad)


Una Priority Queue es una colección de pares (clave, valor) donde la clave representa la
prioridad. A diferencia de FIFO, el orden de salida lo determina la prioridad, no el tiempo de
llegada.

Sus dos operaciones fundamentales son:

insert(k, v) — agrega el par (clave k, valor v) a la cola.


removeMin() — extrae y retorna el par cuya clave es la menor de todas.
Complejidad según implementación:

Implementación insert removeMin min Uso ideal

Lista desordenada Pocas extracciones

Lista ordenada Pocas inserciones

Heap binario Uso general (estándar)

Heap de Fibonacci amort. Grafos con muchas decrements

Qué analizar en una Priority Queue:

¿Se necesita min-heap (mínimo primero) o max-heap (máximo primero)?


¿Las prioridades cambian durante la ejecución? (requiere decreaseKey)
¿Se procesan elementos en tiempo real o en lote?
¿Es segura para hilos concurrentes? (PriorityBlockingQueue)

Caso de Estudio: Traza de operaciones (Goodrich et al.)


Se ejecuta la siguiente secuencia sobre una PQ vacía:

# Operación Contenido PQ (ordenado por clave ↑) Retorna

1 insert(5, A) (5,A) —

2 insert(4, B) (4,B) (5,A) —

3 insert(7, F) (4,B) (5,A) (7,F) —

4 insert(1, D) (1,D) (4,B) (5,A) (7,F) —

5 removeMin() (4,B) (5,A) (7,F) (1, D)

6 insert(3, J) (3,J) (4,B) (5,A) (7,F) —

7 insert(6, L) (3,J) (4,B) (5,A) (6,L) (7,F) —

8 removeMin() (4,B) (5,A) (6,L) (7,F) (3, J)

12 insert(2, H) (2,H) (6,L) (7,F) (8,G) —

13 removeMin() (6,L) (7,F) (8,G) (2, H) ← ¡caso clave!

Regla clave: La PQ NO respeta el orden de inserción. El elemento (2,H) se insertó tarde


pero sale antes que (6,L), (7,F) y (8,G) porque su clave es menor.

import [Link];

public class EjemploPriorityQueue {


public static void main(String[] args) {
// Min-heap por defecto en Java (mínimo primero)
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[0] - b[0]);

[Link](new int[]{5, 'A'});


[Link](new int[]{4, 'B'});
[Link](new int[]{7, 'F'});
[Link](new int[]{1, 'D'});

int[] min = [Link](); // Extrae (1, D)


[Link]("removeMin → clave=%d, valor=%c%n", min[0], (char) min[1]);
}
}

3. Análisis Profundo: Priority Queue con Heap Binario


El heap binario es la implementación estándar de la Priority Queue. Es un árbol binario completo
almacenado en un array que cumple la propiedad de heap: cada nodo tiene una clave menor o
igual que la de sus hijos.
Estructura en memoria (array)
Para un nodo en la posición i (índice base 1):

Padre: posición
Hijo izquierdo: posición
Hijo derecho: posición

Operaciones clave
insert — Up-Heap Bubbling (burbuja hacia arriba):

1. Agrega el nuevo elemento al final del array (mantiene la completitud).


2. Compara con su padre: si viola la propiedad de heap, intercambia (swap).
3. Repite hasta que el elemento esté en su posición correcta.
4. En el peor caso sube hasta la raíz: comparaciones.
removeMin — Down-Heap Bubbling (burbuja hacia abajo):

1. Extrae la raíz (el mínimo).


2. Mueve el último elemento a la raíz (mantiene la completitud).
3. Compara con sus hijos: intercambia con el hijo menor si viola la propiedad.
4. Repite hasta llegar a una hoja: comparaciones.

4. Ejemplo de Alta Complejidad — Stack Implementado con Priority Queue


Este ejemplo clásico de Goodrich et al. demuestra la versatilidad de los ADTs: implementar una
pila (LIFO) usando únicamente una Priority Queue mínima.

El problema conceptual
Stack y Priority Queue tienen políticas de salida opuestas:

Aspecto Stack (pila) Priority Queue

Política de salida LIFO — sale el último en entrar Sale la menor clave

¿Qué elemento sale primero? El más reciente El de menor clave

Solución: Claves negativas decrecientes


Invariante: el elemento insertado más recientemente debe tener siempre la clave con mayor
prioridad (mínima) en la cola.

Mecanismo: se usa una variable timestamp que empieza en 0 y se decrementa con cada push. Así,
el último elemento siempre tiene la clave más negativa → siempre sale primero →
comportamiento LIFO.
Análisis comparativo de soluciones:

Aspecto Solución A (claves negativas) Solución B (claves positivas) Mejor

Variable extra int timestamp = 0 (decrementa) int count = 0 (incrementa) Igual

Clave 1° push -1 0 —

Operación removeMax() (requiere max-


removeMin() estándar Sol. A
extracción heap)

PQ requerida Min-PQ (la más común) Max-PQ (menos frecuente) Sol. A

[Link] Sol. A
Compatibilidad Java Requiere implementación propia
nativo

Complejidad push Igual

Complejidad pop Igual

Recomendación de expertos: La Solución A es preferida porque usa el Priority Queue


mínimo estándar, disponible directamente en Java, Python y C++. La Solución B requiere
un max-heap o invertir la comparación, añadiendo complejidad innecesaria.

Traza de ejecución
Operación timestamp PQ interna (clave, valor) Resultado

push("A") -1 (-1, A) A ingresa con clave -1

push("B") -2 (-2, B) (-1, A) B es nuevo tope

push("C") -3 (-3, C) (-2, B) (-1, A) C es nuevo tope

pop() — (-2, B) (-1, A) Extrae (-3, C) → "C" ✓ LIFO

pop() — (-1, A) Extrae (-2, B) → "B" ✓ LIFO

pop() — (vacía) Extrae (-1, A) → "A" ✓ LIFO

Implementación Java completa y documentada

import [Link];
import [Link];

/**
* Stack (LIFO) implementado sobre una Priority Queue mínima.
*
* Invariante: el elemento en el tope de la pila siempre tiene
* la clave más pequeña (más negativa) en la Priority Queue.
*
* Complejidad: push() y pop() en O(log n), top() en O(1).
*
* @param <V> tipo del valor almacenado
*/
public class StackUsingPQ<V> {
// Cada entrada es un par [clave, índice_del_valor] para poder guardar el valor V.
// Usamos un array de dos elementos: {timestamp, indiceValor}
private final PriorityQueue<long[]> pq;
private final [Link]<V> valores;
private long timestamp = 0; // ÚNICA variable entera adicional permitida

public StackUsingPQ() {
// El comparador ordena por clave (elemento [0]): min-heap
pq = new PriorityQueue<>((a, b) -> [Link](a[0], b[0]));
valores = new [Link]<>();
}

/**
* Apila un elemento en el tope. O(log n)
* Decrementa timestamp para que el nuevo elemento tenga la clave más pequeña.
*/
public void push(V value) {
timestamp--; // 0 → -1 → -2 → -3 ...
int idx = [Link]();
[Link](value);
[Link](new long[]{timestamp, idx});
// Invariante mantenida: timestamp es siempre la clave mínima actual.
}

/**
* Desapila y retorna el elemento del tope. O(log n)
* removeMin() extrae el par con la clave más negativa = el último insertado.
*/
public V pop() {
if (isEmpty()) throw new EmptyStackException();
long[] entrada = [Link](); // Extrae la clave mínima (más negativa)
return [Link]((int) entrada[1]);
}

/**
* Retorna (sin extraer) el elemento del tope. O(1)
*/
public V top() {
if (isEmpty()) throw new EmptyStackException();
long[] entrada = [Link](); // Consulta sin extraer
return [Link]((int) entrada[1]);
}

public boolean isEmpty() { return [Link](); }


public int size() { return [Link](); }

// ---- Demostración ----


public static void main(String[] args) {
StackUsingPQ<String> stack = new StackUsingPQ<>();

[Link]("A");
[Link]("B");
[Link]("C");

[Link]("top(): " + [Link]()); // C


[Link]("pop(): " + [Link]()); // C ← LIFO
[Link]("pop(): " + [Link]()); // B ← LIFO
[Link]("pop(): " + [Link]()); // A ← LIFO
}
}

Análisis de complejidad
Método Tiempo Justificación

push(v) O(log n) Llama a [Link]() → heap sube el elemento: O(log n) comparaciones

pop() O(log n) Llama a [Link]() → heap reestructura bajando: O(log n)

top() O(1) Llama a [Link]() → la raíz del heap siempre es el mínimo

isEmpty() O(1) Solo verifica si el heap tiene elementos

size() O(1) El heap mantiene un contador interno

Espacio extra O(1) Solo la variable timestamp; la PQ ya era necesaria

Comparación con Stack nativo: Un Stack con array dinámico tiene push y pop en
amortizado, más rápido. Sin embargo, este ejercicio demuestra la versatilidad del ADT
Priority Queue: puede simular otras estructuras de datos cambiando cómo se asignan las
claves.

5. Ejemplo de Alta Complejidad — Algoritmo de Dijkstra con Priority Queue


El algoritmo de Dijkstra para caminos más cortos es el caso de uso más importante de las Priority
Queues en la práctica real.

Análisis del problema


Dado un grafo ponderado con vértices y aristas, encontrar el camino de menor costo desde
un nodo origen hasta todos los demás.

Complejidad con Priority Queue (min-heap):

— la inserción y extracción del heap domina.


Sin heap (con array): — aceptable solo para grafos densos.

Qué analiza el experto antes de implementar


1. Estructura del grafo: ¿lista de adyacencia o matriz? (lista para grafos dispersos).
2. Tipo de heap: min-heap por peso de arista.
3. Manejo de nodos ya visitados: lazy deletion vs. decrease-key.
4. Ciclos negativos: Dijkstra falla con pesos negativos (usar Bellman-Ford en ese caso).
import [Link].*;

public class Dijkstra {


/**
* Calcula la distancia mínima desde 'origen' a todos los nodos.
* Complejidad: O((V + E) log V)
*
* @param grafo Lista de adyacencia: [Link](u) = lista de {v, peso}
* @param origen Nodo de inicio (0-indexado)
* @param V Número de vértices
* @return array de distancias mínimas desde origen
*/
public static int[] dijkstra(List<List<int[]>> grafo, int origen, int V) {
int[] dist = new int[V];
[Link](dist, Integer.MAX_VALUE);
dist[origen] = 0;

// Min-heap: [distancia_acumulada, nodo]


PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[0] - b[0]);
[Link](new int[]{0, origen});

while (![Link]()) {
int[] actual = [Link]();
int distActual = actual[0];
int u = actual[1];

// Lazy deletion: si ya encontramos un camino mejor, ignoramos esta entrada


if (distActual > dist[u]) continue;

// Relajación de aristas adyacentes


for (int[] arista : [Link](u)) {
int v = arista[0];
int peso = arista[1];
int nuevaDist = dist[u] + peso;

if (nuevaDist < dist[v]) {


dist[v] = nuevaDist;
[Link](new int[]{nuevaDist, v}); // Inserción en heap: O(log V)
}
}
}
return dist;
}

public static void main(String[] args) {


int V = 5;
List<List<int[]>> grafo = new ArrayList<>();
for (int i = 0; i < V; i++) [Link](new ArrayList<>());

// Grafo de ejemplo (no dirigido):


// 0 --4-- 1 --1-- 2
// | | |
// 2 5 2
// | | |
// 3 --1-- 4 ------+
[Link](0).add(new int[]{1, 4}); [Link](1).add(new int[]{0, 4});
[Link](0).add(new int[]{2, 2}); [Link](2).add(new int[]{0, 2});
[Link](1).add(new int[]{2, 1}); [Link](2).add(new int[]{1, 1});
[Link](1).add(new int[]{4, 5}); [Link](4).add(new int[]{1, 5});
[Link](2).add(new int[]{4, 2}); [Link](4).add(new int[]{2, 2});
[Link](3).add(new int[]{4, 1}); [Link](4).add(new int[]{3, 1});

int[] distancias = dijkstra(grafo, 0, V);


[Link]("Distancias desde nodo 0:");
for (int i = 0; i < V; i++) {
[Link](" nodo %d → %d%n", i, distancias[i]);
}
// nodo 0 → 0
// nodo 1 → 3 (0→2→1: 2+1=3, no 0→1: 4)
// nodo 2 → 2 (0→2: 2)
// nodo 3 → 5 (0→2→4→3: 2+2+1=5)
// nodo 4 → 4 (0→2→4: 2+2=4)
}
}

6. Mapa de Decisión: ¿Qué Cola Usar?

¿Qué elemento debe procesarse primero?



├── El que llegó primero (orden de llegada)
│ └── → Cola FIFO (Queue / ArrayDeque)

├── El que llegó último (más reciente)
│ └── → Pila Stack (Deque como pila)

├── Acceso por ambos extremos
│ └── → Deque (ArrayDeque / LinkedList)

├── El de mayor o menor prioridad numérica
│ ├── Pocas operaciones (< 100 elementos)
│ │ └── → Lista ordenada O(n) insert, O(1) removeMin
│ └── Muchas operaciones (n > 100)
│ └── → Heap binario: PriorityQueue de Java
│ ├── Min-heap (predeterminado)
│ └── Max-heap: PriorityQueue<>((a,b) -> b - a)

└── Entorno concurrente (multi-hilo)
├── Cola FIFO → LinkedBlockingQueue
└── Priority Queue → PriorityBlockingQueue

7. Tabla de Referencia Rápida


Tipo de
Clase Java Política insert remove peek Cuándo usar
Cola

BFS, buffer de
Cola FIFO ArrayDeque / LinkedList FIFO O(1) O(1) O(1)
mensajes
Tipo de
Clase Java Política insert remove peek Cuándo usar
Cola

DFS, deshacer
Pila ArrayDeque (como stack) LIFO O(1) O(1) O(1)
acciones, parsing

Ventana
Deque ArrayDeque Ambos O(1) O(1) O(1) deslizante,
palíndromos

Dijkstra,
Priority Por O(log O(log
Queue
PriorityQueue clave n) n)
O(1) scheduling,
eventos

Productor-
Blocking
LinkedBlockingQueue FIFO O(1) O(1) O(1) consumidor multi-
Queue
hilo

Priority Por O(log O(log Scheduling


PriorityBlockingQueue O(1)
Blocking clave n) n) concurrente

8. Conclusión y Mejores Prácticas


1. Usa ArrayDeque sobre Stack y LinkedList para implementaciones de pila y cola FIFO en Java
moderno — es más eficiente en memoria y más rápida en caché.
2. Usa PriorityQueue con un comparador explícito para max-heaps o prioridades
personalizadas: new PriorityQueue<>((a, b) -> b - a).
3. La complejidad del heap es suficiente para la mayoría de los problemas prácticos
con hasta decenas de millones de elementos.
4. Antes de implementar, pregunta: ¿necesitas decreaseKey? Si sí, Java's PriorityQueue nativo
no lo soporta eficientemente — considera una implementación personalizada de heap de
Fibonacci.
5. Las colas no son solo almacenamiento — son decisiones de diseño que determinan la
corrección y eficiencia del algoritmo completo.

Documento generado el 8 de abril de 2026 · Basado en Goodrich, Tamassia & Goldwasser — Data
Structures & Algorithms in Java, 6ª edición

También podría gustarte