Lic. Carlos Enrique Loria Beeche.
Tercer artículo técnico de la saga “Estructuras de datos: cuando la memoria se convierte en inteligencia”
Este artículo forma parte de la saga que inicié con Estructuras de datos: cuando la memoria se convierte en inteligencia, donde presenté las ocho estructuras de datos fundamentales que iremos recorriendo paso a paso.
En las entregas anteriores vimos dos formas muy distintas de organizar información. Primero estudiamos el array, el estacionamiento numerado de la memoria, donde cada elemento se ubica mediante un índice. Luego vimos la linked list, la búsqueda del tesoro en la memoria, donde cada nodo conoce el camino hacia el siguiente.
Ahora damos un paso más y estudiamos una estructura muy sencilla en apariencia, pero profundamente importante: la stack, o pila.
La pila aparece en acciones tan comunes como deshacer cambios con Control Z, regresar de una función, manejar llamadas recursivas o conservar temporalmente información mientras un programa resuelve una tarea.
Una pila nos enseña que el orden de regreso importa: muchas veces, para avanzar correctamente, primero hay que resolver lo último que quedó pendiente.
¿Qué es una stack?
Una stack, o pila, es una estructura de datos que organiza sus elementos bajo una regla muy sencilla:
El último elemento en entrar es el primero en salir.
En inglés, esta regla se conoce como LIFO: Last In, First Out.
La analogía clásica es una pila de platos. Si colocamos varios platos uno encima de otro, el último plato que pusimos arriba será el primero que podremos quitar. No podemos sacar cómodamente el plato del fondo sin mover antes los que están encima.
En una pila ocurre lo mismo. Los elementos se agregan por arriba y también se retiran por arriba.
TOPE
|
v
+-----+
| D | <- último en entrar, primero en salir
+-----+
| C |
+-----+
| B |
+-----+
| A |
+-----+
BASE
En este ejemplo, D está en el tope de la pila. Si sacamos un elemento, saldrá D. Después saldrá C, luego B y finalmente A.
La regla LIFO
La regla LIFO es el corazón de la pila.
Si insertamos los elementos en este orden:
A, B, C, D
La pila queda así:
D
C
B
A
Y si empezamos a sacar elementos, el orden de salida será:
D, C, B, A
Es decir, el último elemento que entró es el primero que sale.
La pila invierte el orden de salida respecto al orden de entrada.
Operaciones básicas: push, pop y peek
Las pilas suelen tener tres operaciones fundamentales:
- push: agregar un elemento al tope de la pila.
- pop: retirar el elemento que está en el tope.
- peek: consultar el elemento del tope sin retirarlo.
Push: agregar al tope
La operación push inserta un nuevo elemento arriba de la pila.
Pila inicial:
C
B
A
push(D)
Resultado:
D
C
B
A
El nuevo elemento D queda colocado en el tope.
Pop: retirar del tope
La operación pop elimina y devuelve el elemento superior.
Pila inicial:
D
C
B
A
pop()
Resultado:
C
B
A
Elemento retirado: D
No se retira cualquier elemento. Siempre se retira el que está arriba.
Peek: mirar sin quitar
La operación peek permite consultar el elemento superior sin modificar la pila.
Pila:
D
C
B
A
peek() devuelve: D
La pila sigue igual:
D
C
B
A
Esta operación es útil cuando necesitamos saber cuál es el próximo elemento disponible, pero todavía no queremos retirarlo.
Control Z: una pila cotidiana
Uno de los ejemplos más fáciles de entender es el famoso Control Z.
Cuando escribimos en un editor de texto, cada acción puede guardarse en una pila de acciones realizadas:
Escribir "Hola"
Poner negrita
Cambiar color
Borrar una palabra
Si presionamos Control Z, normalmente se deshace primero la última acción:
1. Se deshace: Borrar una palabra
2. Se deshace: Cambiar color
3. Se deshace: Poner negrita
4. Se deshace: Escribir "Hola"
Esto es exactamente una pila: lo último que hicimos es lo primero que podemos deshacer.
Control Z funciona porque el programa recuerda las acciones en orden inverso: la última acción queda arriba de la pila.
La pila de llamadas
La pila también aparece en un lugar fundamental de la ejecución de programas: la pila de llamadas, conocida en inglés como call stack.
Cuando una función llama a otra función, el programa necesita recordar dónde debe continuar cuando esa función termine. Si esa función llama a otra, y esa otra llama a otra más, el sistema debe conservar varios puntos de retorno.
Podemos imaginarlo así:
main()
llama a funcionA()
llama a funcionB()
llama a funcionC()
La ejecución entra en funcionC, pero cuando esta termina debe volver a funcionB. Luego funcionB vuelve a funcionA. Finalmente funcionA vuelve a main.
El orden de regreso es inverso al orden de llamada:
Entrada: main -> funcionA -> funcionB -> funcionC
Salida: funcionC -> funcionB -> funcionA -> main
Eso es comportamiento de pila.
Este recuerdo conecta naturalmente con el lenguaje ensamblador y con instrucciones como BALR 14,15 en IBM System/360, donde el programa debía conservar una dirección de retorno para poder continuar después de ejecutar una subrutina.
En lenguajes modernos, muchos detalles se ocultan, pero la idea permanece: cuando una función llama a otra, el programa necesita conservar contexto, dirección de retorno, variables locales y estado temporal.
Stack y recursividad
La recursividad también depende de la idea de pila.
Una función recursiva es una función que se llama a sí misma. Cada llamada necesita conservar su propio contexto hasta que pueda resolverse.
Veamos el ejemplo clásico del factorial:
factorial(4)
necesita factorial(3)
necesita factorial(2)
necesita factorial(1)
Cuando llegamos al caso base, las llamadas empiezan a resolverse en orden inverso:
factorial(1) devuelve 1
factorial(2) devuelve 2 * 1
factorial(3) devuelve 3 * 2
factorial(4) devuelve 4 * 6
Cada llamada pendiente queda, conceptualmente, en una pila.
La recursividad se entiende mejor cuando recordamos que cada llamada queda esperando su turno de regreso.
Operaciones básicas de una stack
| Operación | Descripción | Costo típico |
|---|---|---|
| push | Agregar un elemento al tope de la pila | O(1) |
| pop | Retirar el elemento del tope | O(1) |
| peek | Consultar el elemento del tope sin retirarlo | O(1) |
| isEmpty | Verificar si la pila está vacía | O(1) |
| recorrer | Visitar todos los elementos | O(n) |
La pila es eficiente porque sus operaciones principales ocurren en un solo extremo: el tope.
Ejemplo en C++
En C++, podemos usar la clase estándar stack, incluida en la biblioteca estándar.
#include <iostream>
#include <stack>
using namespace std;
int main() {
stack<int> pila;
pila.push(10);
pila.push(20);
pila.push(30);
cout << "Elemento en el tope: " << pila.top() << endl;
pila.pop();
cout << "Nuevo elemento en el tope: " << pila.top() << endl;
while (!pila.empty()) {
cout << "Sacando: " << pila.top() << endl;
pila.pop();
}
return 0;
}
En este ejemplo, insertamos tres elementos. El último insertado fue 30, por eso aparece primero en el tope. Después de hacer pop, el nuevo tope es 20.
Ejemplo en C#
En C#, podemos usar la clase genérica Stack<T>.
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
Stack<int> pila = new Stack<int>();
pila.Push(10);
pila.Push(20);
pila.Push(30);
Console.WriteLine("Elemento en el tope: " + pila.Peek());
int retirado = pila.Pop();
Console.WriteLine("Elemento retirado: " + retirado);
Console.WriteLine("Nuevo elemento en el tope: " + pila.Peek());
while (pila.Count > 0)
{
Console.WriteLine("Sacando: " + pila.Pop());
}
}
}
En este caso, Push agrega elementos, Pop retira el elemento superior y Peek permite consultar el tope sin quitarlo.
Ejemplo en Python
En Python, una lista puede utilizarse como pila usando los métodos append y pop.
pila = []
pila.append(10)
pila.append(20)
pila.append(30)
print("Elemento en el tope:", pila[-1])
retirado = pila.pop()
print("Elemento retirado:", retirado)
print("Nuevo elemento en el tope:", pila[-1])
while len(pila) > 0:
print("Sacando:", pila.pop())
El método append agrega al final de la lista, que usamos como tope de la pila. El método pop retira el último elemento, cumpliendo la regla LIFO.
¿Cuándo conviene usar una stack?
Una pila conviene cuando necesitamos manejar elementos en orden inverso al que llegaron.
Algunos casos típicos son:
- Deshacer acciones, como en Control Z.
- Manejar llamadas de funciones.
- Implementar recursividad.
- Evaluar expresiones matemáticas.
- Verificar paréntesis balanceados.
- Navegar hacia atrás en historiales.
- Procesar algoritmos de búsqueda en profundidad.
La pila es útil cuando el último elemento pendiente debe resolverse primero.
Aplicación clásica: paréntesis balanceados
Un uso muy conocido de las pilas es verificar si los paréntesis, llaves o corchetes están balanceados.
Por ejemplo, esta expresión está balanceada:
{ [ ( ) ] }
Pero esta no lo está:
{ [ ( ] ) }
La idea es usar una pila para guardar los símbolos de apertura. Cuando aparece un símbolo de cierre, se compara con el último símbolo abierto. Si coincide, se retira. Si no coincide, hay error.
Esto funciona porque el último símbolo abierto debe ser el primero en cerrarse.
La pila permite comprobar estructuras anidadas porque respeta exactamente el orden de cierre esperado.
La lección de la pila
La pila es una estructura sencilla, pero revela una idea profunda: no siempre procesamos los datos en el mismo orden en que llegan.
A veces necesitamos regresar por el camino inverso. A veces lo último que ocurrió es lo primero que debemos atender. A veces una operación queda pendiente hasta que otra termine.
Por eso la pila aparece en editores, compiladores, navegadores, sistemas operativos, lenguajes de programación, algoritmos y ejecución de funciones.
El array nos enseñó el poder del índice. La lista enlazada nos enseñó el valor del enlace. La pila nos enseña el valor del retorno.
La pila nos recuerda que programar bien no solo consiste en avanzar, sino también en saber regresar ordenadamente.
Próxima entrega
En el próximo artículo de esta saga estudiaremos la queue, o cola. Allí veremos una estructura que funciona con una lógica distinta: el primero en entrar es el primero en salir.
Pasaremos de la pila de platos y el Control Z a la fila del banco, la cola de impresión y el procesamiento ordenado de tareas.
Referencias para profundizar
- Carlos Enrique Loría Beeche — Estructuras de datos: cuando la memoria se convierte en inteligencia
- Carlos Enrique Loría Beeche — Array: el estacionamiento numerado de la memoria
- Carlos Enrique Loría Beeche — Linked List: la búsqueda del tesoro en la memoria
- Adolfo Di Mare Hering — Tres formas diferentes de explicar la recursividad
