En tancar el mòdul de piles vam deixar tres fils pendents: un HistorialAmbLimit que necessitava treballar pels dos extrems, un min-stack que podia consultar l'element més prioritari però no extreure'l, i una promesa: processar les tasques de TaskFlow "per ordre just d'arribada". Els tres fils es lliguen en aquest mòdul, i tots tres apunten a la mateixa família d'estructures: les cues. En aquesta lliçó presentem el TAD cua, el seu principi FIFO, el seu contracte d'operacions i els llocs (ben reals) on les cues sostenen sistemes sencers. Encara no implementarem res en detall: primer cal entendre bé què és una cua i per a què serveix; el com arriba a la lliçó següent.
Contingut
- De la pila a la cua: els tres ponts pendents
- El principi FIFO
- LIFO vs FIFO: dues polítiques oposades
- El contracte del TAD cua
- Analogies: el supermercat i la impressora
- Cues en sistemes reals
- La cua de notificacions de TaskFlow
- El mapa del mòdul 4
De la pila a la cua: els tres ponts pendents
Recordem exactament on ens vam quedar al final del mòdul 3:
- L'historial amb límit: el nostre
HistorialAmbLimitapilava per dalt però, en omplir-se, descartava pel fons amb unpop(0)de cost O(n). Necessitàvem una estructura eficient pels dos extrems. Aquesta estructura existeix, es diu deque (cua doble), i la veurem a la lliçó 04-05. - El "més prioritari": la
PilaAmbMinimresponia en O(1) "quin és l'element mínim?", però no permetia extreure'l si no era al cim. Extreure sempre el més prioritari és just el que fa una cua de prioritat (lliçó 04-04). - L'ordre just: una pila atén primer l'últim que ha arribat. En moltes situacions això és exactament el contrari del que volem: les notificacions de TaskFlow s'han d'enviar en l'ordre en què es van generar. Aquesta política és el FIFO, i és el cor d'aquest mòdul.
Fixa't en el patró: no canviem d'estructura per caprici, sinó perquè el problema ens hi empeny. Igual que la pila era l'estructura natural del "desfer", la cua és l'estructura natural de l'"atendre per ordre d'arribada".
El principi FIFO
FIFO són les sigles de First In, First Out: el primer que entra és el primer que surt. Una cua és un TAD (tipus abstracte de dades, com vam veure al mòdul 1) amb dues regles d'accés molt estrictes:
- Els elements entren sempre per un extrem, anomenat el final (o cua, en el sentit de "final de la fila").
- Els elements surten sempre per l'altre extrem, anomenat el front.
Aquesta separació d'extrems és la diferència essencial amb la pila, on tot passava pel mateix costat (el cim). En usar extrems oposats, l'ordre de sortida reprodueix exactament l'ordre d'arribada:
graph LR
subgraph Cua FIFO
direction LR
F["Notif 1<br/>(front)"] --> B["Notif 2"] --> C["Notif 3<br/>(final)"]
end
E["encuar(Notif 4)"] -.entra pel final.-> C
F -.surt pel front.-> S["desencuar() → Notif 1"]
Si encuem les notificacions 1, 2 i 3 (en aquest ordre) i després desencuem tres vegades, obtenim 1, 2 i 3, en aquest mateix ordre. Amb una pila hauríem obtingut 3, 2 i 1.
LIFO vs FIFO: dues polítiques oposades
Convé tenir les dues polítiques cara a cara, perquè triar malament entre elles produeix errors silenciosos (el programa funciona, però atén les coses en l'ordre equivocat):
| Aspecte | Pila (LIFO) | Cua (FIFO) |
|---|---|---|
| Regla | L'últim d'entrar és el primer de sortir | El primer d'entrar és el primer de sortir |
| Extrems usats | Un de sol (el cim) | Dos (entra pel final, surt pel front) |
| Operació d'entrada | apilar (push) |
encuar (enqueue) |
| Operació de sortida | desapilar (pop) |
desencuar (dequeue) |
| Consulta sense extreure | cim (peek) |
front (front) |
| Metàfora | Pila de plats | Cua del supermercat |
| Pregunta que respon | "Què és el més recent?" | "Què és el més antic pendent?" |
| Ús típic a TaskFlow | Desfer l'última acció | Enviar notificacions per ordre d'arribada |
Una manera útil de decidir quina necessites: pregunta't quin element s'ha d'atendre a continuació. Si la resposta és "el més recent" (desfer, tornar enrere, tancar l'últim parèntesi obert), és una pila. Si és "el que fa més temps que espera" (atendre peticions, enviar missatges, repartir feina amb justícia), és una cua.
El contracte del TAD cua
Com vam fer amb la pila, definim la cua pel seu contracte: quines operacions ofereix i què promet cadascuna, sense dir encara res de com s'implementa per dins. Aquest és l'esperit del TAD del mòdul 1: primer la interfície, després la implementació.
| Operació | Què fa | Què promet |
|---|---|---|
encuar(element) |
Afegeix element pel final |
L'element sortirà després de tots els que ja hi eren |
desencuar() |
Extreu i retorna l'element del front | És sempre el més antic; error si la cua és buida |
front() |
Retorna l'element del front sense extreure'l | No modifica la cua; error si és buida |
esta_buida() |
Indica si no hi ha elements | True/False, mai no falla |
mida() |
Nombre d'elements encuats | Enter ≥ 0 |
Dues observacions que ja coneixes del mòdul 3 i que continuen vigents:
- Error en cua buida: mantindrem la decisió que vam prendre amb la pila:
desencuar()ifront()sobre una cua buida llancen una excepció (estil EAFP), en lloc de retornarNone. UnNonesilenciós es confon amb un element vàlid i amaga errors. - Contracte estable, implementació intercanviable: igual que
PilaiPilaEnllacadacompartien contracte (i ho verificàvem ambprovar_contracte), en aquest mòdul veurem diverses implementacions de cua amb aquest mateix contracte. El codi client no hauria de notar el canvi.
Encara que no hem implementat res, ja podem escriure codi contra el contracte, que és com pensa un bon dissenyador de programari:
# Suposant que existeix una classe Cua que compleix el contracte
# (la construirem a la lliçó 04-02):
cua = Cua()
cua.encuar({"id": 1, "titol": "Desplegar el web", "prioritat": 2, "estat": "pendent"})
cua.encuar({"id": 2, "titol": "Revisar l'informe", "prioritat": 1, "estat": "pendent"})
cua.encuar({"id": 3, "titol": "Còpia de seguretat", "prioritat": 3, "estat": "pendent"})
print(cua.front()["titol"]) # "Desplegar el web" (la més antiga, sense extreure-la)
tasca = cua.desencuar() # extreu la tasca amb id 1
print(cua.mida()) # 2Observa que la tasca amb prioritat: 1 (la màxima a TaskFlow) no surt la primera: en una cua FIFO mana l'ordre d'arribada, no la urgència. Quan vulguem que mani la urgència necessitarem una altra estructura —la cua de prioritat de la lliçó 04-04—. Distingir totes dues necessitats és mitja batalla guanyada.
Analogies: el supermercat i la impressora
La cua del supermercat. Les persones s'incorporen pel final i la caixera atén pel front. Ningú (en un món civilitzat) no es cola: qui fa més temps que espera és atès primer. Les dues operacions del contracte són a la vista: arribar a la fila és encuar, ser atès és desencuar, i mirar qui és el següent sense atendre'l encara és front.
La cua d'impressió. Diverses persones envien documents a una impressora compartida. La impressora només pot imprimir un document alhora, així que les feines esperen en una cua: s'imprimeixen en l'ordre exacte en què es van enviar. Aquesta analogia afegeix un matís important que el supermercat no té: la cua actua d'amortidor entre un productor ràpid i un consumidor lent. Deu persones poden enviar documents en un segon; la impressora trigarà minuts a processar-los, però cap no es perd i cap no avança un altre. Guarda't aquesta idea: és la clau de gairebé tots els usos professionals de les cues.
Cues en sistemes reals
Les cues no són un exercici acadèmic; són una de les estructures més usades en sistemes en producció:
- Cues de missatges (RabbitMQ, Amazon SQS, Kafka en la seva versió més simple): un servei deixa missatges en una cua i un altre servei els consumeix al seu ritme. És la impressora a escala industrial: desacobla productors de consumidors i absorbeix pics de càrrega sense perdre feina.
- Buffers d'entrada/sortida: quan escrius al teclat més de pressa del que el programa llegeix, les pulsacions esperen en un buffer FIFO. El mateix passa amb paquets de xarxa en un router o amb àudio en streaming. Molts d'aquests buffers tenen mida fixa i "fan la volta": són les cues circulars de la lliçó 04-03.
- Planificadors de processos: el sistema operatiu manté cues de processos a punt per executar-se. La variant més famosa, el round-robin, reparteix torns de CPU per ordre d'arribada; connecta directament amb el
RepartidorTasquesque vam construir sobre laLlistaCircularal mòdul 2, i el reprendrem als exercicis (04-06). - Recorreguts de grafs: l'algorisme BFS (cerca en amplada) explora un graf "per capes" usant una cua. Només ho deixem anotat com a avançament: es desenvolupa al mòdul 7.
La cua de notificacions de TaskFlow
Situem tot això a la nostra aplicació. Cada cop que a TaskFlow passa alguna cosa rellevant —s'assigna una tasca, canvia un estat, s'acosta una data límit— cal notificar-ho a l'usuari afectat. Enviar una notificació (correu, avís al mòbil) és lent comparat amb generar l'esdeveniment, així que no podem enviar-les "al moment": les acumularíem i bloquejaríem l'aplicació.
La solució professional és la de la impressora: un productor (l'aplicació) encua notificacions a l'instant, i un consumidor (el procés d'enviament) les va desencuant i enviant al seu ritme. Requisits:
- Cap notificació no es perd.
- S'envien en l'ordre en què es van generar (seria absurd rebre "tasca completada" abans que "tasca creada").
- Encuar ha de ser instantani, perquè passa enmig de l'acció de l'usuari.
Els requisits 1 i 2 els garanteix el contracte FIFO. El requisit 3 és una exigència de cost: encuar i desencuar han de ser O(1). I aquí ve l'avís per a la propera lliçó: la implementació "òbvia" amb una list de Python incompleix aquest requisit, per la mateixa trampa del pop(0) que vam mesurar amb timeit al mòdul 1. Resoldre-ho amb elegància —reutilitzant la LlistaEnllacada del mòdul 2— és l'objectiu de la lliçó 04-02.
El mapa del mòdul 4
Perquè sàpigues què t'espera, aquest és el pla del mòdul, i no és casual: cada lliçó respon a un dels ponts del mòdul 3.
| Lliçó | Estructura | Pont que tanca |
|---|---|---|
| 04-02 | Cua FIFO ben implementada | L'"ordre just d'arribada" |
| 04-03 | Cua circular (ring buffer) | Buffers de mida fixa |
| 04-04 | Cua de prioritat | El min-stack que no podia extreure |
| 04-05 | Deque (cua doble) | L'HistorialAmbLimit i el seu pop(0) |
| 04-06 | Exercicis integradors | Tot l'anterior, junt |
Errors Comuns i Consells
- Confondre la política amb l'estructura: FIFO és una política (un contracte); la cua es pot implementar de moltes maneres (llista enllaçada, dues piles, array circular). No diguis "una cua és una llista": una cua és un contracte que una llista pot (bé o malament) implementar.
- Usar una pila on toca una cua (o viceversa): el codi no fallarà, però l'ordre d'atenció serà l'invers de l'esperat. Si processes notificacions amb una pila, l'usuari rebrà primer l'última generada. Pregunta't sempre: ha de sortir "el més recent" o "el més antic"?
- Esperar que la cua FIFO respecti prioritats: no ho fa, i no ho ha de fer. Si una tasca de
prioritat1 ha d'avançar les altres, necessites una cua de prioritat (04-04), no un FIFO amb pedaços. - Consell: quan llegeixis documentació de sistemes (missatgeria, sistemes operatius, xarxes), busca les paraules queue, enqueue, dequeue, front/head i rear/tail. Reconèixer el contracte sota noms diferents és senyal que has interioritzat el TAD.
Exercicis
Exercici 1: predir la sortida
Sense executar res, indica què imprimeix aquest codi (assumint una Cua que compleix el contracte):
cua = Cua()
cua.encuar("A")
cua.encuar("B")
print(cua.desencuar())
cua.encuar("C")
print(cua.front())
print(cua.mida())
print(cua.desencuar())
print(cua.desencuar())Exercici 2: pila o cua?
Per a cada escenari de TaskFlow, decideix si l'estructura adequada és una pila (LIFO) o una cua (FIFO), i justifica-ho en una frase:
- Reproduir els canvis d'una tasca en ordre cronològic per a auditoria.
- Desfer les últimes modificacions de la descripció d'una tasca.
- Repartir les peticions d'exportació d'informes entre els usuaris, atenent-les amb justícia.
- Comprovar que els parèntesis d'una fórmula de filtre estan balancejats.
Exercici 3: dissenyar un contracte
La impressora de l'oficina necessita, a més del contracte bàsic de cua, una operació perquè un administrador cancel·li totes les feines pendents. Escriu la taula de contracte completa (operació, què fa, què promet) d'aquesta CuaImpressio, sense implementar-la. Pista: cancel·lar-ho tot no ha d'obligar a desencuar en bucle des de fora.
Solucions
Solució 1:
A # desencuar retorna el més antic
B # front: després de sortir "A", el més antic és "B" (no l'extreu)
2 # queden "B" i "C"
B
CLa clau és a la tercera línia: front() no extreu, per això després desencuar() continua retornant "B".
Solució 2:
- Cua: l'auditoria exigeix ordre cronològic d'arribada (FIFO).
- Pila: desfer atén sempre el més recent (LIFO), com el
GestorDesferReferdel mòdul 3. - Cua: "amb justícia" = per ordre d'arribada, ningú no es cola.
- Pila: cada tancament casa amb l'obertura més recent, exactament el
filtre_balancejatdel mòdul 3.
Solució 3:
| Operació | Què fa | Què promet |
|---|---|---|
encuar(treball) |
Afegeix una feina al final | S'imprimirà després de les ja encuades |
desencuar() |
Extreu la feina del front | És la més antiga; excepció si és buida |
front() |
Consulta la propera feina | No modifica la cua; excepció si és buida |
esta_buida() |
Hi ha feines pendents? | True/False |
mida() |
Feines pendents | Enter ≥ 0 |
cancelar_tot() |
Buida la cua d'una vegada | Deixa mida() == 0; mai no falla (buidar una cua buida és vàlid) |
Fixa't que ampliar un contracte és legítim; l'important és prometre amb precisió què fa la nova operació, casos frontera inclosos.
Conclusió
Hem definit la cua com a TAD: un contracte FIFO amb encuar, desencuar, front, esta_buida i mida, on els elements entren pel final i surten pel front, garantint l'ordre d'arribada. L'hem contrastada punt per punt amb la pila, hem vist que sosté sistemes reals (cues de missatges, buffers, planificadors) i hem identificat el seu paper a TaskFlow: la cua de notificacions pendents d'enviar, que exigeix a més costos O(1) a tots dos extrems. Just aquí hi ha el repte: la implementació ingènua sobre list cau a la trampa del pop(0) que ja coneixem. A la propera lliçó executarem les operacions pas a pas, mesurarem aquesta trampa i construirem la classe Cua correcta reutilitzant la LlistaEnllacada del mòdul 2 — amb una sorpresa final: una cua feta amb dues piles.
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
