Pila, cola y lista enlazada
En resumen: Una pila devuelve primero el último elemento añadido (LIFO), una cola devuelve primero el primero añadido (FIFO), y una lista enlazada guarda cada elemento en un nodo con una flecha al siguiente.
Problema
«Deshacer» deshace lo que no es
Un editor de texto guarda las acciones en un array. Ctrl+Z debería deshacer la última, pero deshace la primera:
const actions = [];actions.push("Texto escrito");actions.push("Título en negrita");actions.push("Foto insertada");
// Ctrl+Z: deshacer la última acción.const undone = actions.shift();console.log(`Deshecho: ${undone}`);console.log("Queda:", actions);Deshecho: Texto escritoQueda: [ 'Título en negrita', 'Foto insertada' ]La corrección es actions.pop(). Pero todo el que toque este código tiene que recordar de qué extremo se quita. Y en la caja de la tienda es al revés: se atiende primero al que llegó primero. ¿Hay una colección que sepa sola de qué extremo?
La idea
Pila: lo último, arriba
Una pila (stack) es como una pila de platos. Pones arriba y sacas de arriba. Lo último que pusiste sale primero. En inglés esto se llama LIFO: last in, first out.
El botón «Atrás» del navegador es una pila:
const history = [];history.push("Inicio");history.push("Portátiles");history.push("Portátil de 14 pulgadas");console.log(`Ahora: ${history.at(-1)}`);
history.pop();console.log(`Atrás: ${history.at(-1)}`);console.log(history);Ahora: Portátil de 14 pulgadasAtrás: Portátiles[ 'Inicio', 'Portátiles' ]JavaScript no tiene una clase aparte para la pila: un array es una pila si pones y sacas solo por el final.
pushpone arriba,pop()quita y devuelve el de arriba, yat(-1)solo mira el de arriba.- Arriba es el final del array. Las dos operaciones son rápidas, porque al final no se mueve nada.
console.logimprime la pila de abajo arriba: el de arriba es el último.
Más precisamente, para curiosos
pop()yat(-1)en un array vacío no detienen el programa. Danundefined:
const history = [];console.log(history.pop());console.log(history.at(-1));undefinedundefinedAsí que comprueba history.length antes de sacar.
Pruébalo
¿Qué queda por deshacer?
¿Qué imprimirá el programa?
const undo = [];undo.push("fuente");undo.push("color");undo.push("tamaño");undo.pop();undo.push("subrayado");while (undo.length > 0) { console.log(undo.pop());}pop quita «tamaño», porque está arriba. Luego «subrayado» queda arriba. El bucle los saca de arriba abajo: subrayado, color, fuente.
La idea
Cola: el primero que llega
Una cola (queue) es como la fila de la caja. Te pones al final, y se atiende al primero. En inglés esto se llama FIFO: first in, first out.
Los trabajos de impresión esperan en una cola:
const printer = [];printer.push("factura.pdf");printer.push("contrato.docx");printer.push("foto.jpg");console.log(`Siguiente: ${printer[0]}`);
while (printer.length > 0) { console.log(`Imprimiendo ${printer.shift()}`);}console.log("Cola:", printer);Siguiente: factura.pdfImprimiendo factura.pdfImprimiendo contrato.docxImprimiendo foto.jpgCola: []JavaScript no tiene una clase aparte para la cola. Un array se convierte en cola si pones al final y sacas por delante:
pushpone al final,shiftquita y devuelve el primero, yprinter[0]mira el primero.shiften un array vacío daundefined. No detiene el programa.
Pero shift tiene un costo. Mueve todos los demás un lugar hacia delante, como al insertar al principio en la lección sobre arrays. Con pocos trabajos de impresión no lo notas. Con una cola de decenas de miles de elementos, es muchas veces más lento que solo leer en orden. Ahí se usa la lista enlazada de la tarjeta siguiente, que guarda tanto el primer nodo como el último.
Más precisamente, para curiosos
shiften una cola vacía:
const clients = ["Cliente 1"];console.log(`Atendido: ${clients.shift()}`);console.log(clients.shift());Atendido: Cliente 1undefined- El motor V8 a veces hace rápido
shiftmoviendo el inicio del array en lugar de mover los elementos. Con arrays grandes eso no pasa. No cuentes con ello.
Pruébalo
El siguiente en la caja
La cajera llama al siguiente cliente. ¿Qué método lo saca de la cola?
const checkout = [];
checkout.push("Carlos");
checkout.push("Lucía");
const client = checkout.();
console.log(`Tu turno, ${client}`);shift quita y devuelve el primero de la cola, aquí Carlos. checkout[0] solo miraría, sin quitarlo, y la próxima vez volvería a ser Carlos. Y pop() sacaría a la última, Lucía.
Míralo
Una lista enlazada por dentro
Un array guarda los elementos uno junto a otro en un array. Por eso insertar al principio mueve todos los demás. Hay otra forma: cada elemento está en un objeto aparte que guarda el valor y una flecha al siguiente. Un objeto así se llama nodo (node):
class Node { constructor(value, next) { this.value = value; this.next = next; }}Una lista de nodos se llama lista enlazada (linked list). Basta con guardar el primer nodo, head. Cada siguiente lo encuentras siguiendo la flecha next. El último apunta a null.
let head = null;head = new Node("Café", head);head = new Node("Leche", head);head = new Node("Pan", head);
for (let n = head; n !== null; n = n.next) { console.log(n.value);}Paso a paso, en palabras
- La lista está vacía: head apunta a null.
- Un nodo nuevo: next es el head anterior (null), y head pasa al nodo.
- El segundo nodo apunta al primero. Nada se mueve en la memoria: solo cambian las flechas.
- El tercer nodo también entra por delante: el último añadido es el primero.
- n empieza en head e imprime cada nodo.
- n llega a null: el bucle se detiene.
- Insertar al principio: un nodo nuevo cuyo
nextapunta al antiguo primero, yheadapunta al nuevo. Dos flechas, y no mueves nada. - Recorrer: empiezas en
heady siguesnexthasta llegar anull. - El costo: los nodos no tienen índices. Al quinto elemento llegas pasando por los cuatro primeros.
La idea
El array, y cuál usar cuándo
JavaScript no tiene una lista enlazada lista para usar. Un array puede añadir y quitar en los dos extremos: push y pop al final, unshift y shift al principio:
const route = [];route.push("Zaragoza");route.push("Lleida");route.unshift("Madrid");route.push("Barcelona");console.log(route);
route.splice(route.indexOf("Lleida"), 1);console.log("Después del cambio:", route);console.log(`De ${route[0]} a ${route.at(-1)}`);[ 'Madrid', 'Zaragoza', 'Lleida', 'Barcelona' ]Después del cambio: [ 'Madrid', 'Zaragoza', 'Barcelona' ]De Madrid a BarcelonaCuál usar cuándo:
- Una lista por número: un array. Es la elección habitual.
- Una pila: un array con
pushypop. - Una cola: un array con
pushyshift, mientras sea pequeña. Una cola grande, con miles de elementos: una lista enlazada como la de la tarjeta anterior, que también guarda el último nodo.
Más precisamente, para curiosos
unshiftes comoshift: mueve todos los elementos un lugar, así que es lento al principio de un array grande.- ¿Por qué un array suele ser más rápido que una lista enlazada, incluso al insertar? Cada nodo es un objeto aparte en algún lugar de la memoria, y el procesador lee más rápido los datos que están uno junto a otro. Un array es justo eso.
- El nodo de esta tarjeta solo tiene
next. Una lista así se llama simplemente enlazada.
Pruébalo
Encuentra el error
La función debería contar los nodos. Para la lista Pan → Leche → Café devuelve 2 en lugar de 3. Y con una lista vacía se detiene con TypeError. ¿Qué línea tiene el error?
Elige la línea con el error.
n.next !== null se detiene cuando n está en el último nodo, antes de contarlo. Y si head es null, no hay de dónde leer n.next. La condición debería ser n !== null: sigue mientras haya un nodo.
El mensaje con una lista vacía es TypeError: Cannot read properties of null (reading 'next'): null no tiene next.
Ejercicio
Pilas y colas en acción
# delante.1. Un tren de juguete. Una clase Train con los métodos addWagon(wagon) y removeWagon(). El tren es una pila: el último vagón enganchado es el primero que se desengancha. removeWagon() devuelve el vagón desenganchado, o null si no hay ninguno. Añade count(). Engancha tres vagones y desengancha dos.
Desenganchado: Vagón 3Desenganchado: Vagón 2Vagones: 12. Una línea de autobús. Una clase BusRoute con una cola de paradas y los métodos addStop(stop) y getNextStop(), que devuelve la próxima parada, o null si el recorrido terminó. Añade «Estación Central», «Plaza Mayor» y «Ciudad Universitaria», y recorre la línea.
Próxima parada: Estación CentralPróxima parada: Plaza MayorPróxima parada: Ciudad UniversitariaFin del recorrido.3. Un indicador de bolsa. Una clase StockTicker guarda los últimos N precios de una acción. El constructor recibe el símbolo y N. updatePrice(price) añade un precio, y si hay más de N precios, quita el más antiguo. Añade getLastPrice() y average() para los precios guardados. Para SAP con N = 3, pasa los precios 150, 152, 149 y 155. Pista: at(-1) es el último precio. Imprime los precios con dos decimales, con toFixed(2).
SAP: último precio 155.00 €Media de los últimos 3: 152.00 €4. Paréntesis equilibrados. Un editor de código comprueba si los paréntesis (), [] y {} se cierran en el orden correcto. Escribe isBalanced(text) con una pila: con un paréntesis que abre, lo pones en la pila, y con uno que cierra, quitas el de arriba y compruebas que sea del mismo tipo. Al final la pila tiene que estar vacía. Lee tres líneas.
(1 + [2 * 3])(1 + 2]((5)(1 + [2 * 3]) -> sí(1 + 2] -> no((5) -> no5. Tu propia lista enlazada. Una clase MyLinkedList con los nodos de la lección y los métodos add(value) (al final), remove(value), contains(value), size() y toString(). En remove, busca el nodo anterior al que buscas y redirige su next. Comprueba aparte el caso en que se quita el primero.
[Factura, Informe, Reunión]Contiene Informe: trueDespués de quitar: [Factura, Reunión]Tamaño: 2Más tareas
- Deshacer y rehacer. Guarda dos pilas:
undoyredo. Una acción nueva va aundoy vacíaredo. «Deshacer» pasa el de arriba deundoaredo, y «Rehacer» lo devuelve. - Palabras al revés. Lee una frase e imprímela con las palabras en orden inverso, pasando por una pila: «Hoy es viernes» se convierte en «viernes es Hoy».
- Dos cajas. Los clientes llegan uno a uno y van a la caja con la fila más corta. Imprime quién fue atendido en qué caja.
- Invierte la lista enlazada. Añade a
MyLinkedListun métodoreverse()que dé la vuelta a las flechas sin crear nodos nuevos.
- Una cola rápida. Añade a
MyLinkedListun campo para el último nodo, para queaddal final también sea rápido. Ahora es una cola sinshift:addal final, y quitar el primero por delante.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Polimorfismo · Interfaces sin interface · Excepciones · Arrays: push, splice y sort
- Continúa en: Códigos hash y funciones hash · Clases anidadas y datos · Command · Complejidad en espacio
- Ejercicios sobre el tema: Colecciones