Saltar al contenido
pilas-y-colas.java · devschool

Pilas y colas en Java

Lección 29 de 39 · 13 min de lectura · Actualizado el

En esta lección
  1. LIFO y FIFO: dos formas de hacer fila
  2. Las interfaces Queue y Deque
  3. Pilas con ArrayDeque
  4. Colas con ArrayDeque
  5. Deque: añadir y sacar por los dos extremos
  6. Por qué no usar la clase Stack
  7. PriorityQueue: colas con prioridad
  8. ¿Y LinkedList?
  9. Cuál elegir
  10. Errores frecuentes
  11. Resumen

Hay problemas en los que no te importa buscar un elemento concreto, sino en qué orden salen. El botón “Deshacer” de un editor recupera siempre lo último que hiciste; la cola de la carnicería atiende siempre a quien llegó primero. Para esos casos Java tiene dos estructuras clásicas: las pilas y las colas. En esta lección aprenderás qué son, cómo usarlas con ArrayDeque, por qué no conviene usar la vieja clase Stack y cuándo necesitas una cola con prioridad (PriorityQueue).

LIFO y FIFO: dos formas de hacer fila

Piensa en dos situaciones del día a día:

  • Una pila de platos en la cocina. Pones cada plato limpio encima del anterior y, cuando necesitas uno, coges el de arriba. El último que pusiste es el primero que sale. Esto se llama LIFO (Last In, First Out: el último en entrar es el primero en salir).
  • La cola del supermercado. El primero que llega es el primero al que atienden, y los nuevos se ponen al final. Esto se llama FIFO (First In, First Out: el primero en entrar es el primero en salir).
EstructuraOrdenSe añade por…Se saca por…Ejemplos reales
Pila (stack)LIFOArribaArribaDeshacer, botón “Atrás” del navegador, platos
Cola (queue)FIFOEl finalEl principioTurnos de una tienda, cola de impresión, mensajes pendientes

En las dos estructuras no accedes por posición: solo puedes mirar o sacar el elemento de un extremo. Esa limitación es justo lo que las hace útiles, porque el propio código deja claro en qué orden se procesan las cosas.

Las interfaces Queue y Deque

En el framework de colecciones las pilas y las colas se representan con dos interfaces del paquete java.util:

  • Queue<E>: una cola. Se añade por un extremo y se saca por el otro.
  • Deque<E> (se pronuncia “dek”, de double-ended queue): una cola de doble extremo, en la que puedes añadir y sacar por los dos lados. Por eso sirve tanto de cola como de pila.

La implementación recomendada para ambas es ArrayDeque: es rápida, gasta poca memoria y no tiene límite de tamaño. Como siempre, declara la variable con la interfaz:

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Queue;

Deque<String> pila = new ArrayDeque<>();   // la usarás como pila
Queue<String> cola = new ArrayDeque<>();   // la usarás como cola

Pilas con ArrayDeque

Una pila tiene tres operaciones básicas:

MétodoQué haceSi la pila está vacía
push(x)Pone x arriba—
pop()Quita y devuelve el de arribaLanza NoSuchElementException
peek()Devuelve el de arriba sin quitarloDevuelve null

Vamos con la pila de platos:

Deque<String> platos = new ArrayDeque<>();
platos.push("plato azul");
platos.push("plato rojo");
platos.push("plato verde");

System.out.println(platos);        // [plato verde, plato rojo, plato azul]
System.out.println(platos.peek()); // plato verde
System.out.println(platos.pop());  // plato verde
System.out.println(platos.pop());  // plato rojo
System.out.println(platos);        // [plato azul]
System.out.println(platos.size()); // 1

Fíjate en que println muestra la pila empezando por arriba: el primer elemento que ves es el que saldrá con el próximo pop.

Lo habitual es vaciar una pila con un while que comprueba isEmpty(). Así funciona, simplificado, el botón “Atrás” de un navegador:

Deque<String> historial = new ArrayDeque<>();
historial.push("inicio");
historial.push("productos");
historial.push("carrito");

while (!historial.isEmpty()) {
    System.out.println("Volver desde: " + historial.pop());
}
// Volver desde: carrito
// Volver desde: productos
// Volver desde: inicio

System.out.println(historial.peek()); // null

Ejemplo: deshacer en un editor de texto

