A la lliçó anterior vam descobrir que la cerca binària resol en vint comparacions el que a la lineal li costa un milió, però exigeix un requisit que TascaFàcil encara no compleix: que les dades estiguin ordenades. També va quedar dit que ordenar costa. És hora d'esbrinar quant i com, obrint la segona caixa negra del curs: què fa sorted per dins.

Ordenar és, probablement, l'operació més rendible que aprendràs. No només perquè un llistat ordenat es llegeix millor: una col·lecció ordenada habilita la cerca binària, permet detectar duplicats d'un cop d'ull, fa trivials els informes per rangs i converteix «els cinc encàrrecs més urgents» en un tall. En aquesta lliçó implementaràs tres algorismes clàssics amb la seva traça pas a pas, entendràs què significa que una ordenació sigui estable i acabaràs fent servir l'eina de debò —sorted i .sort()— sabent per fi què passa a dins.

Contingut

  1. Què significa ordenar i per què compensa
  2. El criteri d'ordre
  3. Estabilitat: per què importa a Estudi Alba
  4. Ordenació per selecció
  5. Ordenació per inserció
  6. Ordenació per bombolla i el sentinella
  7. Els tres algorismes, comparats
  8. Algorismes per divisió: mergesort i quicksort
  9. Ordenar en Python de debò
  10. Mesurar la diferència amb time.perf_counter()
  11. TascaFàcil v0.13: prioritat, dies i el pla de demà
  12. Errors comuns i consells
  13. Exercicis
  14. Conclusió

  1. Què significa ordenar i per què compensa

Ordenar és reorganitzar els elements d'una col·lecció de manera que cadascun sigui «menor o igual» que el següent segons un criteri. La definició amaga dues decisions que cal prendre abans d'escriure una sola línia: què es compara i què es fa amb els empats. La primera és el criteri d'ordre, la segona és l'estabilitat, i les dues seccions següents se n'ocupen.

El que fa de l'ordenació una inversió i no una despesa és tot allò que habilita després:

Amb la col·lecció ordenada… …això passa a ser fàcil
Cerca binària Localitzar un element en 20 comparacions en comptes d'un milió
Els primers elements «Els cinc encàrrecs més urgents» és un tall [:5]
Duplicats Els iguals queden junts: n'hi ha prou de comparar amb el veí
Informes i llistats Surten llegibles i agrupats sense feina extra
Fusionar dues col·leccions Es recorren en paral·lel, una sola passada

Per això ordenar un cop i consultar moltes vegades gairebé sempre guanya a consultar sense ordenar. És el mateix raonament de l'índex invertit de 06-01: pagar un cost inicial perquè tot el que vingui després surti barat.

  1. El criteri d'ordre

