Al llarg del curs hem anat deixant afirmacions a compte: que imbricar dos bucles quadruplica la feina en doblar les dades (03-03), que la cerca binària resol en vint comparacions el que a la lineal li costa un milió (06-01), que sorted és 3.600 vegades més ràpid que la nostra bombolla (06-02) i que el Fibonacci ingenu «explota» (06-03). Totes parlen del mateix: quant costa un programa segons la mida de les seves dades. Aquesta lliçó et dona per fi el vocabulari per dir-ho amb precisió.
Aquest vocabulari s'anomena notació Big-O, i és una d'aquelles eines que et canvien la manera de mirar el codi: així que la tens, pots obrir una funció que no has escrit tu i saber, sense executar-la, si aguantarà mil dades o dues-centes mil. També aprendràs el contrari, que importa igual: quan no cal optimitzar res.
Contingut
- Per què no es mesura en segons
- Què es compta al seu lloc
- La notació Big-O i com se simplifica
- Catàleg de complexitats
- La taula del creixement
- Com analitzar un fragment de codi
- Tres fragments de TascaFàcil analitzats
- Pitjor cas, millor cas i cas mitjà
- Complexitat en espai i l'intercanvi temps-memòria
- Cost de les operacions habituals en Python
- Quan optimitzar i quan no
- TascaFàcil amb 20 tasques i amb 200.000
- Errors comuns i consells
- Exercicis
- Conclusió del mòdul
- Per què no es mesura en segons
A 06-02 vam mesurar la bombolla amb time.perf_counter() i van sortir 11 segons per a 10.000 elements. És una dada útil, però no serveix per comunicar res fora d'aquell moment concret, perquè depèn de:
- La màquina: un portàtil de fa vuit anys i un servidor modern difereixen en un factor de deu.
- El llenguatge i la seva versió: la mateixa bombolla en C és unes cinquanta vegades més ràpida que en Python.
- El que estigués fent l'ordinador en aquell instant: un altre programa, l'antivirus, el navegador.
- Les dades concretes que li van tocar: una llista gairebé ordenada o una del revés donen temps diferents.
Si et dic «la meva funció triga 0,4 segons», no saps si és bona. Si et dic «la meva funció dobla el seu temps cada vegada que dobla el nombre de tasques», ja saps tot el que importa: saps que amb 200.000 tasques anirà 10.000 vegades més lenta que amb 20, i això és cert en qualsevol ordinador i en qualsevol llenguatge.
Aquest és el canvi de mentalitat: en comptes de mesurar quant triga, descrivim com creix allò que triga quan creixen les dades. És una propietat de l'algorisme, no de la màquina.
- Què es compta al seu lloc
Es compten operacions elementals en funció de la mida de l'entrada, que per conveni s'anomena n. Una operació elemental és aquella el cost de la qual no depèn de la mida de les dades: una suma, una comparació, una assignació, accedir a llista[i], cridar una funció.
def comptar_pendents(agenda): # n = len(agenda)
total = 0 # 1 operacio, es fa una vegada
for tasca in agenda: # el bucle fa n voltes
if not tasca["completada"]: # 1 comparacio per volta -> n
total += 1 # 1 suma per volta (com a molt) -> n
return total # 1 operacioSumant: 1 + n + n + 1, és a dir, 2n + 2 operacions. L'important no és el nombre exacte —depèn de com comptis— sinó la forma de l'expressió: hi ha un terme que creix amb n i uns afegits que no. I el primer que cal entendre és que n no és «quantes dades hi ha» en abstracte, sinó la mida d'allò que fa créixer la feina: aquí, el nombre de tasques de l'agenda.
- La notació Big-O i com se simplifica
La notació Big-O (o notació asimptòtica) descriu com creix el nombre d'operacions quan n es fa gran, ignorant tot allò que deixa d'importar a aquella escala. S'escriu O(...) i es llegeix «de l'ordre de».
Formalment és una fita superior: dir que un algorisme és O(n) significa que el seu cost no creix més ràpid que n, tret d'un factor constant. I d'aquí surten les dues regles de simplificació:
- S'ignoren les constants multiplicatives.
3ninsón tots dosO(n): un algorisme tres vegades més lent continua doblant el seu temps en doblar les dades. La constant depèn de la màquina; la forma de créixer, no. - S'ignoren els termes menors. A
n² + 500n + 1000, quannval un milió,n²és un bilió i500nsón cinc-cents milions: el segon terme és el 0,05 % del total. QuedaO(n²).
| Operacions comptades | Big-O | Per què |
|---|---|---|
2n + 2 |
O(n) |
Se'n van la constant 2 i el +2 |
3n + 5 |
O(n) |
Igual: creix proporcional a n |
n² + 500n + 1000 |
O(n²) |
n² domina tota la resta |
100 |
O(1) |
No depèn de n en absolut |
5n log n + 3n |
O(n log n) |
n log n creix més que n |
La conseqüència pràctica d'aquesta màniga ampla és que Big-O no compara dos algorismes amb la mateixa forma: entre dos algorismes O(n), un pot ser cinc vegades més ràpid que l'altre i Big-O no ho dirà. Per a això hi ha time.perf_counter(). Big-O respon una altra pregunta, molt més important: què passarà quan les dades creixin?
- Catàleg de complexitats
Aquestes sis cobreixen pràcticament tot el que et trobaràs, i de totes ja en tens un exemple en aquest curs:
| Complexitat | Nom | Exemple del curs | En doblar n… |
|---|---|---|---|
O(1) |
Constant | llista[5], dic["Marta"], .append() |
no canvia |
O(log n) |
Logarítmica | Cerca binària (06-01) | afegeix una operació |
O(n) |
Lineal | Recórrer l'agenda, cerca lineal | es dobla |
O(n log n) |
Quasilineal | sorted, mergesort (06-02, 06-03) |
poc més que el doble |
O(n²) |
Quadràtica | Bucles imbricats (03-03), bombolla | es quadruplica |
O(2ⁿ) |
Exponencial | Fibonacci ingenu (06-03) | s'eleva al quadrat |
Val la pena aturar-se en dues. O(1) no significa «ràpid», significa «el mateix cost sigui quina sigui la mida»: accedir a llista[999999] costa el mateix que a llista[0], perquè Python calcula l'adreça de memòria en comptes de recórrer. I O(log n) és gairebé tan bo com O(1): el logaritme en base 2 d'un milió és 20, i el de mil milions és 30. Un algorisme logarítmic amb prou feines nota que les dades creixin.
A l'altre extrem, O(2ⁿ) és un mur: cada element nou duplica la feina total. Amb n = 60 no hi ha ordinador al món que acabi, i per això el Fibonacci ingenu era inservible. Ordenats de millor a pitjor:
graph LR
A["O(1)"] --> B["O(log n)"] --> C["O(n)"] --> D["O(n log n)"]
D --> E["O(n quadrat)"] --> F["O(2 elevat a n)"]
- La taula del creixement
Els números són més eloqüents que les corbes. Operacions aproximades per a cada mida:
n |
O(1) |
O(log n) |
O(n) |
O(n log n) |
O(n²) |
O(2ⁿ) |
|---|---|---|---|---|---|---|
| 10 | 1 | 3 | 10 | 33 | 100 | 1.024 |
| 1.000 | 1 | 10 | 1.000 | 10.000 | 1.000.000 | inabastable |
| 1.000.000 | 1 | 20 | 1.000.000 | 20.000.000 | 1 bilió | inabastable |
Traduïm l'última fila a temps, suposant deu milions d'operacions per segon, que és un ordre de magnitud raonable per a Python:
| Complexitat | Amb n = 1.000.000 |
|---|---|
O(log n) |
instantani |
O(n) |
0,1 segons |
O(n log n) |
2 segons |
O(n²) |
més d'un dia |
Aquí hi ha tota la lliçó en una taula. Amb un milió de dades, un algorisme O(n log n) respon mentre esperes i un O(n²) no acaba mai a la pràctica. I fixa't en una cosa crucial: amb n = 10 tots són instantanis. La complexitat només importa quan les dades creixen, i aquesta observació serà la secció 11.
- Com analitzar un fragment de codi
Tres regles pràctiques basten per al 95 % dels casos:
- Instruccions soltes (assignacions, comparacions, accessos per índex o per clau):
O(1). - Els bucles seqüencials se sumen; els bucles imbricats es multipliquen. Dos bucles seguits de
nvoltes sónn + n = 2n→O(n). Un bucle denvoltes dins d'un altre denvoltes sónn × n→O(n²). - Es pren el terme dominant. Si un fragment fa un
sorted(O(n log n)) i després un recorregut (O(n)), el total ésO(n log n), perquè mana el major.
def analitza_aixo(agenda):
total = 0 # O(1)
for t in agenda: # bucle 1: O(n)
total += t["dies"]
for t in agenda: # bucle 2: O(n), SEQUENCIAL -> se suma
print(t["titol"])
for a in agenda: # bucle exterior: n voltes
for b in agenda: # bucle interior: n voltes -> es multiplica
if a["responsable"] == b["responsable"]:
pass
return totalEl càlcul és 1 + n + n + n² = n² + 2n + 1, que se simplifica a O(n²). I aquesta és la lliçó de disseny més rendible de l'anàlisi: el parell de bucles imbricats domina sobre tota la resta. Optimitzar els dos primers bucles no serviria de res; l'únic canvi que importa és eliminar la imbricació, cosa que saps fer des de 06-01 amb un índex.
Hi ha dos paranys freqüents en analitzar. El primer: una crida a una funció té el cost de la funció, no O(1). Si dins d'un bucle de n voltes crides sorted sobre l'agenda, el total és n × n log n, no n. El segon: l'operador in sobre una llista és O(n), així que un if x in llista dins d'un bucle és un bucle imbricat disfressat.
- Tres fragments de TascaFàcil analitzats
Fragment 1: resum_per_responsable (des de la v0.10), que compta les tasques de cada persona amb un diccionari.
recompte = {}
for tasca in agenda: # n voltes
nom = tasca["responsable"] # O(1)
recompte[nom] = recompte.get(nom, 0) + 1 # O(1): taula hashUn bucle de n voltes amb feina O(1) a dins: O(n). És òptim, perquè per comptar cal mirar totes les tasques almenys una vegada. La clau és que recompte.get(...) sigui O(1); si en comptes d'un diccionari féssim servir una llista de noms i un .index(), cada volta seria O(n) i el total O(n²).
Fragment 2: mostrar_llistat (v0.13), que ordena abans d'imprimir.
sorted és O(n log n) i el recorregut és O(n); el terme dominant mana: O(n log n). No es pot fer millor mentre calgui ordenar, i aquest és l'argument honest per no obsessionar-se: la part cara no és el teu codi, és l'ordenació, i l'ordenació ja la fa Timsort.
Fragment 3: detectar títols duplicats, escrit de la manera ingènua.
duplicats = []
for i, a in enumerate(agenda): # n voltes
for b in agenda[i + 1:]: # fins a n voltes -> imbricat
if a["titol"] == b["titol"]:
duplicats.append(a["titol"])Bucles imbricats: O(n²). Amb 20 tasques són unes 190 comparacions, res. Amb 200.000 serien vint mil milions: hores d'espera. La versió amb conjunt, que ja saps escriure des de 05-03, resol el mateix en O(n):
vistos, duplicats = set(), []
for tasca in agenda: # n voltes
if tasca["titol"] in vistos: # O(1): pertinenca en conjunt
duplicats.append(tasca["titol"])
vistos.add(tasca["titol"]) # O(1)Mateix resultat, un sol bucle. Aquest és l'exemple perfecte de per què la lliçó importa: el segon codi no és més llest, és més barat, i la diferència només es veu si saps comptar.
- Pitjor cas, millor cas i cas mitjà
Un mateix algorisme pot costar coses molt diferents segons les dades concretes que li toquin. Es distingeixen tres escenaris, i ja els vas veure a la cerca lineal de 06-01:
| Cas | Què és | Cerca lineal | Inserció (06-02) |
|---|---|---|---|
| Millor | Les dades més favorables | O(1): és el primer |
O(n): ja està ordenada |
| Mitjà | Dades aleatòries típiques | O(n): la meitat de la llista |
O(n²) |
| Pitjor | Les dades més desfavorables | O(n): no hi és |
O(n²): al revés |
Quan algú diu «aquest algorisme és O(x)» sense precisar, es refereix al pitjor cas. És la convenció, i té una raó sòlida: el pitjor cas és una garantia. Si el pitjor cas és O(n log n), saps que mai no trigarà més, passin les dades que passin. El cas mitjà és una expectativa, no una promesa, i hi ha algorismes famosos —el quicksort de 06-02— amb un cas mitjà excel·lent i un pitjor cas dolent.
- Complexitat en espai i l'intercanvi temps-memòria
El temps no és l'únic que es consumeix: també hi ha complexitat en espai, que mesura quanta memòria extra necessita un algorisme a més de les dades d'entrada.
| Algorisme | Espai extra | Per què |
|---|---|---|
| Bombolla, selecció, inserció | O(1) |
Ordenen al lloc, només variables soltes |
sorted() |
O(n) |
Construeix una llista nova |
| Mergesort (06-03) | O(n) |
Necessita llistes auxiliars per barrejar |
Recursivitat de profunditat n |
O(n) |
Una entrada de pila per crida pendent |
| Índex invertit (06-01) | O(n) |
Un diccionari amb totes les tasques |
| Memoïtzació de Fibonacci (06-03) | O(n) |
Un resultat desat per cada n |
Les dues últimes files són el famós intercanvi temps-memòria: es gasta memòria per guanyar temps. El cas de la memoïtzació és demolidor: passa Fibonacci de O(2ⁿ) a O(n) en temps, a canvi de O(n) en espai. És dels millors negocis que existeixen en programació.
L'índex invertit és el mateix tracte en petit: index_per_responsable costa O(n) construir-lo i O(n) de memòria, i a canvi cada consulta «què té el Luis?» passa de O(n) a O(1). Amb una consulta no compensa; amb quinze al dia, es paga sol. I hi ha una tercera dimensió que el tracte ignora i convé no oblidar: l'índex s'ha de mantenir actualitzat, i aquest cost és de manteniment del codi, no de temps d'execució.
- Cost de les operacions habituals en Python
Aquesta taula és de les coses més útils que t'enduràs del mòdul. Amb n = nombre d'elements:
| Operació | Llista | Diccionari / Conjunt |
|---|---|---|
Accés per posició x[i] |
O(1) |
— |
Accés per clau d[k] |
— | O(1) |
x in colleccio |
O(n) |
O(1) |
.append(x) / .add(x) |
O(1) |
O(1) |
.insert(0, x) |
O(n) |
— |
.pop() (l'últim) |
O(1) |
— |
.pop(0) (el primer) |
O(n) |
— |
del d[k] / .remove(x) |
O(n) |
O(1) |
.sort() / sorted() |
O(n log n) |
— |
Recórrer amb for |
O(n) |
O(n) |
len() |
O(1) |
O(1) |
Les files en negreta són les que causen problemes reals. insert(0, x) i pop(0) són O(n) perquè una llista desa els seus elements consecutius en memòria: ficar o treure pel principi obliga a desplaçar tots els altres. Un bucle que fa pop(0) n vegades és O(n²) sense que ho sembli. La solució de la biblioteca estàndard és collections.deque, una llista doblement enllaçada on tots dos extrems són O(1).
I x in llista és O(n) mentre que x in conjunt és O(1), que és la traducció a aquest vocabulari de tot el que vam veure a 06-01. Canviar una llista per un conjunt és l'optimització més barata i més rendible que existeix: una línia de codi i un canvi de complexitat.
- Quan optimitzar i quan no
Aquí arriba la part que gairebé mai no s'explica, i és tan important com la resta. La majoria de les optimitzacions no calen.
- Mentre
nsigui petit, la claredat mana. Amb 20 tasques, un algorismeO(n²)fa 400 operacions: microsegons. Reescriure aquest codi per fer-loO(n)el torna més difícil de llegir a canvi d'un estalvi que ningú no percebrà. - Mesura abans de tocar. La intuïció sobre on és la lentitud és notòriament dolenta. Fes servir
time.perf_counter()al voltant dels trossos sospitosos i descobreix el coll d'ampolla en comptes de suposar-lo. A la majoria de programes reals, el temps se'n va en la xarxa o el disc, no en el teu bucle. - Optimitza la complexitat, no les constants. Si cal millorar alguna cosa, passar de
O(n²)aO(n)canviant una llista per un conjunt val mil vegades més que estalviar tres operacions dins d'un bucle. - Però tria bé des del principi quan és gratis. Escriure
if x in conjunten comptes deif x in llistano costa esforç extra ni llegibilitat. Això no és optimitzar prematurament: és no espatllar el programa per descuit.
La regla que resumeix tot això és de Donald Knuth i té mig segle: l'optimització prematura és l'arrel de tots els mals. Escriu codi clar i correcte; mesura; i optimitza només el que la mesura assenyali. Big-O no hi és perquè ho optimitzis tot, sinó perquè sàpigues què passarà quan les dades creixin i detectis a temps el que no aguantarà.
- TascaFàcil amb 20 tasques i amb 200.000
Analitzem el programa sencer, que va per la v0.14, en els dos escenaris.
| Operació | Complexitat | Amb n = 20 |
Amb n = 200.000 |
|---|---|---|---|
Registrar una tasca (.append) |
O(1) |
instantani | instantani |
| Mostrar el llistat (ordena) | O(n log n) |
instantani | ≈ 1 segon |
| Cercar per títol (lineal) | O(n) |
instantani | ≈ 0,05 s |
| Cercar per responsable amb índex | O(n) construir + O(1) consultar |
instantani | ≈ 0,1 s cada vegada |
| Resum per responsable | O(n) |
instantani | ≈ 0,1 s |
| Desar / carregar el JSON | O(n) |
instantani | uns quants segons |
| Detectar duplicats (versió ingènua) | O(n²) |
190 comparacions | 20.000 milions |
Amb 20 tasques, tot està bé i no hi ha res per canviar. Aquest és el veredicte honest, i no una concessió: el programa és clar, correcte i respon a l'instant. Optimitzar-lo seria feina malgastada.
Amb 200.000 tasques, en canvi, l'anàlisi diu exactament què tocar i en quin ordre:
- Eliminar qualsevol
O(n²), començant per la detecció de duplicats: la versió amb conjunt de la secció 7 ho arregla en quatre línies. És l'única cosa veritablement urgent. - Construir l'índex una sola vegada en arrencar i mantenir-lo al dia a cada alta i cada baixa, en lloc de reconstruir-lo a cada consulta. Passa les cerques per responsable de
O(n)aO(1). - No ordenar l'agenda sencera per mostrar vint línies. Si el llistat es pagina,
heapq.nsmallest(20, agenda, key=clau_ordre)ésO(n log 20)en comptes deO(n log n). - Deixar de carregar i desar el fitxer complet. Un JSON de 200.000 tasques es llegeix sencer en memòria cada vegada; a aquesta escala la resposta no és un algorisme millor, és una base de dades, que indexa, cerca i desa només el que canvia.
Fixa't en l'ordre: primer es treuen les complexitats dolentes, després s'apliquen intercanvis temps-memòria i només al final es canvia d'eina. I fixa't en el que no hi apareix: reescriure sorted, microoptimitzar les f-strings o treure funcions per estalviar crides. Això no mouria l'agulla.
Errors Comuns i Consells
Confondre «ràpid» amb «bona complexitat». Un algorisme O(n²) ben escrit pot guanyar-ne un O(n log n) per a n petit, perquè les constants que Big-O ignora existeixen de debò. Timsort, sense anar més lluny, ordena els trams curts amb inserció justament per això.
Oblidar el cost del que es crida. if titol in [t["titol"] for t in agenda] sembla una línia innocent i és O(n) cada vegada: construeix una llista sencera i la recorre. Dins d'un bucle, O(n²).
Creure que O(1) significa instantani i O(n²) significa inacceptable. Són formes de créixer, no velocitats. O(n²) amb n = 20 és perfecte; O(n) amb una operació costosa a dins pot ser un desastre.
Analitzar el millor cas sense dir-ho. «La meva cerca és O(1) perquè normalment és al principi» és una mitja veritat. Per conveni es parla del pitjor cas, que és el que dona garanties.
Optimitzar sense mesurar. Canviar codi «perquè sembla lent» sol empitjorar la llegibilitat sense millorar el temps. Mesura, localitza i llavors actua.
Consell: aprèn a veure els bucles imbricats. No sempre estan indentats l'un dins de l'altre. Un in sobre una llista, un .index(), una comprensió o una crida a una altra funció que recorre són bucles ocults. Pregunta't sempre: aquesta línia recorre alguna cosa?
Consell: el millor canvi sol ser d'estructura de dades, no d'algorisme. Llista → conjunt per a pertinença; llista → diccionari per cercar per clau. Una línia, i la complexitat baixa un esglaó sencer.
Exercicis
Exercici 1: Determinar la complexitat
Indica la complexitat en temps de cada fragment, en funció de n = len(agenda), i justifica la resposta en una frase.
# (a)
primera = agenda[0]["titol"]
# (b)
for t in agenda:
if t["titol"] in [x["titol"] for x in agenda]:
print(t["titol"])
# (c)
noms = {t["responsable"] for t in agenda}
for nom in noms:
print(nom)
# (d)
copia = sorted(agenda, key=lambda t: t["dies"])
for t in copia[:5]:
print(t["titol"])Exercici 2: Rebaixar la complexitat
Aquesta funció comprova quines tasques d'una llista d'encàrrecs nous ja existeixen a l'agenda. Digues la seva complexitat, reescriu-la perquè sigui O(n + m) (sent m el nombre d'encàrrecs nous) i indica quanta memòria extra gasta la nova versió.
def ja_registrades(agenda, encarrecs):
repetides = []
for encarrec in encarrecs:
for tasca in agenda:
if tasca["titol"] == encarrec:
repetides.append(encarrec)
break
return repetidesExercici 3: Mesurar i comprovar la predicció
Escriu una funció que mesuri amb time.perf_counter() quant triga x in llista davant de x in conjunt cercant un element que no hi és, per a n = 10.000 i n = 100.000. Prediu abans d'executar què li passarà a cada temps en multiplicar n per deu, i comprova si has encertat.
Solucions
Solució 1.
| Fragment | Complexitat | Motiu |
|---|---|---|
| (a) | O(1) |
Accés per índex i per clau, sense recórrer res |
| (b) | O(n²) |
La comprensió construeix i recorre una llista de n elements a cada volta |
| (c) | O(n) |
Construir el conjunt és O(n) i recórrer-lo, O(n): se sumen |
| (d) | O(n log n) |
sorted domina; el tall de 5 i el seu bucle són O(1) |
El (b) és el cas més instructiu: no hi ha dos bucles indentats a la vista, però la comprensió de dins és el bucle interior. La versió correcta construeix el conjunt de títols una sola vegada, fora del bucle, i queda en O(n).
Solució 2.
La versió original és O(n × m): per cada encàrrec recorre l'agenda sencera. La reescriptura canvia el bucle interior per un conjunt:
def ja_registrades(agenda, encarrecs):
"""Retorna els encarrecs el titol dels quals ja existeix a l'agenda."""
titols = {tasca["titol"] for tasca in agenda} # O(n), una sola vegada
return [e for e in encarrecs if e in titols] # O(m): cada 'in' es O(1)Construir el conjunt costa O(n) i la comprensió O(m), així que el total és O(n + m). Amb 200.000 tasques i 100 encàrrecs, es passa de 20 milions de comparacions a 200.100 operacions: unes cent vegades menys. El cost és O(n) de memòria extra —el conjunt de títols—, i és l'intercanvi temps-memòria en la seva forma més neta: una línia de més, un esglaó de complexitat menys.
Solució 3.
import time
def comparar_pertinenca(n):
"""Compara el cost de cercar un element absent en llista i en conjunt."""
llista = list(range(n))
conjunt = set(llista)
absent = n + 1 # segur que no hi es
inici = time.perf_counter()
absent in llista # recorre els n elements
t_llista = time.perf_counter() - inici
inici = time.perf_counter()
absent in conjunt # calcula la casella i prou
t_conjunt = time.perf_counter() - inici
print(f"n={n:>7} llista={t_llista:.6f}s conjunt={t_conjunt:.8f}s")
for n in (10_000, 100_000):
comparar_pertinenca(n)La predicció és directa a partir de la taula de la secció 10: in sobre una llista és O(n), així que en multiplicar n per deu el temps es multiplica per deu; sobre un conjunt és O(1), així que no canvia. En executar-ho veuràs justament això: la llista passa d'unes dècimes de mil·lisegon a uns quants mil·lisegons, mentre que el conjunt es queda clavat en l'ordre de la milionèsima de segon, mesuri el que mesuri. Cercar l'element absent és deliberat: és el pitjor cas de la llista, que obliga a recórrer-la sencera, i en el conjunt no canvia res. Guarda aquest experiment: és la demostració pràctica de tot el mòdul en vint línies.
Conclusió del mòdul
L'eficiència no es mesura en segons perquè els segons depenen de la màquina, del llenguatge i de la sort. Es descriu comptant operacions elementals en funció de n i quedant-se amb com creixen: això és la notació Big-O, una fita superior en què s'ignoren les constants multiplicatives i els termes menors, de manera que 3n + 5 és O(n) i n² + 500n és O(n²). El catàleg cap en una línia —O(1), O(log n), O(n), O(n log n), O(n²), O(2ⁿ)— i totes tenen el seu exemple en aquest curs, des de l'accés a un diccionari fins al Fibonacci ingenu. Per analitzar codi basten tres regles: les instruccions soltes són O(1), els bucles seqüencials se sumen i els imbricats es multipliquen, i mana el terme dominant. Per conveni es parla del pitjor cas, perquè és l'únic que ofereix garanties. I al costat del temps hi ha la complexitat en espai, amb el seu intercanvi temps-memòria: memoïtzació i índexs invertits gasten O(n) de memòria per rebaixar el temps un esglaó sencer. La taula de costos de llistes, diccionaris i conjunts —amb in en O(n) davant d'O(1), i els insert(0, x) i pop(0) que són O(n) sense semblar-ho— és la xuleta que faràs servir cada dia. Però la conclusió més important és la contrària: mentre n sigui petit, la claredat mana; mesura abans de tocar i optimitza només el que la mesura assenyali.
Amb això es tanca el mòdul 6, i amb ell les caixes negres que arrossegàvem. Saps cercar: linealment amb sortida primerenca, binàriament descartant meitats sobre dades ordenades i per clau gràcies a la taula hash, a més de preparar índexs invertits quan una consulta es repeteix. Saps ordenar: selecció, inserció i bombolla amb el seu sentinella, la diferència entre estable i inestable, l'estratègia de divisió que hi ha darrere de mergesort i quicksort, i l'ús professional de sorted/.sort() amb key, reverse i tuples per a diversos criteris. Saps fer servir la recursivitat amb el seu cas base i el seu cas recursiu, reconèixer quan guanya —dades amb forma d'arbre, algorismes que divideixen— i arreglar la seva feina repetida amb memoïtzació. I ara saps dir quant costa tot això. TascaFàcil ha arribat a la v0.14: cerca, ordena per dos criteris, reparteix el pla de demà i suma projectes desglossats a qualsevol profunditat.
I tanmateix, mira què sosté tot això: una llista de diccionaris. Res no impedeix que a un diccionari li falti la clau completada, que un altre tingui prioritat escrita com a "Alta", o que un tercer arrossegui un camp inventat que ningú més no entén; el programa no protestarà fins que rebenti per un KeyError a mitja llista. I les funcions que operen sobre tasques —mostrar_fitxa, dies_totals, clau_ordre— són escampades pel fitxer, separades de les dades que manipulen, sense res que digui que formen un conjunt. A De les dades als objectes: classes i instàncies comença el mòdul 7, on dades i comportament deixen d'anar per separat: aprendràs a definir què és exactament una tasca, garantir que totes neixen completes i desar al seu costat les operacions que els pertanyen.
Fonaments de la Programació
Mòdul 1: Introducció a la Programació
- Què és la programació?
- Història de la programació
- Llenguatges de programació
- Entorns de desenvolupament
- Del problema a l'algorisme
Mòdul 2: Conceptes Bàsics
- Variables i tipus de dades
- Operadors i expressions
- Entrada i sortida de dades
- Conversió de tipus i validació de dades
Mòdul 3: Estructures de Control
Mòdul 4: Funcions i Procediments
- Definició i ús de funcions
- Paràmetres i retorn de valors
- Àmbit de variables
- Descompondre un programa en funcions
- Funcions com a valors: lambda i ordre superior
Mòdul 5: Estructures de Dades
- Llistes i arrays
- Cadenes de caràcters
- Diccionaris i conjunts
- Tuples i estructures imbricades
- Desar dades en fitxers: text, CSV i JSON
Mòdul 6: Algorismes Bàsics
Mòdul 7: Objectes i Organització del Codi
- De les dades als objectes: classes i instàncies
- Atributs, mètodes i constructor
- Col·leccions d'objectes
- Mòduls, paquets i importacions
Mòdul 8: Bones Pràctiques i Eines
- Documentació i comentaris
- Depuració i gestió d'errors
- Control de versions
- Proves automatitzades
- Estil, llegibilitat i refactorització