Cada vez que el usuario escribe algo, guardamos ese trozo en una pila. Al pulsar “Deshacer”, sacamos el último trozo y lo borramos del texto:

StringBuilder texto = new StringBuilder();
Deque<String> deshacer = new ArrayDeque<>();

String[] escrituras = {"Hola", ", ", "mundo", "!!!"};
for (String trozo : escrituras) {
    texto.append(trozo);
    deshacer.push(trozo);
}
System.out.println(texto); // Hola, mundo!!!

for (int i = 0; i < 2; i++) {          // el usuario pulsa Deshacer dos veces
    String ultimo = deshacer.pop();
    texto.setLength(texto.length() - ultimo.length());
    System.out.println("Deshecho \"" + ultimo + "\" -> " + texto);
}
// Deshecho "!!!" -> Hola, mundo
// Deshecho "mundo" -> Hola,

setLength recorta el StringBuilder (lo viste en cadenas). Un editor real guardaría acciones más complejas (borrar, pegar, cambiar formato), pero la idea es la misma: lo último que hiciste es lo primero que se deshace, y eso es una pila.

Ejemplo: paréntesis equilibrados

Un problema clásico de examen y de entrevista: comprobar si en un texto los paréntesis, corchetes y llaves están bien abiertos y cerrados. El compilador de Java hace algo parecido con tu código.

La idea: recorremos el texto carácter a carácter. Cada vez que algo se abre, lo apilamos. Cada vez que algo se cierra, tiene que coincidir con lo último que se abrió, que está arriba de la pila.

static boolean equilibrado(String texto) {
    Deque<Character> pila = new ArrayDeque<>();
    for (char c : texto.toCharArray()) {
        switch (c) {
            case '(', '[', '{' -> pila.push(c);
            case ')', ']', '}' -> {
                if (pila.isEmpty()) {
                    return false;               // cierra algo que nunca se abrió
                }
                char abierto = pila.pop();
                if ((c == ')' && abierto != '(')
                        || (c == ']' && abierto != '[')
                        || (c == '}' && abierto != '{')) {
                    return false;               // no casan: ( con ] por ejemplo
                }
            }
            default -> { }                      // el resto de caracteres no importa
        }
    }
    return pila.isEmpty();                      // si queda algo, faltan cierres
}
System.out.println(equilibrado("(a + b) * [c - d]"));  // true
System.out.println(equilibrado("{ lista[(i + 1)] }")); // true
System.out.println(equilibrado("(a + b]"));            // false
System.out.println(equilibrado("((x)"));               // false
System.out.println(equilibrado("x)("));                // false

Hay tres formas de fallar y el método comprueba las tres: un cierre sin apertura (pila vacía), un cierre que no casa con su apertura, y aperturas que se quedan sin cerrar (la pila no termina vacía). La pila es de Character y no de char porque las colecciones solo guardan objetos; Java convierte entre ambos automáticamente.

Colas con ArrayDeque

Para una cola FIFO se usan estos métodos de Queue:

MétodoQué haceSi la cola está vacía
offer(x)Añade x al final—
poll()Quita y devuelve el primeroDevuelve null
peek()Devuelve el primero sin quitarloDevuelve null

Simulemos los turnos del mostrador de una tienda:

Queue<String> turnos = new ArrayDeque<>();
turnos.offer("Carmen");
turnos.offer("Iván");
turnos.offer("Rosa");
System.out.println(turnos);                         // [Carmen, Iván, Rosa]

System.out.println("Siguiente: " + turnos.peek());  // Siguiente: Carmen
System.out.println("Atendiendo a " + turnos.poll()); // Atendiendo a Carmen

turnos.offer("Óscar");                              // llega alguien nuevo: al final
System.out.println(turnos);                         // [Iván, Rosa, Óscar]

while (!turnos.isEmpty()) {
    System.out.println("Atendiendo a " + turnos.poll());
}
// Atendiendo a Iván
// Atendiendo a Rosa
// Atendiendo a Óscar

System.out.println(turnos.poll());                  // null

Óscar llegó después que Iván y Rosa, así que se le atiende después aunque se añadiera a mitad del proceso. Eso es lo que garantiza una cola.

Dos familias de métodos: excepción o valor especial

Queue tiene cada operación repetida en dos versiones. Se diferencian en qué hacen cuando no pueden cumplir su trabajo:

OperaciónLanza excepciónDevuelve valor especial
Añadiradd(x)offer(x) → false
Sacarremove()poll() → null
Consultarelement()peek() → null
Queue<String> vacia = new ArrayDeque<>();
System.out.println(vacia.poll()); // null
System.out.println(vacia.peek()); // null
vacia.remove();                   // NoSuchElementException

Con ArrayDeque, add y offer siempre funcionan porque no tiene límite. La diferencia importa en colas con capacidad máxima, como las que se usan para repartir trabajo entre hilos.

Consejo: usa offer, poll y peek y comprueba el null o isEmpty(). Es más claro que capturar excepciones. Para la pila, pop() lanza excepción si está vacía, así que comprueba isEmpty() antes.

Deque: añadir y sacar por los dos extremos

Como ArrayDeque es de doble extremo, tiene versiones “First” y “Last” de cada método:

Al principioAl final
offerFirst(x)offerLast(x)
pollFirst()pollLast()
peekFirst()peekLast()

De hecho, push, pop y peek de la pila son atajos de offerFirst, pollFirst y peekFirst; y offer/poll de la cola son offerLast/pollFirst. Un ejemplo en el que hace falta usar los dos extremos: en la cola de una consulta, quien tiene cita previa pasa delante, y quien se cansa de esperar se va por el final.

Deque<String> fila = new ArrayDeque<>();
fila.offerLast("Ana");
fila.offerLast("Bruno");
fila.offerFirst("Clara (cita previa)");
System.out.println(fila);            // [Clara (cita previa), Ana, Bruno]

System.out.println(fila.pollLast()); // Bruno  (se cansa y se va)
System.out.println(fila.peekFirst() + " / " + fila.peekLast()); // Clara (cita previa) / Ana

Cuidado: no mezcles los dos estilos con la misma variable. Si una Deque es tu pila, usa solo push/pop/peek; si es tu cola, solo offer/poll/peek. Mezclarlos produce órdenes muy difíciles de seguir.

Por qué no usar la clase Stack

En apuntes y código antiguo verás la clase Stack. Funciona, pero hoy no se recomienda, y la propia documentación de Java aconseja ArrayDeque en su lugar. Los motivos:

  • Hereda de Vector, una lista antigua. Eso significa que tiene métodos como get(i) o add(posicion, x) que permiten saltarse el orden de la pila, justo lo que una pila debería impedir.
  • Es más lenta, porque todos sus métodos están sincronizados (preparados para varios hilos a la vez) aunque no lo necesites.
  • Su orden al recorrerla o imprimirla es el contrario al de ArrayDeque: empieza por abajo.
Stack<String> vieja = new Stack<>();
vieja.push("a"); vieja.push("b"); vieja.push("c");

Deque<String> nueva = new ArrayDeque<>();
nueva.push("a"); nueva.push("b"); nueva.push("c");

System.out.println(vieja);       // [a, b, c]  (de abajo arriba)
System.out.println(nueva);       // [c, b, a]  (de arriba abajo)

System.out.println(vieja.get(0)); // a          (acceso por posición: rompe la idea de pila)
vieja.add(0, "intruso");
System.out.println(vieja);       // [intruso, a, b, c]

Las operaciones push, pop y peek se llaman igual en las dos, así que cambiar código antiguo a ArrayDeque suele ser tan sencillo como cambiar la declaración.

PriorityQueue: colas con prioridad

En urgencias de un hospital no se atiende por orden de llegada, sino por gravedad. Para eso existe PriorityQueue, una cola en la que poll() saca siempre el elemento más prioritario, en lugar del más antiguo.

Por defecto, el más prioritario es el menor según el orden natural:

PriorityQueue<Integer> numeros = new PriorityQueue<>(List.of(42, 7, 19, 3, 25));
while (!numeros.isEmpty()) {
    System.out.print(numeros.poll() + " ");
}
// 3 7 19 25 42

Con objetos propios hay que decirle cómo compararlos pasándole un Comparator al crearla. Aquí queremos que salga primero el de mayor gravedad:

record Paciente(String nombre, int gravedad) {}

PriorityQueue<Paciente> urgencias = new PriorityQueue<>(
        Comparator.comparingInt(Paciente::gravedad).reversed());
urgencias.offer(new Paciente("Lola", 2));
urgencias.offer(new Paciente("Tomás", 5));
urgencias.offer(new Paciente("Nerea", 1));
urgencias.offer(new Paciente("Javi", 4));