Els nombres i els textos tenen un ordre natural (3 < 7, "Luis" < "Marta" per l'ordre dels caràcters que vas veure a 05-02), però un diccionari no: ningú no sap si «la tasca del cartell» és major o menor que «la del menú». El criteri d'ordre és la regla que converteix cada element en una cosa comparable.

tasques = [{"titol": "Cartell fira", "dies": 3}, {"titol": "Menu Sole", "dies": 5}]
per_dies = sorted(tasques, key=lambda t: t["dies"])       # criteri: els dies
per_titol = sorted(tasques, key=lambda t: t["titol"])     # criteri: el titol

Aquest key és el callback de 04-05: una funció que rep un element i retorna el valor pel qual es compara. Tots els algorismes d'aquesta lliçó s'escriuen primer comparant nombres, perquè així es veu el mecanisme, i després es generalitzen amb key. La idea clau és que l'algorisme d'ordenació i el criteri són coses separades: el mateix algorisme ordena per dies, per títol o per prioritat canviant només la funció de comparació.

  1. Estabilitat: per què importa a Estudi Alba

Una ordenació és estable quan els elements que empaten conserven l'ordre que tenien abans. Sona a subtilesa acadèmica fins que se'n veu un cas real.

La Marta ordena l'agenda per dies estimats, de menys a més, per veure primer el que es despatxa ràpid:

Títol Prioritat Dies
Pressupost març mitjana 1
Logotip Vidal alta 2
Cartell fira alta 3
Menú Forn Solé mitjana 5

Després la reordena per prioritat. Amb una ordenació estable, dins d'«alta» es manté l'ordre anterior: primer Logotip Vidal (2 dies) i després Cartell fira (3). El resultat és el que la Marta volia: ordenat per prioritat i, a igualtat de prioritat, per dies. Amb una ordenació inestable, aquestes dues tasques podrien sortir en qualsevol ordre i la feina de la primera ordenació es perdria.

D'aquí la tècnica clàssica: per ordenar per diversos criteris, s'ordena pel menys important primer i pel més important al final, i l'estabilitat conserva l'anterior. A la secció 9 veuràs l'alternativa moderna, que encara és més clara. I retén la dada: sorted() i .sort() de Python són estables, sempre, i això ho garanteix el llenguatge.

  1. Ordenació per selecció

La idea és la que faries servir amb una mà de cartes: buscar la més petita de totes, posar-la la primera; buscar la més petita de les que queden, posar-la la segona, i així successivament.

def ordenar_seleccio(valors):
    """Ordena la llista al lloc, de menor a major, per seleccio."""
    n = len(valors)
    for i in range(n - 1):                  # posicio que anem a omplir
        minim = i                           # suposem que el minim es l'actual
        for j in range(i + 1, n):           # busquem un de menor mes endavant
            if valors[j] < valors[minim]:
                minim = j
        if minim != i:
            valors[i], valors[minim] = valors[minim], valors[i]          # intercanvi
    return valors

Tres coses per entendre aquí. El bucle exterior recorre les posicions que es van fixant; arriba fins a n - 1 perquè quan queda un sol element ja està col·locat per força. El bucle interior és la cerca del mínim del patró de 03-02, però desant la posició i no el valor, perquè cal intercanviar. I l'intercanvi a, b = b, a és el desempaquetatge de tuples de 05-04 fent la seva feina sense variable auxiliar.

Traça sobre els dies estimats [3, 5, 2, 4, 1]:

Passada i Tros examinat Mínim trobat Intercanvi Llista després
1 0 3 5 2 4 1 1 (posició 4) 3 ↔ 1 1 5 2 4 3
2 1 5 2 4 3 2 (posició 2) 5 ↔ 2 1 2 5 4 3
3 2 5 4 3 3 (posició 4) 5 ↔ 3 1 2 3 4 5
4 3 4 5 4 (posició 3) cap 1 2 3 4 5

Fixa't en la passada 4: la llista ja estava ordenada i tot i així l'algorisme va fer la passada completa. La selecció no s'assabenta mai que ha acabat: sempre fa exactament el mateix nombre de comparacions, estigui la llista ordenada o del revés. A canvi, fa molt pocs intercanvis: un per passada com a màxim, cosa que la fa interessant quan moure un element és car.

I un advertiment important per a la secció 7: la selecció clàssica no és estable, precisament per aquest intercanvi a distància, que pot saltar un element igual per damunt d'un altre.

  1. Ordenació per inserció

És la que fa servir tothom en ordenar cartes a la mà: s'agafa la següent i es col·loca al seu lloc entre les que ja estan ordenades, desplaçant les majors cap a la dreta.

def ordenar_insercio(valors):
    """Ordena la llista al lloc, de menor a major, per insercio."""
    for i in range(1, len(valors)):         # el primer ja esta "ordenat" ell sol
        actual = valors[i]                  # la carta que tenim a la ma
        j = i - 1
        while j >= 0 and valors[j] > actual:
            valors[j + 1] = valors[j]       # desplacem el major una casella
            j -= 1
        valors[j + 1] = actual              # i deixem la carta al seu forat
    return valors

La part delicada és el while, i convé llegir-lo amb calma. Retrocedeix des de la posició anterior mentre es compleixin dues condicions: que no ens hàgim sortit per l'esquerra (j >= 0) i que l'element examinat sigui major que el que portem a la mà. L'ordre d'aquestes dues condicions no és casual: gràcies a l'avaluació mandrosa de l'and que vas veure a 02-02, si j arriba a -1 la segona comparació ni s'avalua, i així no s'accedeix a valors[-1], que en Python seria l'últim element i produiria un error silenciós.

Traça sobre [3, 5, 2, 4, 1]; la part ja ordenada va en negreta:

Passada actual Desplaçaments Llista després
1 5 cap (5 > 3) 3 5 2 4 1
2 2 5 i 3 a la dreta 2 3 5 4 1
3 4 5 a la dreta 2 3 4 5 1
4 1 5, 4, 3 i 2 a la dreta 1 2 3 4 5

Aquí hi ha la gran virtut de la inserció, i és la raó que continuï viva al programari professional: amb dades gairebé ordenades és rapidíssima. Si cada element ja és a prop del seu lloc, el while fa una volta o cap i l'ordenació sencera costa poc més que un recorregut. Sobre una llista ja ordenada, la inserció fa una sola comparació per element i cap desplaçament: és el millor cas possible. I és estable, perquè el while s'atura així que troba un element igual i mai no el salta.

  1. Ordenació per bombolla i el sentinella

La bombolla compara veïns i intercanvia els que estan del revés, passada rere passada, fins que no en queda cap de desordenat. A cada passada el major dels que queden «flota» fins al final, d'aquí el nom.

def ordenar_bombolla(valors):
    """Ordena la llista al lloc, de menor a major, per bombolla amb sentinella."""
    n = len(valors)
    for passada in range(n - 1):
        hi_ha_hagut_canvi = False                  # el sentinella (bandera de 03-02)
        for j in range(n - 1 - passada):           # el de la dreta ja esta col.locat
            if valors[j] > valors[j + 1]:
                valors[j], valors[j + 1] = valors[j + 1], valors[j]
                hi_ha_hagut_canvi = True
        if not hi_ha_hagut_canvi:                  # una passada neta: ja esta ordenada
            break
    return valors

Dues millores conviuen en aquest codi. El range(n - 1 - passada) escurça cada passada, perquè després de la primera l'últim element ja és el major i no cal tornar-lo a mirar. I hi_ha_hagut_canvi és el sentinella: el patró bandera de 03-02 aplicat aquí. Si una passada completa no intercanvia res, la llista està ordenada i el break surt del bucle. Sense sentinella, la bombolla faria sempre totes les passades encara que la llista arribés ja ordenada.

Traça sobre [3, 5, 2, 4, 1], mostrant l'estat al final de cada passada:

Passada Comparacions i intercanvis Llista al final hi_ha_hagut_canvi
1 3-5 no; 5-2 sí; 5-4 sí; 5-1 sí 3 2 4 1 5 True
2 3-2 sí; 3-4 no; 4-1 sí 2 3 1 4 5 True
3 2-3 no; 3-1 sí 2 1 3 4 5 True
4 2-1 sí 1 2 3 4 5 True

Amb cinc elements, range(n - 1) dona quatre passades i aquí s'esgoten totes. Però si la llista d'entrada fos [1, 2, 3, 4, 5], la primera passada no intercanviaria res, hi_ha_hagut_canvi continuaria en False i el break acabaria la feina amb quatre comparacions en total. Aquest és tot el valor del sentinella.

La bombolla és estable —només intercanvia veïns estrictament desordenats, mai iguals— i és, a la pràctica, l'algorisme més lent dels tres, perquè fa moltíssims intercanvis. S'ensenya perquè el seu mecanisme es veu d'un cop d'ull, no perquè es faci servir.

  1. Els tres algorismes, comparats

Selecció Inserció Bombolla
Comparacions (5 elements, pitjor cas) 10 fins a 10 fins a 10
Comparacions si ja està ordenada 10 (sempre les mateixes) 4 4 (amb sentinella)
Intercanvis / desplaçaments Com a molt 4 Molts Moltíssims
Amb dades gairebé ordenades Igual de lenta Excel·lent Bona amb sentinella
És estable? No
Detecta que ja està ordenada? No Sí, implícitament Sí, amb el sentinella
Es fa servir a la pràctica per a… Gairebé res Llistes petites o gairebé ordenades Ensenyar

Els tres tenen en comú una cosa que es veu al codi: dos bucles imbricats. Recorda el que es va dir a 03-03: amb dos bucles imbricats, doblar les dades quadruplica la feina. Deu elements són unes cent operacions; mil elements, un milió. Aquest és el seu sostre, i a Eficiència i notació Big-O li posarem nom formal.

Si t'haguessis de quedar amb un per implementar-lo a mà, és la inserció: és estable, és simple i és la millor amb dades gairebé ordenades, que és la situació més habitual a la vida real (una agenda a la qual s'afegeix una tasca al final ja està gairebé ordenada).

  1. Algorismes per divisió: mergesort i quicksort

