DEV Community

Cover image for Estructuras de datos: ¿Qué son los Stacks (LIFO)?
Axel Espinosa for AWS

Posted on

Estructuras de datos: ¿Qué son los Stacks (LIFO)?

¿Alguna vez te has preguntado qué pasa cuando presionas el botón "atrás" en tu navegador? ¿O cómo Ctrl + Z sabe exactamente qué acción deshacer? Detrás de esas funciones hay una estructura de datos que llevas años usando sin darte cuenta: el stack.

En los artículos anteriores vimos los arrays y los strings. Hoy toca hablar de la siguiente pieza: los stacks. Si los arrays son el almacén general, el stack es una pila de platos. Ya lo vas a ver.

Todo esto que ya usas es un stack: botón atrás, Ctrl+Z y stack traces

Lo que encontrarás en este artículo:

  • Qué es un stack y qué significa LIFO
  • Las operaciones básicas: push, pop, peek, size, isEmpty
  • Cómo implementar tu propio stack en JavaScript
  • La complejidad Big O de cada operación
  • Dónde se usan los stacks en la vida real

¿Qué es un stack?

Un stack (o pila) es una estructura de datos que sigue el principio LIFO: Last In, First Out. En español: el último que entra es el primero que sale.

Imagina una pila de libros sobre tu escritorio. Vas apilándolos uno sobre otro. Cuando quieres leer uno, ¿por cuál empiezas? Por el de arriba, no por el de abajo. El último libro que pusiste es el primero que vas a tomar.

Esa es la esencia de un stack. No puedes sacar un elemento del medio o del fondo. Solo trabajas con el que está hasta arriba.

LIFO: Last In, First Out

Pero aquí viene el detalle interesante: aunque suene restrictivo, esta limitación es justo lo que hace al stack útil. Cuando solo te importa el último elemento agregado, un stack es la herramienta perfecta.

Operaciones básicas de un stack

Un stack tiene cinco operaciones fundamentales. Estas son las que aparecen en el ADT (Abstract Data Type) de un stack:

  • push(item): agrega un elemento a la parte superior
  • pop(): elimina el elemento de arriba y lo devuelve
  • peek(): mira el elemento de arriba sin eliminarlo
  • size(): devuelve cuántos elementos hay en el stack
  • isEmpty(): dice si el stack está vacío o no

Las dos estrellas son push y pop. Con ellas agregas y quitas elementos, siempre desde la misma puerta: la de arriba.

push y pop paso a paso

Fíjate en cómo pop() siempre saca el último que entró. En el paso 3 agregamos 🐱, y en el paso 4 pop() nos devuelve 🐱. Si hubiéramos hecho pop() otra vez, nos devolvería 🐶.

Cómo implementar un stack en JavaScript

Dato curioso antes de empezar: los arrays en JavaScript ya se comportan como un stack. Si haces miArray.push(x) y miArray.pop(), estás trabajando como si fuera un stack..

Pero un array te deja hacer cosas que un stack no debería permitir: acceder por cualquier índice, unshift, splice, lo que quieras. Y esa libertad de más es justo lo que queremos esconder.

Así que vamos a implementarlo desde cero. Sin usar .push() ni .pop() del array. Solo lo vamos a usar como almacenamiento indexado y el control lo lleva nosotros. Así se ve qué pasa por debajo.

class Stack {
  constructor() {
    this.items = [];
    this.count = 0; // posición donde iría el próximo elemento
  }

  push(item) {
    this.items[this.count] = item;
    this.count++;
  }

  pop() {
    if (this.isEmpty()) {
      throw new Error("El stack está vacío");
    }
    this.count--;
    const item = this.items[this.count];
    this.items.length = this.count; // recortamos el array
    return item;
  }

  peek() {
    if (this.isEmpty()) {
      throw new Error("El stack está vacío");
    }
    return this.items[this.count - 1];
  }

  size() {
    return this.count;
  }

  isEmpty() {
    return this.count === 0;
  }
}
Enter fullscreen mode Exit fullscreen mode

Probemos cómo se usa:

const miStack = new Stack();

miStack.push("🐶");
miStack.push("🐱");
miStack.push("👻");

console.log(miStack.size()); // 3
console.log(miStack.peek()); // "👻" (solo mira, no elimina)
console.log(miStack.pop()); // "👻" (lo elimina y devuelve)
console.log(miStack.size()); // 2
console.log(miStack.isEmpty()); // false
Enter fullscreen mode Exit fullscreen mode

Así se ve el stack en cada paso:

Paso a paso de push, peek y pop sobre miStack

Puedes probar este código en RunJS o verlo paso a paso en PythonTutor, que también soporta JavaScript.

Vamos a desarmar lo que está pasando:

  • count lleva la cuenta de cuántos elementos hay. También indica la siguiente posición libre del array. Recuerda que los arrays empiezan en el índice 0, así que cuando el stack está vacío count vale 0 y ese es justo el lugar donde va a caer el primer elemento.
  • push() coloca el nuevo elemento en la posición count y luego incrementa el contador.
  • pop() hace lo contrario: decrementa count primero (ahora apunta al último elemento), lee ese valor y recorta el array.
  • peek() lee la posición count - 1 (el último elemento) sin mover nada.