System.out.println(urgencias.peek().nombre()); // Tomás

while (!urgencias.isEmpty()) {
    Paciente p = urgencias.poll();
    System.out.println(p.nombre() + " (gravedad " + p.gravedad() + ")");
}
// Tomás (gravedad 5)
// Javi (gravedad 4)
// Lola (gravedad 2)
// Nerea (gravedad 1)

Los comparadores (comparingInt, reversed, thenComparing…) se explican a fondo en Comparable y Comparator.

Cuidado: una PriorityQueue no está ordenada por dentro. Solo garantiza que peek() y poll() dan el más prioritario. Si la imprimes o la recorres con for-each, el orden parece aleatorio: System.out.println(new PriorityQueue<>(List.of(42, 7, 19, 3, 25))) muestra [3, 7, 19, 42, 25]. Para procesarla en orden, sácalos con poll().

¿Y LinkedList?

LinkedList implementa a la vez List y Deque, así que puede hacer de lista, de pila y de cola. La verás a menudo en ejemplos como Queue<String> cola = new LinkedList<>();. Diferencias con ArrayDeque:

ArrayDequeLinkedList
Velocidad como pila/colaMás rápidaMás lenta
MemoriaMenosMás (cada elemento guarda enlaces al anterior y al siguiente)
Acceso por posición get(i)NoSí (pero lento)
Admite nullNo (NullPointerException)Sí

Elige ArrayDeque para pilas y colas. LinkedList solo tiene sentido si además necesitas que sea una List o guardar null, cosa que conviene evitar porque poll() y peek() usan null para decir “vacía”.

Cuál elegir

Necesitas…Usa
Una pila (LIFO)Deque<T> p = new ArrayDeque<>(); con push, pop, peek
Una cola (FIFO)Queue<T> c = new ArrayDeque<>(); con offer, poll, peek
Añadir y sacar por los dos ladosDeque<T> con offerFirst, pollLast…
Atender por prioridadPriorityQueue<T> con un Comparator
Buscar por posiciónUna lista, no una pila ni una cola

Errores frecuentes

  • Usar Stack en código nuevo. Funciona, pero es antigua, lenta y permite saltarse el orden. Usa ArrayDeque.
  • Llamar a pop() sobre una pila vacía. Lanza NoSuchElementException. Comprueba antes isEmpty().
  • Confundir pop y peek. peek solo mira; si lo usas en un while (!pila.isEmpty()) en lugar de pop, el bucle no termina nunca.
  • Mezclar métodos de pila y de cola en la misma Deque. Los elementos acaban saliendo en un orden que nadie espera.
  • Esperar que una PriorityQueue impresa salga ordenada. Solo poll() respeta la prioridad.
  • Guardar null en un ArrayDeque. Lanza NullPointerException.
  • Usar Deque<char> o Queue<int>. No compila: son Deque<Character> y Queue<Integer>.

Resumen

ConceptoCódigo
Crear pilaDeque<String> p = new ArrayDeque<>();
Apilar / desapilar / mirarp.push(x), p.pop(), p.peek()
Crear colaQueue<String> c = new ArrayDeque<>();
Encolar / sacar / mirarc.offer(x), c.poll(), c.peek()
¿Está vacía?isEmpty(), size()
Doble extremoofferFirst, offerLast, pollFirst, pollLast
Cola con prioridadnew PriorityQueue<>(comparador)
LIFOPila: el último en entrar sale primero
FIFOCola: el primero en entrar sale primero

En la siguiente lección aprenderás a escribir tus propias clases y métodos que funcionan con cualquier tipo, como hacen ArrayDeque<E> o List<E>: genéricos.

Pon a prueba lo que has aprendido

[Java] ¿Qué imprime este código?
Deque<String> p = new ArrayDeque<>();
p.push("a");
p.push("b");
p.push("c");
p.pop();
System.out.println(p);

[Java] ¿Qué imprime este código?
Queue<String> q = new ArrayDeque<>();
q.offer("x");
q.offer("y");
q.offer("z");
q.poll();
q.offer("w");
System.out.println(q);

[Java] ¿Qué imprime este código?
System.out.println(new PriorityQueue<>(List.of(42, 7, 19, 3, 25)));

[Java] Se apilan "a", "b" y "c" con push en un Stack y en un ArrayDeque. ¿Cómo se imprimen respectivamente?

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.