Els tres algorismes anteriors comparteixen un sostre del qual no es pot baixar amb la seva estratègia. Per superar-lo cal canviar d'idea: en comptes de recórrer la llista un cop i un altre, partir-la en trossos, ordenar cada tros i combinar els resultats. És l'estratègia anomenada divideix i venceràs, i d'ella surten els dos algorismes que es fan servir de debò. L'ordenació per barreja (mergesort) parteix la llista per la meitat, ordena cada meitat i fusiona les dues meitats ordenades en una sola passada; és estable i el seu rendiment no depèn de les dades d'entrada. L'ordenació ràpida (quicksort) tria un element com a pivot, col·loca a l'esquerra els menors i a la dreta els majors, i repeteix a cada costat; és més ràpida a la pràctica, però no és estable i té un cas dolent.

La diferència d'escala és brutal: on la bombolla necessita un milió d'operacions per a mil elements, aquests en necessiten unes deu mil. Tots dos es recolzen en el fet que un algorisme es crida a si mateix sobre els trossos més petits, així que necessiten una eina que encara no tens. La coneixeràs a la lliçó següent, i allà implementarem el mergesort complet amb la seva traça: la idea de dividir es reprèn a Recursivitat.

  1. Ordenar en Python de debò

Tot l'anterior existeix perquè entenguis el mecanisme. En un programa real es fa servir el que porta el llenguatge, i Python porta dues maneres d'ordenar que convé no confondre:

sorted(colleccio) colleccio.sort()
Què retorna Una llista nova ordenada None: modifica al lloc
Original Intacte Queda ordenat
Serveix per a Llistes, tuples, cadenes, diccionaris… Només llistes
Quan fer-lo servir Si necessites conservar l'original Si vols ordenar la llista i prou
from operator import itemgetter

dies = [3, 5, 2, 4, 1]
print(sorted(dies))                       # [1, 2, 3, 4, 5] -- dies segueix intacta
print(sorted(dies, reverse=True))         # [5, 4, 3, 2, 1] -- de major a menor
dies.sort()                               # ara dies SI queda ordenada; retorna None

per_dies = sorted(agenda, key=lambda t: t["dies"])          # amb lambda (04-05)
per_dies = sorted(agenda, key=itemgetter("dies"))           # equivalent i mes rapid
per_dos = sorted(agenda, key=itemgetter("prioritat", "dies"))   # dos criteris

operator.itemgetter("dies") construeix una funció que fa exactament el mateix que lambda t: t["dies"], però està escrita en C i es llegeix millor quan hi ha diversos camps. I aquesta última línia és la tècnica moderna per ordenar per diversos criteris: la clau retorna una tupla, i Python compara tuples element a element —primer el primer, i només si empaten mira el segon—, que és justament el que significa «per prioritat i, dins de la mateixa, per dies».