Por ejemplo, si hacemos push("🐶"), push("🐱"), push("👻"), el array queda así:

Estado interno del stack

Cuando hacemos pop(), el contador baja de 3 a 2 y leemos items[2] que es 👻. Luego hacemos this.items.length = 2 para recortar el array, y listo.

Un truco de los arrays en JavaScript

Fíjate en esta línea: this.items.length = this.count. Podría parecer que solo estamos "cambiando un número", pero en realidad estamos modificando el array. En JavaScript, asignarle un valor menor a .length recorta el array y elimina los elementos que sobran.

Míralo por fuera de la clase:

const numbers = [1, 2, 3, 4, 5];

console.log(numbers.length); // 5

// Definir la longitud
numbers.length = 3;

console.log(numbers); // [1, 2, 3]
console.log(numbers.length); // 3
console.log(numbers[3]); // undefined (los elementos extra se eliminan)
Enter fullscreen mode Exit fullscreen mode

Este es un detalle que muchos desarrolladores desconocen y te abre posibilidades interesantes. En nuestra clase lo usamos para asegurarnos de que el array y el contador count siempre estén sincronizados.

Si te fijas, no usamos los métodos push y pop del array nativo de JavaScript. Solo lo usamos como almacenamiento indexado. Todo el control lo lleva nuestra variable count.

Complejidad Big O de las operaciones

Ya vimos qué hace cada operación. Ahora, ¿cuánto cuestan? Aquí es donde entra Big O, la notación que nos dice cómo crece el tiempo de una operación conforme crece el stack.

Operación Complejidad en tiempo Complejidad en espacio
push() O(1)* O(1)
pop() O(1) O(1)
peek() O(1) O(1)
size() O(1) O(1)
isEmpty() O(1) O(1)

* Amortizado. Ocasionalmente O(n) cuando el array dinámico necesita crecer por debajo.

Todas las operaciones de un stack son O(1). ¿Por qué? Porque siempre trabajamos con un solo elemento: el de arriba. No importa si el stack tiene 10 o 10 millones de elementos, push y pop tardan lo mismo.

Compara esto con un array, donde insertar al inicio es O(n) porque hay que mover todos los elementos. En un stack no tenemos ese problema porque solo tocamos la punta.

Esta es la razón principal por la que los stacks se usan tanto: son rápidos y predecibles. Si tu problema solo necesita trabajar con el último elemento agregado, un stack te da esa operación en tiempo constante.

Eso sí, rápido no quiere decir infinito. Un stack vive en memoria, así que si crece demasiado va a depender por completo de los recursos del dispositivo donde corra. En celulares, tablets y computadoras con poca RAM esto importa más de lo que crees: un stack que crece sin control puede tronar la app. Los lenguajes hasta tienen un límite específico para el call stack, y si lo rebasas te topas con el famoso stack overflow.

¿Dónde se usan los stacks en la vida real?

Antes de ver ejemplos, quédate con esto:

Regla de oro: cuando lo único que te importa es el último elemento, un stack es la respuesta.

Todo lo que viene a continuación es una variante del mismo patrón. Una vez que lo ves, empiezas a reconocer stacks en todos lados.

Navegador web: botón de "atrás"

Cada vez que visitas una página nueva, el navegador hace push() con la URL. Cuando presionas "atrás", hace pop() y te lleva a la página anterior.

Historial del navegador como stack

Funciona perfecto porque siempre quieres volver a la última página que visitaste, no a una del medio.

Ctrl + Z: deshacer acciones

Cuando escribes en un editor de texto, cada acción (escribir una letra, borrar, pegar) se agrega a un stack. Cuando presionas Ctrl + Z, la última acción se hace pop() y se deshace. Presionas Ctrl + Z otra vez y la penúltima se deshace.

Mismo patrón: siempre deshaces la acción más reciente.

Call stack de tu programa

Cuando una función llama a otra, que a su vez llama a otra, tu programa va apilando cada llamada en un "call stack". Cuando una función termina, se hace pop() y el control vuelve a la función anterior.

Por eso cuando ves un error con "stack trace" en la consola, estás viendo exactamente eso: el estado del stack de llamadas en el momento del error.

Validación de paréntesis balanceados

Un problema clásico de entrevistas. ¿Cómo verificas que en ((a + b) * (c - d)) todos los paréntesis estén correctamente balanceados? Con un stack. Cada ( hace push, cada ) hace pop. Si al final el stack está vacío, los paréntesis están balanceados.

Cierre

Los stacks son simples: cinco métodos, todos O(1), un solo principio (LIFO). Y justo por eso los encuentras en tantos lugares, desde el botón de atrás hasta el call stack de tu lenguaje de programación.

Si te sirvió el artículo, dale ❤️ y sígueme para no perderte el siguiente. Nos leemos pronto. 🙌🏻

Top comments (0)