En la lección anterior conociste el contrato del TDA pila: push, pop, peek, esta_vacia y tamano, todas con coste O(1). Ahora vamos a abrir cada operación y verla funcionar paso a paso, con trazas del estado de la pila tras cada movimiento, exactamente como harías al depurar. Además tomaremos una decisión de diseño que todo autor de estructuras debe afrontar (¿qué hacer cuando alguien hace pop sobre una pila vacía?), formularemos los invariantes que toda pila debe cumplir, y aplicaremos todo esto al deshacer de TaskFlow. Para practicar sin esperar a la implementación completa (lección 03-03), usaremos la list de Python como pila provisional. Dominar la mecánica fina de estas operaciones es lo que te permitirá después implementarlas y, sobre todo, razonar con seguridad sobre cualquier algoritmo que use pilas.
Contenido
push: apilar paso a pasopop: desapilar paso a pasopeek: mirar sin tocaresta_vaciaytamano: las consultas de estado- El caso de la pila vacía: excepción vs
None - Invariantes del TDA pila
- Secuencias de operaciones: el deshacer de TaskFlow
- Mini-práctica guiada:
listcomo pila provisional
push: apilar paso a paso
push(elemento) coloca elemento en la cima. Es la única forma de entrar en una pila. Veamos una traza partiendo de una pila vacía, apilando acciones del historial de TaskFlow (dibujamos la pila en vertical, con la cima arriba, como crece de verdad):
| Operación | Estado de la pila (cima arriba) | tamano() |
|---|---|---|
| (inicio) | (vacía) | 0 |
push("crear tarea 7") |
crear tarea 7 |
1 |
push("cambiar prioridad") |
cambiar prioridad ← cimacrear tarea 7 |
2 |
push("marcar en_curso") |
marcar en_curso ← cimacambiar prioridadcrear tarea 7 |
3 |
Puntos clave:
- Cada
pushtapa al elemento anterior:crear tarea 7sigue ahí, pero ya no es accesible hasta que se retiren los de encima. pushnunca falla por estado de la pila: una pila (conceptual) no tiene límite de capacidad. (Existen pilas acotadas —lo veremos en un ejercicio de 03-05 con el límite de historial— pero no forman parte del contrato base.)pushno devuelve nada: su efecto es el cambio de estado.
En diagrama, los dos primeros pasos:
flowchart LR
subgraph P1["Tras push(crear tarea 7)"]
direction TB
a1["crear tarea 7 ← cima"]
end
subgraph P2["Tras push(cambiar prioridad)"]
direction TB
b2["cambiar prioridad ← cima"]
b1["crear tarea 7"]
b2 --- b1
end
P1 -->|"push(cambiar prioridad)"| P2
pop: desapilar paso a paso
pop() hace dos cosas en una: retira el elemento de la cima y lo devuelve. Continuemos la traza anterior (la pila tenía 3 elementos):
| Operación | Devuelve | Estado de la pila (cima arriba) | tamano() |
|---|---|---|---|
| (estado previo) | — | marcar en_cursocambiar prioridadcrear tarea 7 |
3 |
pop() |
"marcar en_curso" |
cambiar prioridadcrear tarea 7 |
2 |
pop() |
"cambiar prioridad" |
crear tarea 7 |
1 |
pop() |
"crear tarea 7" |
(vacía) | 0 |
pop() |
¿? | (vacía) | 0 |
Observa dos cosas:
- Los elementos salen en orden inverso al de entrada: entraron 7→prioridad→en_curso y salieron en_curso→prioridad→7. Esa es la esencia LIFO, y es exactamente el orden que necesita un deshacer.
- El cuarto
pop()es un problema: la pila está vacía y no hay nada que devolver. Ese "¿?" merece su propia sección (la número 5).
Un patrón habitual es vaciar una pila procesando cada elemento:
Este bucle procesa los elementos en orden inverso al de inserción. Memoriza el patrón: lo usaremos una y otra vez (deshacer todo, invertir secuencias, evaluar expresiones...).
peek: mirar sin tocar
peek() devuelve el elemento de la cima sin retirarlo. La pila queda exactamente igual:
| Operación | Devuelve | Estado tras la operación |
|---|---|---|
| (estado previo) | — | cambiar prioridadcrear tarea 7 |
peek() |
"cambiar prioridad" |
cambiar prioridadcrear tarea 7 (idéntico) |
peek() |
"cambiar prioridad" |
(idéntico: peek es repetible) |
pop() |
"cambiar prioridad" |
crear tarea 7 |
¿Para qué sirve si pop ya devuelve la cima? Para decidir antes de actuar. Dos usos típicos en TaskFlow:
- La interfaz quiere mostrar "Deshacer: cambiar prioridad" en el botón. Necesita leer la próxima acción sin deshacerla:
peek. - Un algoritmo quiere desapilar solo si la cima cumple una condición (lo verás en la conversión de expresiones de 03-04): primero
peek, se comprueba la condición, y solo entoncespop.
Regla de oro: si tras llamar a una operación de consulta la pila cambió, esa operación está mal implementada. peek, esta_vacia y tamano son observadoras; push y pop son modificadoras.
esta_vacia y tamano: las consultas de estado
esta_vacia()devuelveTruesitamano() == 0. Es la guardia natural antes de cualquierpopopeek, y la condición de parada del bucle de vaciado.tamano()devuelve el número de elementos. Para que sea O(1), la implementación mantendrá un contador que se incrementa en cadapushy se decrementa en cadapop— la misma técnica del atributotamanodeListaEnlazadaen el módulo 2, donde contar recorriendo habría costado O(n).
Aunque parezcan triviales, estas dos operaciones son las que hacen seguro el uso de la pila: casi todo bug con pilas es un pop sin comprobar esta_vacia antes.
El caso de la pila vacía: excepción vs None
¿Qué debe hacer pop() (o peek()) sobre una pila vacía? Es una decisión de diseño, y las dos opciones razonables tienen consecuencias distintas:
| Estrategia | Comportamiento | Ventajas | Inconvenientes |
|---|---|---|---|
| Lanzar excepción | raise IndexError("pop sobre pila vacía") |
El error no pasa desapercibido; obliga al llamador a pensar; es lo que hace list.pop() de Python |
Requiere try/except o comprobación previa |
Devolver None |
return None |
Código llamador más corto | Bug silencioso: si alguien apila None como valor legítimo, no puedes distinguir "pila vacía" de "la cima era None"; el error explota lejos de su causa |
En este curso adoptamos la excepción, por dos motivos:
- Coherencia con Python:
[].pop()lanzaIndexError. Nuestra pila se comportará como el resto del lenguaje. - Fallar pronto y fuerte: un
Noneinesperado puede viajar por medio programa antes de romper algo, y entonces el traceback no apunta a la causa. Una excepción en elpopculpable apunta exactamente al error.
El llamador tiene entonces dos estilos correctos, ambos válidos:
# Estilo 1: mirar antes de saltar (LBYL, "Look Before You Leap")
if not historial_vacio():
accion = desapilar()
revertir(accion)
else:
print("Nada que deshacer")
# Estilo 2: pedir perdón en vez de permiso (EAFP, el estilo idiomático en Python)
try:
accion = desapilar()
revertir(accion)
except IndexError:
print("Nada que deshacer")Lo importante no es qué estilo elijas, sino que la decisión "excepción, no None" quede escrita en el contrato y todos los usuarios de la pila puedan confiar en ella.
Invariantes del TDA pila
Un invariante es una propiedad que se cumple siempre, antes y después de cada operación (ya razonamos así en el módulo 2, cuando manteníamos cabeza, cola y tamano coherentes tras cada inserción). Los invariantes de la pila son el "control de calidad" de cualquier implementación:
tamano() >= 0en todo momento; yesta_vacia()equivale atamano() == 0.- Tras
push(x):peek()devuelvex, ytamano()ha crecido exactamente en 1. pop()traspush(x)(sin operaciones intermedias) devuelvexy deja la pila exactamente como estaba antes delpush. Es decir:popdeshacepush.peek()no modifica nada:tamano()y el contenido son idénticos antes y después.- Orden LIFO global: si se apilan n elementos y luego se desapilan n, salen en orden exactamente inverso.
Estos invariantes son directamente traducibles a tests. Cuando implementemos las dos pilas de la lección 03-03, cualquiera de ellas debe pasar exactamente las mismas comprobaciones: esa es la prueba práctica de que el TDA es independiente de la implementación.
Secuencias de operaciones: el deshacer de TaskFlow
Juguemos una sesión realista de TaskFlow, todavía con pseudocódigo del contrato. Cada acción del usuario se registra con push; cada pulsación de Deshacer ejecuta pop y revierte. Seguimos la tarea {"id": 7, "titulo": "Revisar presupuesto", "prioridad": 2, "estado": "pendiente"}:
| # | El usuario... | Operación sobre la pila | Pila de historial (cima arriba) |
|---|---|---|---|
| 1 | Crea la tarea 7 | push(crear id=7) |
crear 7 |
| 2 | Sube prioridad 2→1 | push(prioridad 7: 2→1) |
prioridad 2→1crear 7 |
| 3 | Marca en curso | push(estado 7: pendiente→en_curso) |
estado →en_cursoprioridad 2→1crear 7 |
| 4 | Pulsa Deshacer | pop() → revierte estado a pendiente |
prioridad 2→1crear 7 |
| 5 | Asigna a Ana | push(asignar 7 a ana) |
asignar anaprioridad 2→1crear 7 |
| 6 | Pulsa Deshacer | pop() → quita la asignación |
prioridad 2→1crear 7 |
| 7 | Pulsa Deshacer | pop() → prioridad vuelve a 2 |
crear 7 |
| 8 | Pulsa Deshacer | pop() → se elimina la tarea 7 |
(vacía) |
| 9 | Pulsa Deshacer | esta_vacia() es True → botón inactivo, no se llama a pop |
(vacía) |
Dos observaciones importantes:
- En el paso 4 se deshizo el estado y en el 5 el usuario hizo algo nuevo. La acción deshecha no "vuelve": la pila solo registra lo vigente. (¿Y si quisiera rehacerla? Necesitaríamos una segunda pila; esa es exactamente la construcción deshacer/rehacer de la lección 03-04.)
- Cada acción apilada lleva la información necesaria para revertirla (el valor previo:
2→1,pendiente→en_curso). En 03-03 formalizaremos esto con diccionarios{"tipo": ..., "tarea": ..., "datos_previos": ...}.
Mini-práctica guiada: list como pila provisional
Aún no tenemos nuestra clase Pila (llega en 03-03), pero la list de Python puede ejercer de pila provisional, porque sus operaciones por el final son las adecuadas:
lista.append(x)→ hace depush→ O(1) amortizado.lista.pop()(sin argumento) → hace depop→ O(1).lista[-1]→ hace depeek→ O(1).len(lista) == 0→ hace deesta_vacia→ O(1).
¿Por qué por el FINAL y no por el inicio? Recuerda la trampa del módulo 1: insert(0, x) y pop(0) son O(n), porque una list es un array dinámico y tocar el inicio obliga a desplazar todos los elementos. La cima de nuestra pila provisional debe ser el final de la lista, donde el array trabaja en O(1). Una pila hecha con insert(0)/pop(0) "funciona" pero degrada todas las operaciones a O(n): rompería la tabla de costes del contrato.
Abre un intérprete y reproduce la sesión de TaskFlow de la sección anterior:
historial = [] # pila vacía (provisional, sobre list)
# El usuario trabaja: cada acción se apila con append (= push)
historial.append("crear tarea 7")
historial.append("prioridad 7: 2 -> 1")
historial.append("estado 7: pendiente -> en_curso")
print(len(historial)) # 3 (= tamano)
print(historial[-1]) # 'estado 7: pendiente -> en_curso' (= peek: sin retirar)
# Pulsa Deshacer: pop() retira y devuelve la cima
accion = historial.pop()
print(f"Deshaciendo: {accion}") # Deshaciendo: estado 7: pendiente -> en_curso
print(historial[-1]) # 'prioridad 7: 2 -> 1' (nueva cima)
# Deshacer todo con el patrón de vaciado
while len(historial) > 0: # (= not esta_vacia)
print(f"Deshaciendo: {historial.pop()}")
# Deshaciendo: prioridad 7: 2 -> 1
# Deshaciendo: crear tarea 7
historial.pop() # IndexError: pop from empty listExplicación línea a línea:
historial = []crea la pila vacía. La lista vacía es nuestra pila vacía.- Los tres
appendapilan en orden; tras ellos, la cima (final de la lista) es la acción más reciente. historial[-1]lee la cima sin retirarla: es nuestropeek. Ojo: sobre una lista vacía,[-1]también lanzaIndexError, coherente con nuestra decisión de diseño.historial.pop()sin argumento retira del final: LIFO garantizado y O(1).- El bucle
whilees el patrón de vaciado: imprime las acciones en orden inverso al que ocurrieron, que es justo el orden correcto de un "deshacer todo". - El último
pop()sobre la lista vacía lanzaIndexError: Python ya implementa la estrategia de excepción que elegimos.
Esta pila provisional es totalmente funcional, pero tiene un defecto: no protege el contrato. Nada impide que otro programador haga historial.insert(0, x) o historial[3] y rompa la disciplina LIFO. Encapsular la lista dentro de una clase que solo exponga las cinco operaciones es precisamente el trabajo de la próxima lección.
Errores Comunes y Consejos
- Hacer
popsin comprobar antes (o sintry/except): el clásicoIndexErroren producción. Toda llamada apop/peekdebe estar protegida poresta_vacia()o por unexcept IndexError. - Usar
pop(0)oinsert(0, x)sobre lalist-pila: funciona, pero convierte O(1) en O(n). La cima vive en el final de la lista. Si dudas, vuelve a la tabla de costes delistdel módulo 1. - Usar
popcuando solo querías consultar: si después de "mirar" la cima la necesitas de nuevo, has destruido información. Consulta =peek(lista[-1]); extracción =pop. - Devolver
Noneen pila vacía "para simplificar": acabas conif resultado is not Nonerepartidos por todo el código y bugs cuandoNonees un valor válido. Excepción y punto. - Olvidar que
popdevuelve el elemento: escribirpila.pop()y luego intentar leer la cima conpeekpara saber "qué se desapiló" — ya es tarde, era el valor de retorno del propiopop. - Consejo: cuando escribas o depures secuencias de operaciones, dibuja la traza en una tabla como las de esta lección (operación → devuelve → estado). Dos minutos de tabla ahorran veinte de depurador.
Ejercicios
Ejercicio 1: traza completa
Partiendo de una pila vacía, construye la tabla de traza (operación, valor devuelto, estado de la pila, tamaño) de esta secuencia:
Indica en qué paso (si lo hay) se lanza IndexError según nuestra decisión de diseño.
Ejercicio 2: el "deshacer todo" seguro
Usando una list como pila provisional, escribe una función deshacer_todo(historial) que reciba la pila de acciones (lista de cadenas) y devuelva una lista con los mensajes "Deshaciendo: <accion>" en el orden correcto de deshacer, dejando la pila vacía. La función no debe fallar nunca, ni siquiera si recibe la pila ya vacía. Escribe dos versiones: una con estilo LBYL (comprobar antes) y otra con estilo EAFP (try/except).
Ejercicio 3: detectar la implementación rota
Un compañero ha escrito estas "operaciones de pila" sobre list. Señala todos los errores respecto al contrato y a los invariantes de esta lección, y corrígelos:
def push(pila, elemento):
pila.insert(0, elemento)
def pop(pila):
if len(pila) == 0:
return None
return pila.pop(0)
def peek(pila):
return pila.pop(0)Soluciones
Solución 1:
| Paso | Operación | Devuelve | Pila (cima arriba) | Tamaño |
|---|---|---|---|---|
| 1 | push(10) |
— | 10 |
1 |
| 2 | push(20) |
— | 20, 10 |
2 |
| 3 | peek() |
20 |
20, 10 |
2 |
| 4 | push(30) |
— | 30, 20, 10 |
3 |
| 5 | pop() |
30 |
20, 10 |
2 |
| 6 | pop() |
20 |
10 |
1 |
| 7 | push(40) |
— | 40, 10 |
2 |
| 8 | peek() |
40 |
40, 10 |
2 |
| 9 | pop() |
40 |
10 |
1 |
| 10 | pop() |
10 |
(vacía) | 0 |
| 11 | pop() |
IndexError |
(vacía) | 0 |
El paso 11 lanza IndexError: la pila está vacía y elegimos excepción, no None.
Solución 2:
# Versión LBYL: comprobar antes de desapilar
def deshacer_todo(historial):
mensajes = []
while len(historial) > 0: # guardia: solo pop si no está vacía
accion = historial.pop() # retira la cima (la más reciente)
mensajes.append(f"Deshaciendo: {accion}")
return mensajes
# Versión EAFP: intentar y capturar la excepción
def deshacer_todo_eafp(historial):
mensajes = []
while True:
try:
accion = historial.pop()
except IndexError: # pila agotada: terminamos
break
mensajes.append(f"Deshaciendo: {accion}")
return mensajesAmbas devuelven los mensajes en orden inverso a la inserción (el orden correcto de deshacer) y dejan historial vacía. Con una pila ya vacía, el while de la primera no llega a ejecutarse y el try de la segunda rompe el bucle en la primera vuelta: ninguna falla.
Solución 3: hay cuatro errores.
pushusainsert(0, ...): apila por el inicio de la lista → O(n) por desplazamiento del array. Rompe el coste del contrato.popusapop(0): mismo problema, O(n). (Curiosamente el conjunto "insertar y borrar por el inicio" mantiene el orden LIFO, así que funciona... lentísimo. El bug de rendimiento es el más traicionero porque los tests de corrección no lo cazan.)popdevuelveNoneen pila vacía: contradice la decisión de diseño; debe dejar que la excepción salte (o lanzarla explícitamente).peekhacepop(0): ¡retira el elemento! Viola el invariante 4 (peek no modifica). Debe ser solo lectura.
Versión corregida:
def push(pila, elemento):
pila.append(elemento) # cima = final de la lista: O(1)
def pop(pila):
return pila.pop() # retira del final: O(1); IndexError si vacía
def peek(pila):
return pila[-1] # solo lectura: O(1); IndexError si vacíaConclusión
Ya dominas la mecánica completa del TDA pila: push tapa la cima anterior, pop retira y devuelve en un solo gesto, peek observa sin modificar, y esta_vacia/tamano hacen seguro todo lo demás. Has tomado una decisión de diseño razonada (excepción IndexError en pila vacía, como hace la propia list de Python), has formulado los invariantes que cualquier implementación deberá cumplir, y has trazado sesiones reales del deshacer de TaskFlow operación a operación. Además ya tienes una pila provisional funcionando con list —siempre por el final, nunca por el inicio— aunque le falta lo esencial: una frontera que impida saltarse el contrato. En la próxima lección construiremos esa frontera dos veces: una clase Pila sobre list y una PilaEnlazada sobre los nodos del módulo 2, mediremos ambas con timeit y montaremos el HistorialAcciones real de TaskFlow.
Curso de Estructuras de Datos
Módulo 1: Introducción a las Estructuras de Datos
- ¿Qué son las Estructuras de Datos?
- Importancia de las Estructuras de Datos en la Programación
- Tipos de Estructuras de Datos
- Complejidad Algorítmica y Notación Big O
- Arrays y Memoria: la Base de las Estructuras de Datos
Módulo 2: Listas
- Introducción a las Listas
- Listas Enlazadas
- Listas Doblemente Enlazadas
- Listas Circulares
- Ejercicios con Listas
Módulo 3: Pilas
- Introducción a las Pilas
- Operaciones Básicas con Pilas
- Implementación de Pilas
- Aplicaciones de las Pilas
- Ejercicios con Pilas
Módulo 4: Colas
- Introducción a las Colas
- Operaciones Básicas con Colas
- Colas Circulares
- Colas de Prioridad
- Colas Dobles (Deques)
- Ejercicios con Colas
Módulo 5: Tablas Hash y Diccionarios
- Introducción a las Tablas Hash
- Funciones Hash y Resolución de Colisiones
- Diccionarios y Conjuntos en la Práctica
- Ejercicios con Tablas Hash
Módulo 6: Árboles
- Introducción a los Árboles
- Árboles Binarios
- Recorridos de Árboles
- Árboles Binarios de Búsqueda
- Árboles AVL
- Árboles B
- Montículos (Heaps)
- Ejercicios con Árboles
Módulo 7: Grafos
- Introducción a los Grafos
- Representación de Grafos
- Algoritmos de Búsqueda en Grafos
- Algoritmos de Caminos Mínimos
- Árboles de Expansión Mínima
- Aplicaciones de los Grafos
- Ejercicios con Grafos