ORDRE_PRIORITAT = {"alta": 0, "mitjana": 1, "baixa": 2}
llistat = sorted(agenda, key=lambda t: (ORDRE_PRIORITAT[t["prioritat"]], t["dies"]))

Aquest diccionari ORDRE_PRIORITAT, que TascaFàcil ja tenia des de la v0.10, resol un problema real: alfabèticament «alta» va abans que «baixa» i que «mitjana», cosa que és pura coincidència i no l'ordre que volem. Traduir cada prioritat a un nombre imposa l'ordre lògic en comptes de l'alfabètic. Si a més volguessis invertir només un dels dos criteris i l'altre no, el truc habitual amb nombres és negar-los: (-t["dies"], t["titol"]) ordena per dies de major a menor i, en els empats, per títol de la A a la Z.

I què hi ha dins de sorted? Un algorisme anomenat Timsort, escrit per Tim Peters per a Python el 2002 i adoptat després per Java i Android. És un híbrid: detecta els trams que ja estan ordenats a les dades reals —que gairebé sempre n'hi ha—, ordena els trams curts amb inserció, la mateixa de la secció 5, i els fusiona amb la tècnica del mergesort. És estable i aprofita l'ordre preexistent, així que sobre una llista gairebé ordenada s'acosta a una sola passada.

  1. Mesurar la diferència amb time.perf_counter()

Discutir sense mesurar és opinar. time.perf_counter() retorna un nombre de segons d'alta precisió; la diferència entre dues lectures és el temps transcorregut.

import random, time

def mesurar(funcio, dades):
    """Retorna els segons que triga funcio a ordenar una copia de dades."""
    copia = list(dades)                      # copia: cada mesura parteix del mateix
    inici = time.perf_counter()
    funcio(copia)
    return time.perf_counter() - inici

for n in (1000, 10000):
    dades = [random.randint(1, 100000) for _ in range(n)]
    print(f"n={n}  bombolla={mesurar(ordenar_bombolla, dades):.4f}s  "
          f"sorted={mesurar(sorted, dades):.4f}s")

La còpia amb list(dades) és imprescindible: com que aquests algorismes ordenen al lloc, sense ella la segona mesura rebria una llista ja ordenada i el resultat seria fals. Resultats aproximats en un portàtil corrent —els teus variaran, però les proporcions es mantindran:

Elements Bombolla pròpia sorted (Timsort) Quantes vegades més ràpid
1.000 ≈ 0,09 s ≈ 0,0002 s unes 450 vegades
10.000 ≈ 11 s ≈ 0,003 s unes 3.600 vegades

Llegeix la taula en vertical, que és on hi ha la lliçó. En multiplicar per deu les dades, sorted passa de 0,0002 a 0,003 segons —unes quinze vegades més—, mentre que la bombolla passa de 0,09 a 11 segons, més de cent vegades més. Aquesta és la diferència entre un algorisme amb dos bucles imbricats i un que divideix, i per això cap quantitat de trucs de programació salvarà un algorisme mal triat.

La moralitat de sempre, aquesta vegada avalada per números: en producció es fa servir sorted() o .sort(). Estan escrits en C, són estables, aprofiten l'ordre preexistent i cap implementació teva no se'ls acostarà. El d'aquí dalt s'implementa per entendre què fan per dins i per saber triar.

  1. TascaFàcil v0.13: prioritat, dies i el pla de demà

Apliquem el que hem après en dos llocs. El llistat passa a ordenar-se per dos criteris amb una clau de tupla, i afegim l'informe que la Marta demana cada tarda: què fa demà cada membre de l'equip. El menú passa a nou opcions.

