1 - Introducción a las pilas (Stacks)

Visión general

Las pilas son estructuras de datos lineales que modelan el principio de apilar y desapilar como en la vida cotidiana: colocar elementos uno sobre otro y retirarlos en orden inverso. En C podemos verlas como un Tipo de Dato Abstracto (TDA) que se define por su comportamiento antes que por su implementación interna.

En este primer tema aclaramos de forma conceptual qué resuelven, por qué se asocian con el acrónimo LIFO (Last In, First Out) y cuándo conviene preferirlas frente a colas o listas. También revisamos ejemplos prácticos para conectar la teoría con situaciones cotidianas y con el código que construiremos en los temas siguientes.

1.1 ¿Qué es una pila?

Una pila es un contenedor donde las inserciones (push) y las extracciones (pop) ocurren en un único extremo denominado tope. No se recorren posiciones intermedias ni se permite acceder directamente a elementos alejados del tope, lo que simplifica su implementación y asegura complejidad constante en las operaciones principales.

En C podemos representarla mediante un array y un marcador del tope, como muestra el siguiente ejemplo minimalista:

#define TAM_MAX 128

typedef struct {
  int datos[TAM_MAX];
  int tope;
} Pila;

void inicializar(Pila *p) {
  p->tope = 0;
}

void push(Pila *p, int valor) {
  if (p->tope == TAM_MAX) return; /* pila llena */
  p->datos[p->tope++] = valor;
}

int pop(Pila *p) {
  if (p->tope == 0) return -1; /* pila vacia */
  return p->datos[--p->tope];
}

La estructura anterior muestra los componentes esenciales: almacenamiento, tope y funciones que controlan el flujo de datos. En implementaciones profesionales también se declaran funciones para consultar el elemento superior (peek) o para verificar si la pila está vacía.

Para compilarlo o probarlo en CLion basta con agregar una función main que ejecute una secuencia de operaciones controlada:

#include <stdio.h>

int main(void) {
  Pila pila;
  inicializar(&pila);

  push(&pila, 10);
  push(&pila, 20);
  push(&pila, 30);

  printf("Desapilado: %d\n", pop(&pila));
  printf("Desapilado: %d\n", pop(&pila));
  printf("Desapilado: %d\n", pop(&pila));

  return 0;
}

CLion o cualquier otro IDE solo necesita este main para generar un ejecutable de consola, y las impresiones permiten verificar cómo se cumple el orden LIFO.

Diagrama de ejecución de la pila en CLion
Vista de las operaciones push y pop en un proyecto CLion.

1.2 LIFO (Last In, First Out)

LIFO describe el orden de servicio de la pila: el último elemento insertado es el primero en salir. Ese comportamiento aparece en dominios como la ejecución de funciones (stack de llamadas), el manejo de expresiones algebraicas y la navegación por historiales.

  1. Suponiendo una pila vacía, apilamos los valores 3, 7 y 5 en ese orden.
  2. El tope queda apuntando a 5. Si invocamos pop, se obtiene 5 y el nuevo tope pasa a ser 7.
  3. Si insertamos otro valor (por ejemplo 9), quedará sobre el 7 y será el próximo en salir.

Como no hay desplazamiento de elementos internos, cada operación requiere un paso O(1), lo que vuelve a las pilas ideales para resolver problemas que necesitan revertir el orden de datos o mantener historiales acotados.

1.3 Comparación con colas y listas

Las pilas coexisten con otras estructuras lineales. La tabla siguiente resume las diferencias más relevantes:

Característica Pila Cola Lista enlazada
Extremo de inserción Un solo extremo (tope) Final de la cola Posiciones arbitrarias mediante nodos
Extremo de extracción El mismo tope Comienzo de la cola Depende de los enlaces definidos
Acceso aleatorio No permitido No permitido Se puede recorrer nodo por nodo
Complejidad básica O(1) push/pop O(1) enqueue/dequeue Inserción/eliminación O(1) si se conoce el nodo previo

Elegir una u otra estructura depende del orden deseado: pilas para revertir, colas para mantener orden cronológico y listas cuando se requiere insertar o eliminar en posiciones intermedias.

1.4 Ventajas y limitaciones

  • Ventajas: operaciones determinísticas y O(1); facilidad para verificar invariantes; implementaciones compactas sin depender de asignaciones dinámicas si se conoce el tamaño máximo.
  • Limitaciones: no permite buscar elementos internos ni reordenar sin desapilar; las implementaciones basadas en arrays requieren establecer un límite de capacidad o recurrir a realocaciones.
  • Implicancias en C: es necesario validar siempre los estados de pila llena o vacía para evitar lecturas fuera de rango, en especial cuando se utilizan punteros.

1.5 Casos cotidianos

Las pilas son intuitivas porque emulan procesos familiares:

  • Pila de platos: los platos recién lavados se apilan sobre los anteriores y se retiran desde arriba para evitar que los inferiores soporten más peso.
  • Funcionalidad deshacer (undo): cada acción se registra en una pila, permitiendo revertirla extrayendo el último estado.
  • Navegación web: los botones Atrás/Adelante gestionan dos pilas: historial y futuras visitas. Cada visita mueve elementos entre ambas.

Comprender estos paralelismos facilita visualizar la ejecución de algoritmos en C: cualquier flujo que requiera deshacer o volver sobre pasos previos puede traducirse al modelo de pilas con costos previsibles.