Ja tens la pila implementada i mesurada. Aquesta lliçó la posa a treballar en quatre aplicacions reals, desenvolupades amb codi complet: el desfer/refer definitiu de TaskFlow (amb dues piles cooperant), la validació de parèntesis i claudàtors balancejats (que farem servir per als filtres de cerca de TaskFlow), l'avaluació d'expressions postfixes amb la seva conversió des de notació infixa (una calculadora d'estimacions), i la pila de crides de Python, que explica per què una recursió massa profunda rebenta amb RecursionError. Són els quatre usos clàssics de les piles, els que trobaràs en entrevistes tècniques i en codi de producció, i tots comparteixen un mateix patró mental: "el pendent més recent es resol primer". Farem servir la classe Pila de la lliçó anterior en tots els exemples.
Contingut
- Desfer/refer complet amb dues piles
- Parèntesis balancejats: validar els filtres de TaskFlow
- Expressions postfixes: avaluació i conversió des d'infixa
- La pila de crides i la recursió
Desfer/refer complet amb dues piles
HistorialAccions (lliçó 03-03) desfà, però no perdona: si l'usuari desfà per error, l'acció es perd. Tot programari seriós ofereix refer (Ctrl+Y / Ctrl+Maj+Z). La solució clàssica fa servir dues piles cooperant:
- Pila
desfer: les accions realitzades i vigents. - Pila
refer: les accions que l'usuari ha desfet (i podria voler recuperar).
Les regles del ball són tres:
| L'usuari... | Pila desfer |
Pila refer |
|---|---|---|
| Fa una acció nova | push(accio) |
Es buida |
| Prem Desfer | pop() → es reverteix |
push(accio) |
| Prem Refer | push(accio) |
pop() → es reaplica |
Desfer i refer es passen accions l'una a l'altra, com dues mans. La regla subtil —i la que més s'oblida— és la primera: una acció nova invalida el refer. Si després de desfer "canviar prioritat" l'usuari edita el títol, ja no té sentit "refer el canvi de prioritat" sobre un estat que ha divergit: el refer s'ha de buidar.
flowchart LR
U["Accio nova"] -->|push| D["Pila DESFER"]
U -.->|buida| R["Pila REFER"]
D -->|"Desfer: pop"| R
R -->|"Refer: pop"| D
La implementació, sobre la Pila de 03-03 i les accions {"tipus", "tasca", "dades_previes"}. La novetat és que cada acció guarda ara també dades_noves, perquè refer necessita reaplicar el valor nou (desfer restaura el previ; refer restaura el nou):
class GestorDesferRefer:
"""Desfer/refer de TaskFlow amb dues piles cooperant."""
def __init__(self, tasques):
self._tasques = tasques # el tauler: dict de tasques per id
self._desfer = Pila()
self._refer = Pila()
def executar(self, tipus, tasca_id, dades_noves):
"""Registra i aplica una accio nova de l'usuari."""
tasca = self._tasques[tasca_id]
# 1. Capturar els valors previs ABANS de modificar (patro de 03-03)
dades_previes = {camp: tasca[camp] for camp in dades_noves}
# 2. Aplicar el canvi
tasca.update(dades_noves)
# 3. Registrar l'accio completa (amb el previ I el nou)
self._desfer.push({"tipus": tipus, "tasca": tasca_id,
"dades_previes": dades_previes,
"dades_noves": dades_noves})
# 4. Una accio nova invalida tot el refer
self._refer = Pila()
def desfer(self):
if self._desfer.esta_buida():
return "Res a desfer"
accio = self._desfer.pop()
self._tasques[accio["tasca"]].update(accio["dades_previes"])
self._refer.push(accio) # l'accio passa a l'altra pila
return f"Desfet: {accio['tipus']} (tasca {accio['tasca']})"
def refer(self):
if self._refer.esta_buida():
return "Res a refer"
accio = self._refer.pop()
self._tasques[accio["tasca"]].update(accio["dades_noves"])
self._desfer.push(accio) # i torna a la pila de desfer
return f"Refet: {accio['tipus']} (tasca {accio['tasca']})"Sessió completa sobre la tasca 7:
tasques = {7: {"id": 7, "titol": "Revisar el pressupost", "prioritat": 2,
"estat": "pendent"}}
gestor = GestorDesferRefer(tasques)
gestor.executar("canviar_prioritat", 7, {"prioritat": 1})
gestor.executar("canviar_estat", 7, {"estat": "en curs"})
print(tasques[7]["prioritat"], tasques[7]["estat"]) # 1 en curs
print(gestor.desfer()) # Desfet: canviar_estat (tasca 7)
print(tasques[7]["estat"]) # pendent
print(gestor.refer()) # Refet: canviar_estat (tasca 7)
print(tasques[7]["estat"]) # en curs
gestor.desfer() # desfa l'estat una altra vegada
gestor.executar("assignar", 7, {"assignada_a": "anna"}) # accio nova!
print(gestor.refer()) # Res a refer <- el refer s'ha invalidatObserva l'última línia: hi havia una acció esperant a refer, però l'assignació a l'Anna la va invalidar. Aquesta és la regla 1 en acció, i és exactament com es comporta el teu editor de text.
Et recorda alguna cosa? Al mòdul 2, HistorialTasques resolia "enrere/endavant" amb una llista doblement enllaçada i un cursor. Són dues solucions al mateix problema de navegació bidireccional: la llista doble manté tot l'historial i mou un cursor (ideal quan avançar no invalida res, com visitar tasques); les dues piles descarten el futur quan el present canvia (ideal per editar, on les branques mortes no han de sobreviure). Triar entre totes dues és triar la semàntica correcta, no l'estructura "millor".
Parèntesis balancejats: validar els filtres de TaskFlow
TaskFlow vol permetre filtres de cerca avançats escrits per l'usuari:
Abans d'interpretar un filtre cal validar que els seus parèntesis (), claudàtors [] i claus {} estan balancejats: cada obertura té el seu tancament, del tipus correcte i en l'ordre correcte. (a[b)c] està malament encara que hi hagi tants tancaments com obertures: es creuen.
Per què una pila? Perquè en trobar un tancament, ha de casar amb l'obertura pendent més recent: LIFO pur.
L'algorisme:
- Recorre la cadena caràcter a caràcter.
- Si és una obertura (
(,[,{) →push. - Si és un tancament (
),],}) → la pila no pot estar buida i el seu cim ha de ser l'obertura parella;popi comprova. - En acabar, la pila ha de quedar buida (sense obertures òrfenes).
PARELLES = {")": "(", "]": "[", "}": "{"}
def filtre_balancejat(filtre):
"""Comprova que (), [] i {} del filtre estan correctament balancejats."""
pila = Pila()
for caracter in filtre:
if caracter in "([{":
pila.push(caracter) # obertura: queda pendent
elif caracter in ")]}":
if pila.esta_buida():
return False # tancament sense obertura pendent
if pila.pop() != PARELLES[caracter]:
return False # tancament de tipus equivocat
# qualsevol altre caracter (lletres, espais, ':') no afecta el balanc
return pila.esta_buida() # sense obertures sense tancarExplicació dels tres modes de fallada, amb la traça de l'exemple creuat (a[b)c]:
| Caràcter | Acció | Pila (cim a la dreta) | Resultat |
|---|---|---|---|
( |
push | ( |
|
a |
ignorar | ( |
|
[ |
push | (, [ |
|
b |
ignorar | (, [ |
|
) |
pop → surt [, esperava ( |
False: tancament equivocat |
- Tancament amb pila buida (
"tasca)"): un)sense cap(pendent. - Tancament de tipus equivocat (
"(a[b)c]"): el més interessant — hi ha obertures pendents, però la més recent no és la parella. - Pila no buida al final (
"(pendent"): obertures que mai no es van tancar.
print(filtre_balancejat("(prioritat:1 OR prioritat:2) AND [estat:pendent]")) # True
print(filtre_balancejat("(a[b)c]")) # False
print(filtre_balancejat("(pendent")) # False
print(filtre_balancejat("")) # True (res a balancejar)Cost: O(n) en temps (una passada per la cadena, operacions de pila O(1)) i O(n) en espai en el pitjor cas (tot obertures). Aquest algorisme, tal qual, és el que fan servir editors i IDE per pintar-te el parèntesi sense parella en vermell.
Expressions postfixes: avaluació i conversió des d'infixa
TaskFlow vol una minicalculadora d'estimacions: l'usuari escriu (3 + 5) * 2 (hores de tres subtasques...) i l'aplicació ho avalua. Avaluar notació infixa (l'operador entre operands) és incòmode: exigeix prioritats (* abans que +) i parèntesis. Els compiladors ho resolen en dues fases, totes dues amb piles:
- Convertir l'expressió infixa a postfixa (notació polonesa inversa: l'operador després dels seus operands):
(3 + 5) * 2→3 5 + 2 *. - Avaluar la postfixa, que ja no necessita ni prioritats ni parèntesis.
Avaluar postfixa (una pila d'operands)
Regla: recorre els tokens; si és un número, push; si és un operador, pop dos operands, opera i push el resultat. Al final queda exactament un valor: el resultat.
def avaluar_postfixa(expressio):
"""Avalua una expressio postfixa amb tokens separats per espais."""
pila = Pila()
for token in expressio.split():
if token in "+-*/":
b = pila.pop() # compte amb l'ordre!
a = pila.pop() # a va arribar abans que b
if token == "+": pila.push(a + b)
elif token == "-": pila.push(a - b)
elif token == "*": pila.push(a * b)
elif token == "/": pila.push(a / b)
else:
pila.push(float(token)) # operand: a la pila
return pila.pop() # l'unic valor restant
print(avaluar_postfixa("3 5 + 2 *")) # 16.0 == (3 + 5) * 2
print(avaluar_postfixa("3 5 2 * +")) # 13.0 == 3 + 5 * 2Traça de 3 5 + 2 *:
| Token | Acció | Pila (cim a la dreta) |
|---|---|---|
3 |
push 3 | 3 |
5 |
push 5 | 3, 5 |
+ |
pop 5 i 3 → push 8 | 8 |
2 |
push 2 | 8, 2 |
* |
pop 2 i 8 → push 16 | 16 |
El detall que causa el 90 % dels bugs: l'ordre dels pops. El primer pop retorna l'operand dret (b), el segon l'esquerre (a). Amb + i * tant se val (commutatives); amb - i / et canvia el resultat: 6 2 / ha de ser 6 / 2 = 3, no 2 / 6.
Convertir infixa → postfixa (una pila d'operadors)
L'algorisme shunting-yard de Dijkstra, en la seva versió essencial. Els números surten directament a la sortida; els operadors esperen en una pila fins que n'arriba un de prioritat menor o igual (o un tancament de parèntesi) que els obliga a sortir:
PRIORITAT = {"+": 1, "-": 1, "*": 2, "/": 2}
def infixa_a_postfixa(expressio):
sortida = []
pila = Pila() # pila d'operadors en espera
for token in expressio.split():
if token in PRIORITAT: # es un operador
# Desallotjar operadors de prioritat major o igual: van abans
while (not pila.esta_buida() and pila.peek() != "("
and PRIORITAT.get(pila.peek(), 0) >= PRIORITAT[token]):
sortida.append(pila.pop())
pila.push(token)
elif token == "(":
pila.push(token) # marca l'inici d'un grup
elif token == ")":
while pila.peek() != "(": # desallotjar fins a l'obertura
sortida.append(pila.pop())
pila.pop() # descartar el '(' (no surt)
else:
sortida.append(token) # operand: directe a la sortida
while not pila.esta_buida():
sortida.append(pila.pop()) # desallotjar el que queda pendent
return " ".join(sortida)
print(infixa_a_postfixa("( 3 + 5 ) * 2")) # 3 5 + 2 *
print(infixa_a_postfixa("3 + 5 * 2")) # 3 5 2 * +
print(avaluar_postfixa(infixa_a_postfixa("( 3 + 5 ) * 2"))) # 16.0Fixa't en l'ús de peek que vam anunciar a 03-02: el while de desallotjament consulta el cim per decidir si desapilar — mirar abans d'actuar. I al segon exemple, observa com la pila reté el + mentre processa 5 * 2: la prioritat dels operadors es resol sola gràcies a l'ordre LIFO. (La nostra versió exigeix tokens separats per espais i no gestiona operadors unaris ni potències: suficient per a les estimacions de TaskFlow; l'algorisme complet és una extensió directa.)
La pila de crides i la recursió
La pila més important de totes és una que no has creat tu: la pila de crides (call stack). Cada vegada que Python entra en una funció, apila un marc (frame) amb les seves variables locals i el punt de retorn; quan la funció retorna, el desapila. Observa-la en viu:
def a():
print("entro dins de a")
b()
print("surto de a") # s'executa DESPRES que b acabi
def b():
print(" entro dins de b")
c()
print(" surto de b")
def c():
print(" entro dins de c")
print(" surto de c")
a()Les entrades passen en ordre a→b→c, però les sortides en ordre invers c→b→a: l'última funció a entrar és la primera a sortir. LIFO pur; per això l'estructura que ho sosté és una pila. Quan un error no es captura, Python t'imprimeix aquesta pila — el traceback és, literalment, un bolcat de la pila de crides, del fons (a) al cim (c).
Per què una recursió profunda rebenta
Una funció recursiva s'apila a si mateixa una vegada per nivell. Sumar els ids d'una cadena de tasques enllaçades, recursivament:
def sumar_ids(node):
if node is None: # cas base: cadena esgotada
return 0
return node.dada["id"] + sumar_ids(node.seguent) # un marc per nodeAmb 100 tasques, perfecte: 100 marcs apilats i desapilats. Amb 100 000 tasques:
La pila de crides té un límit (per defecte, uns 1000 marcs a CPython — consulta'l amb sys.getrecursionlimit()): protegeix la memòria del procés de recursions desbocades. La conclusió pràctica:
- Recursió: elegant per a profunditats petites o acotades (arbres equilibrats, dividir i vèncer).
- Iteració amb pila explícita: quan la profunditat pot ser gran, substitueix la pila de crides (limitada, implícita) per una
Pilateva (tan gran com la teva memòria, explícita). Tota recursió es pot reescriure així, i és un exercici central de la propera lliçó.
Aquest duet recursió/pila explícita reapareixerà amb força: els recorreguts d'arbres (mòdul 6) i la cerca en profunditat (DFS) de grafs (mòdul 7) són exactament això — de fet, "DFS iteratiu" no és res més que canviar la pila de crides per una pila explícita. Aquí ho deixem anunciat.
Errors Comuns i Consells
- Oblidar buidar la pila de refer en executar una acció nova: el bug clàssic del desfer/refer; produeix "refers" que apliquen canvis sobre un estat que ja no existeix, corrompent dades. La regla és innegociable.
- Guardar només
dades_previesquan hi ha refer: desfer necessita el previ, refer necessita el nou. Sensedades_noves, el refer no sap què reaplicar. - En el balanceig, comprovar només el recompte: "tres obertures i tres tancaments" no n'hi ha prou (
)a(o(a[b)c]fallen). Cal casar tipus i ordre: per això cal la pila i no un comptador. - Oblidar l'
esta_buida()final o inicial en el balanceig: sense la comprovació final,"(pendent"passaria; sense la inicial davant d'un tancament,")"llançariaIndexErroren comptes de retornarFalse. - Invertir els operands en
-i/: el primerpopés l'operand dret. Grava-t'ho:b = pop(); a = pop(); a - b. - "Arreglar" un
RecursionErrorpujant el límit ambsys.setrecursionlimit: gairebé sempre és amagar el problema (i arriscar-te a tombar el procés sencer). La solució robusta és iterar amb pila explícita.
Exercicis
Exercici 1: desfer múltiple
Afegeix a GestorDesferRefer un mètode desfer_diverses(n) que desfaci fins a n accions de cop (la interfície de TaskFlow tindrà un menú "desfer les últimes 5"). Ha de retornar la llista de missatges de cada desfer efectiu i aturar-se sense error si l'historial s'esgota abans. Pregunta extra: després de desfer_diverses(3), què ha de contenir la pila de refer i en quin ordre, perquè tres refer() seguits ho restaurin tot correctament?
Exercici 2: balanceig amb posició de l'error
Millora filtre_balancejat perquè, en comptes de False, retorni la posició (índex) del caràcter que trenca el balanç, o -1 si el filtre és vàlid. Per a obertures sense tancar, retorna la posició de l'obertura òrfena més interna (pista: apila tuples (caracter, index)).
Exercici 3: avaluar una estimació infixa completa
Combina les dues funcions de la secció 3 en estimar(expressio_infixa) que validi primer el balanceig de parèntesis (amb filtre_balancejat), llanci ValueError("expressio mal balancejada") si falla, i si no, converteixi i avaluï. Prova-la amb "( 3 + 5 ) * 2" i "( 3 + 5 * 2".
Solucions
Solució 1:
def desfer_diverses(self, n):
missatges = []
for _ in range(n):
if self._desfer.esta_buida(): # historial esgotat: parar sense error
break
missatges.append(self.desfer()) # reutilitza tota la logica existent
return missatgesReutilitzar self.desfer() garanteix que cada pas mou l'acció a la pila de refer. Pregunta extra: si es desfan les accions A3, A2, A1 (en aquest ordre, de la més recent a la més antiga), la pila de refer queda amb A1 al cim i A3 al fons. Així, el primer refer() reaplica A1, després A2, després A3: exactament l'ordre cronològic original. Les dues piles inverteixen l'ordre dues vegades, i dues inversions restauren l'ordre — un patró que reapareix a l'exercici d'invertir de 03-05.
Solució 2:
def filtre_balancejat_pos(filtre):
pila = Pila() # apilem (caracter, index)
for i, caracter in enumerate(filtre):
if caracter in "([{":
pila.push((caracter, i))
elif caracter in ")]}":
if pila.esta_buida():
return i # tancament sense obertura: culpable aqui
obertura, _ = pila.pop()
if obertura != PARELLES[caracter]:
return i # tancament de tipus equivocat
if not pila.esta_buida():
_, index = pila.pop() # l'obertura orfena mes interna
return index
return -1
print(filtre_balancejat_pos("(a[b)c]")) # 4 (el ')' que no casa)
print(filtre_balancejat_pos("(pendent")) # 0 (el '(' mai tancat)
print(filtre_balancejat_pos("(ok)[si]")) # -1La clau és enriquir el que s'apila: en comptes del caràcter sol, una tupla amb el seu índex. L'estructura de l'algorisme no canvia — un altre avantatge de raonar amb el contracte: la pila no exigeix que els seus elements siguin de cap tipus concret.
Solució 3:
def estimar(expressio_infixa):
if not filtre_balancejat(expressio_infixa):
raise ValueError("expressio mal balancejada")
postfixa = infixa_a_postfixa(expressio_infixa)
return avaluar_postfixa(postfixa)
print(estimar("( 3 + 5 ) * 2")) # 16.0
print(estimar("( 3 + 5 * 2")) # ValueError: expressio mal balancejadaValidar abans de convertir evita que infixa_a_postfixa falli amb un IndexError críptic en buscar un ( que no existeix: l'usuari de TaskFlow rep un error de domini comprensible, no un traceback d'estructura interna. Tres algorismes de pila encadenats en cinc línies.
Conclusió
Has vist la pila desplegada en els seus quatre papers estel·lars: dues piles cooperant donen a TaskFlow un desfer/refer professional (amb la regla d'or d'invalidar el refer davant d'accions noves, i el seu contrast amb l'HistorialTasques de llista doble del mòdul 2); una pila d'obertures pendents valida el balanceig dels filtres de cerca casant cada tancament amb l'obertura més recent; una pila d'operands i una altra d'operadors avaluen i tradueixen expressions aritmètiques resolent les prioritats per pur ordre LIFO; i la pila de crides de Python sosté cada funció que executes, amb el seu límit de profunditat com a causa del RecursionError — i la pila explícita com a cura, que als mòduls 6 i 7 es convertirà en recorreguts d'arbres i DFS. El patró comú: el pendent més recent es resol primer. Només queda consolidar: la propera lliçó és íntegrament d'exercicis, on invertiràs seqüències, construiràs una pila amb mínim en O(1), validaràs seqüències d'operacions i convertiràs recursions en iteracions. Sense teoria nova: pura pràctica.
Curs d'Estructures de Dades
Mòdul 1: Introducció a les Estructures de Dades
- Què són les Estructures de Dades?
- Importància de les Estructures de Dades en la Programació
- Tipus d'Estructures de Dades
- Complexitat Algorísmica i Notació Big O
- Arrays i Memòria: la Base de les Estructures de Dades
Mòdul 2: Llistes
- Introducció a les Llistes
- Llistes Enllaçades
- Llistes Doblement Enllaçades
- Llistes Circulars
- Exercicis amb Llistes
Mòdul 3: Piles
- Introducció a les Piles
- Operacions Bàsiques amb Piles
- Implementació de Piles
- Aplicacions de les Piles
- Exercicis amb Piles
Mòdul 4: Cues
- Introducció a les Cues
- Operacions Bàsiques amb Cues
- Cues Circulars
- Cues de Prioritat
- Cues Dobles (Deques)
- Exercicis amb Cues
Mòdul 5: Taules Hash i Diccionaris
- Introducció a les Taules Hash
- Funcions Hash i Resolució de Col·lisions
- Diccionaris i Conjunts a la Pràctica
- Exercicis amb Taules Hash
Mòdul 6: Arbres
- Introducció als Arbres
- Arbres Binaris
- Recorreguts d'Arbres
- Arbres Binaris de Cerca
- Arbres AVL
- Arbres B
- Monticles (Heaps)
- Exercicis amb Arbres
Mòdul 7: Grafs
- Introducció als Grafs
- Representació de Grafs
- Algorismes de Cerca en Grafs
- Algorismes de Camins Mínims
- Arbres d'Expansió Mínima
- Aplicacions dels Grafs
- Exercicis amb Grafs