# tascafacil.py - Estudi Alba / Versio 0.13: ordenar com cal
OPCIONS = ("1", "2", "3", "4", "5", "6", "7", "8", "9")
ORDRE_PRIORITAT = {"alta": 0, "mitjana": 1, "baixa": 2}
# --- Resta de constants i funcions: sense canvis respecte a la v0.12 ---

def clau_ordre(tasca):
    """Criteri del llistat: primer per prioritat, i a igual prioritat, per dies."""
    return (ORDRE_PRIORITAT[tasca["prioritat"]], tasca["dies"], tasca["titol"])

def mostrar_llistat(agenda):
    """Mostra l'agenda ordenada per prioritat i, dins de cadascuna, per dies."""
    if not agenda:
        print("L'agenda es buida.")
        return
    print("-" * AMPLE)
    for numero, tasca in enumerate(sorted(agenda, key=clau_ordre), start=1):
        estat = "OK" if tasca["completada"] else "  "
        print(f"{numero:>2}. [{estat}] {tasca['titol']:<28}"
              f"{tasca['responsable']:<10}{tasca['prioritat']:<8}{tasca['dies']:>2}d")
    print("-" * AMPLE)

def pla_de_dema(agenda):
    """Mostra en que treballara dema cada membre de l'equip."""
    index = index_per_responsable(agenda)            # l'index de 06-01
    print("PLA DE DEMA".center(AMPLE))
    for nom in EQUIP:
        pendents = sorted([t for t in index.get(nom, []) if not t["completada"]],
                          key=clau_ordre)
        if not pendents:
            print(f"{nom:<10} sense tasques pendents: pot assumir feina nova.")
            continue
        seguent = pendents[0]                        # la primera es la mes urgent
        resta = sum(t["dies"] for t in pendents[1:])
        print(f"{nom:<10} {seguent['titol']:<28}"
              f"({seguent['prioritat']}, {seguent['dies']}d)  "
              f"+{len(pendents) - 1} tasques / {resta}d en cua")

# A main(): l'opcio 8 crida pla_de_dema(agenda) i la sortida passa a ser la 9.

Les decisions de disseny que val la pena assenyalar:

  • clau_ordre és una funció amb nom, no una lambda. Es fa servir en dos llocs diferents i mereix un docstring que expliqui el criteri; és exactament el límit que vam marcar a 04-05 per ascendir una lambda a def.
  • La clau retorna una tupla de tres elements, amb el títol com a tercer criteri de desempat. Així el llistat surt sempre igual davant de les mateixes dades, sense dependre de l'ordre de registre. Un llistat que canvia d'ordre sense motiu desconcerta l'usuari.
  • pla_de_dema combina les dues lliçons del mòdul: l'índex invertit de 06-01 per agrupar per persona i l'ordenació per dos criteris per triar la tasca més urgent de cadascun. pendents[0] és la resposta a «per on començo demà?» precisament perquè la llista està ordenada.
  • S'ordena en mostrar, no en desar. L'agenda viu en l'ordre en què es va registrar; l'ordre és una decisió de presentació. Ordenar la llista real obligaria a reordenar-la després de cada canvi i perdria l'ordre de registre, que és una dada en si mateixa.

Errors Comuns i Consells

Esperar que .sort() retorni la llista ordenada. llista_ordenada = agenda.sort() deixa llista_ordenada valent None, i l'error apareix més tard i lluny, quan alguna cosa intenta recórrer aquest None. La regla: .sort() modifica, sorted() retorna.

Ordenar alfabèticament allò que té un ordre propi. sorted(agenda, key=lambda t: t["prioritat"]) posa «alta», «baixa» i «mitjana» en aquest ordre, que no és el que vols. Tradueix a nombres amb un diccionari com ORDRE_PRIORITAT.

Modificar la llista mentre s'ordena o es recorre. Afegir o esborrar elements dins del bucle que la recorre produeix resultats impredictibles i elements saltats. Construeix una llista nova i substitueix-la al final.

Confondre l'element amb la seva posició a l'ordenació per selecció. Si deses minim = valors[j] en comptes de minim = j, després no sabràs on era i l'intercanvi serà impossible.

Oblidar la còpia en mesurar temps. Si mesures dos algorismes sobre la mateixa llista, el segon rep dades ja ordenades i sembla miraculosament ràpid. list(dades) abans de cada mesura.

Consell: ordena un cop, no a cada consulta. Si el llistat es demana deu vegades sense que l'agenda canviï, ordena un cop i desa el resultat. I si necessites mantenir una col·lecció sempre ordenada mentre insereixes, bisect.insort de 06-01 col·loca cada element al seu lloc sense reordenar res.

Consell: si només necessites els millors, no ordenis. Per a «les tres tasques més urgents» d'una llista enorme, heapq.nsmallest(3, agenda, key=clau_ordre) és més barat que ordenar-ho tot i quedar-te amb [:3].

Exercicis

Exercici 1: Traçar inserció i bombolla

Traça en dues taules l'ordenació de la llista [4, 1, 5, 2] amb inserció (una fila per passada, indicant l'element a la mà i la llista resultant) i amb bombolla amb sentinella (una fila per passada, indicant intercanvis i el valor del sentinella). Indica quantes comparacions fa cadascuna i quantes en faria la bombolla si la llista entrés ja ordenada.

Exercici 2: Ordenació per selecció amb criteri

Adapta ordenar_seleccio perquè accepti un paràmetre clau —una funció, com el key de sorted— amb valor per defecte que deixi l'element tal qual, i ordeni comparant clau(element). Després, fes-la servir per ordenar l'agenda per dies i per títol, i comprova amb sorted que el resultat coincideix.

Exercici 3: Detectar si ja està ordenada

Escriu esta_ordenada(valors, clau=None) que retorni True si la llista ja està ordenada de menor a major segons aquest criteri, recorrent-la una sola vegada i sortint així que trobi un parell desordenat. Després, escriu ordenar_si_cal(agenda), que faci servir l'anterior per no ordenar en va, i informi per pantalla del que ha fet.

Solucions

Solució 1. Inserció sobre [4, 1, 5, 2]:

Passada actual Desplaçaments Llista després Comparacions
1 1 4 a la dreta 1 4 5 2 1
2 5 cap 1 4 5 2 1
3 2 5 i 4 a la dreta 1 2 4 5 3

Cinc comparacions en total. Bombolla amb sentinella sobre la mateixa llista:

Passada Intercanvis Llista al final hi_ha_hagut_canvi
1 4↔1; 5↔2 1 4 2 5 True
2 4↔2 1 2 4 5 True
3 cap 1 2 4 5 Falsebreak

Tres més dos més un: sis comparacions. Si la llista entrés ja ordenada, la primera passada faria tres comparacions, cap intercanvi, i el sentinella tallaria aquí: tres comparacions en total.

Solució 2.

def ordenar_seleccio(valors, clau=None):
    """Ordena la llista al lloc per seleccio, comparant clau(element)."""
    if clau is None:
        clau = lambda x: x                  # per defecte, l'element tal qual
    n = len(valors)
    for i in range(n - 1):
        minim = i
        for j in range(i + 1, n):
            if clau(valors[j]) < clau(valors[minim]):
                minim = j
        if minim != i:
            valors[i], valors[minim] = valors[minim], valors[i]
    return valors

copia = list(agenda)
ordenar_seleccio(copia, clau=lambda t: t["dies"])
print(copia == sorted(agenda, key=lambda t: t["dies"]))   # True si no hi ha empats

El canvi és mínim —tres aparicions de clau(...) a la comparació— i converteix un algorisme que només ordenava nombres en un que ordena qualsevol cosa: és el patró callback de 04-05 un altre cop. Atenció a l'última línia: la comparació amb sorted dona True si no hi ha empats en els dies; si n'hi ha pot donar False, i no perquè el resultat estigui malament, sinó perquè la nostra selecció no és estable i sorted sí. És la millor manera de veure l'estabilitat amb els teus propis ulls.

Solució 3.

def esta_ordenada(valors, clau=None):
    """Indica si la llista ja esta ordenada de menor a major segons el criteri."""
    if clau is None:
        clau = lambda x: x
    for i in range(len(valors) - 1):
        if clau(valors[i]) > clau(valors[i + 1]):
            return False                    # sortida primerenca: un parell mal posat ja basta
    return True

def ordenar_si_cal(agenda):
    """Retorna l'agenda ordenada pel criteri del llistat, sense treballar en va."""
    if esta_ordenada(agenda, clau=clau_ordre):
        print("L'agenda ja estava ordenada.")
        return agenda
    print("Reordenant l'agenda...")
    return sorted(agenda, key=clau_ordre)

esta_ordenada és una cerca lineal de 06-01 disfressada: busca el primer parell desordenat i surt així que el troba. Comprovar costa un sol recorregut, moltíssim menys que ordenar, de manera que la comprovació prèvia surt rendible sempre que hi hagi una probabilitat raonable que ja estigui ordenada. I fixa't que ordenar_si_cal retorna l'agenda en tots dos camins, ordenada o no: una funció que de vegades retorna alguna cosa i de vegades no és una font inesgotable d'errors, com es va dir a 04-02.

Conclusió

Ordenar és reorganitzar segons un criteri d'ordre, aquesta funció key que diu què es compara de cada element, i amb una propietat que decideix què passa amb els empats: l'estabilitat, que conserva l'ordre previ dels iguals i permet ordenar per diversos criteris encadenant ordenacions. Has implementat i traçat els tres algorismes clàssics: la selecció, que busca el mínim i el col·loca, fa pocs intercanvis però sempre la mateixa feina i no és estable; la inserció, que col·loca cada element al seu lloc dins de la part ja ordenada, és estable i excel·lent amb dades gairebé ordenades; i la bombolla, que intercanvia veïns i amb el sentinella sap aturar-se quan ja no queda res per fer, però continua sent la més lenta. Els tres comparteixen dos bucles imbricats i, amb ells, un sostre: mil elements són un milió d'operacions.

Aquest sostre només es trenca canviant d'estratègia, dividint la llista en trossos com fan mergesort i quicksort. Mentrestant, en producció es fa servir sorted() —que retorna una llista nova— o .sort() —que modifica al lloc i retorna None—, amb reverse, amb key (una lambda o un operator.itemgetter) i, per a diversos criteris alhora, amb una tupla com a clau. Per dins porten Timsort, un híbrid estable d'inserció i barreja que aprofita els trams ja ordenats; i les mesures amb time.perf_counter() han posat números a la diferència: 3.600 vegades més ràpid que la nostra bombolla amb 10.000 elements. TascaFàcil arriba a la v0.13 amb el llistat ordenat per prioritat i dies i amb el pla de demà que la Marta reparteix cada tarda.

Queda una peça pendent, i apareix als dos llocs on ens hem aturat. La cerca binària de 06-01 tenia una versió més elegant que no podíem escriure, i el mergesort d'aquesta lliçó necessita ordenar dues meitats que són, al seu torn, llistes per ordenar. Tots dos demanen el mateix: una funció que es cridi a si mateixa. A Recursivitat veuràs com una funció pot resoldre un problema resolent versions més petites d'ell mateix, quines són les dues peces que mai no poden faltar i per què la recursivitat, mal utilitzada, repeteix feina fins a tornar-se inservible.

© Copyright 2026. Tots els drets reservats