Salta al contingut
Llegat fiber

Recorreguts, cerques i modificació de llistes

Els esquemes que faràs servir a tots els problemes de llistes: recórrer-la sencera, buscar-hi un element, modificar-la sense perdre l'iterador, treballar amb dos iteradors i fusionar dues llistes ordenades.

Conceptes clau

  • Esquema de recorregut
  • Esquema de cerca
  • Inserir ordenadament
  • Dos iteradors: capicua
  • Fusió de llistes ordenades en Θ(n+m)\Theta(n + m)
  • splice en Θ(1)\Theta(1)

Per què esquemes?

Gairebé tots els problemes de llistes que trobaràs (a l’examen, al laboratori, al Jutge) són variacions de quatre o cinc idees. Si les reconeixes, el codi surt gairebé sol. Si cada vegada comences de zero, t’equivocaràs en els detalls: un iterador que avança de més, un end() desreferenciat, un element que no es comprova.

Aquest capítol recull aquests esquemes, amb un exemple de cadascun. Al tema de disseny iteratiu els tornaràs a veure, amb vectors i justificant per què són correctes.

1. Recorregut: visitar-los tots

Quan has de mirar tots els elements (sumar-los, comptar-ne, escriure’ls, modificar-los tots):

for (list<T>::const_iterator it = l.begin(); it != l.end(); ++it) {
    tractar *it
}

(Dins d’una plantilla, has d’escriure typename list<T>::const_iterator, o simplement auto.)

// Pre: cert. Post: retorna la suma dels elements de l.
int suma(const list<int>& l) {
    int s = 0;
    for (list<int>::const_iterator it = l.begin(); it != l.end(); ++it) s += *it;
    return s;
}
it = l.begin(): s = 0417endits += *it; ++it: s = 4417endits += *it; ++it: s = 5417endits += *it; ++it: s = 12417endit
suma de [4, 1, 7]. A cada volta se suma l'element de it i it segueix la fletxa al següent. Quan it arriba a end(), el bucle s'acaba: ja s'han sumat tots (en corall).

Si només llegeixes, també pots escriure for (int x : l) s += x;. Per modificar els elements, cal un iterator normal (i la llista per referència):

// Pre: l = [a1, ..., an]. Post: l = [k*a1, ..., k*an].
void multiplica(list<int>& l, int k) {
    for (list<int>::iterator it = l.begin(); it != l.end(); ++it) *it *= k;
}

2. Cerca: aturar-se quan el trobes

Quan busques un element que compleixi una condició i, quan el trobes, ja no cal mirar-ne més:

list<T>::const_iterator it = l.begin();
while (it != l.end() and not condició(*it)) ++it;
// aquí: o bé it == l.end() (no hi és), o bé *it compleix la condició
// Pre: cert. Post: retorna si hi ha algun element negatiu a l.
bool hi_ha_negatiu(const list<int>& l) {
    list<int>::const_iterator it = l.begin();
    while (it != l.end() and *it >= 0) ++it;
    return it != l.end();
}
trobat: [3, 8, -2, 5]38-25enditno trobat: [3, 8, 5]385endit
Les dues maneres d'acabar una cerca. A dalt, it s'atura al -2 i no mira el 5. A baix, cap no és negatiu i it arriba a end().

Dues coses que has de mirar sempre en una cerca:

  • L’ordre de la condició del while. Primer it != l.end(), després *it. L’and s’atura si la primera part és falsa, així que mai no es desreferencia end(). Si les gires, quan l’element no hi és, *it es fa amb it == l.end().
  • Què vol dir cada sortida. Després del bucle, si it != l.end(), l’has trobat i it hi apunta. Pots fer-lo servir (per consultar-lo, modificar-lo, esborrar-lo o inserir-hi davant).

3. Cercar i modificar: inserir ordenadament

Una cerca que acaba amb una modificació. Tens una llista de puntuacions ordenada de gran a petit (un rànquing) i n’arriba una de nova. Has de trobar el primer element més petit o igual que la nova i inserir-la davant.

// Pre: l està ordenada de gran a petit.
// Post: s'hi ha afegit x i continua ordenada de gran a petit.
void insereix_ordenat(list<int>& l, int x) {
    list<int>::iterator it = l.begin();
    while (it != l.end() and *it > x) ++it;
    l.insert(it, x);
}
Pas a pasinsereix_ordenat(l, 80)
list<int>::iterator it = l.begin();
while (it != l.end() and *it > x) ++it;
l.insert(it, x);
90757540endit

it comença al principi, al 90.

90757540endit

90 > 80: encara no és el lloc. ++it.

90757540endit

75 > 80? No: el bucle s'atura. it apunta al primer element que no és més gran que 80.

9080757540endit

insert posa el 80 davant del 75. La llista continua ordenada, i it continua al 75.

Fixa’t que funciona sense cap cas especial:

  • Si x és la més gran de totes, el bucle no avança i s’insereix al principi.
  • Si és la més petita, it arriba a end() i s’insereix al final.
  • Si la llista és buida, it == l.end() des del principi i x passa a ser l’únic element.

Això és gràcies a com està definit insert: insereix davant de l’iterador, i davant d’end() vol dir al final.

4. Dos iteradors

De vegades cal recórrer la llista des de dues bandes alhora. Una llista és capicua si es llegeix igual en tots dos sentits: [4,8,5,8,4][4, 8, 5, 8, 4], [4,8,8,4][4, 8, 8, 4] i [7][7] ho són. Posem un iterador al principi i un altre al final, i els anem acostant:

// Pre: cert. Post: retorna si l es llegeix igual d'esquerra a dreta que de dreta a esquerra.
bool capicua(const list<int>& l) {
    list<int>::const_iterator ini = l.begin();
    list<int>::const_iterator fi = l.end();
    for (int i = 0; i < int(l.size()) / 2; ++i) {
        --fi;
        if (*ini != *fi) return false;
        ++ini;
    }
    return true;
}
Pas a pascapicua([4, 8, 5, 8, 4])
list<int>::const_iterator ini = l.begin();
list<int>::const_iterator fi = l.end();
for (int i = 0; i < int(l.size()) / 2; ++i) {
  --fi;
  if (*ini != *fi) return false;
  ++ini;
}
return true;
48584endinifi

ini al primer element i fi a end(), que no és cap element.

48584endinifi

Primera volta: --fi el porta a l'últim element.

48584endinifi

4 i 4: iguals. Seguim.

48584endinifi

++ini avança cap al mig.

48584endinifi

Segona volta: --fi i es comparen 8 i 8. Iguals.

48584endinifi

++ini. Ja s'han fet les 2 voltes (5 / 2, arrodonit cap avall).

48584endinifi

El 5 del mig no cal comparar-lo amb ell mateix: és capicua.

fi comença a end() i retrocedeix abans de mirar-lo, perquè end() no és cap element. Amb nn elements es fan ⌊n/2⌋\lfloor n/2 \rfloor comparacions.

5. Fusió de llistes ordenades

Tens dues llistes ordenades de petit a gran i vols afegir a la primera tots els elements de la segona, mantenint l’ordre.

La manera lenta: per a cada element de b, insereix_ordenat a a. Cada inserció recorre a des del principi: amb nn elements a a i mm a b, fins a Θ(n⋅m)\Theta(n \cdot m). Amb dues llistes de 10.000 elements, cent milions de passos.

La manera ràpida: com que totes dues estan ordenades, recorrem-les alhora amb dos iteradors. L’element de b que toca inserir va davant del primer element d’a que no és més petit. I l’element següent de b serà més gran, així que no cal tornar enrere a a.

// Pre: a i b estan ordenades de petit a gran.
// Post: a conté tots els elements de a i de b, ordenats; b no canvia.
void fusiona(list<int>& a, const list<int>& b) {
    list<int>::iterator ia = a.begin();
    list<int>::const_iterator ib = b.begin();
    while (ia != a.end() and ib != b.end()) {
        if (*ib < *ia) {
            a.insert(ia, *ib);  // el de b va davant del de a
            ++ib;
        }
        else ++ia;
    }
    while (ib != b.end()) {     // si en queden a b, van al final (ia == a.end())
        a.insert(ia, *ib);
        ++ib;
    }
}
Pas a pasfusiona([1, 4, 4, 9], [2, 3, 4, 10, 12])
while (ia != a.end() and ib != b.end()) {
  if (*ib < *ia) {
      a.insert(ia, *ib);
      ++ib;
  }
  else ++ia;
}
while (ib != b.end()) {
  a.insert(ia, *ib);
  ++ib;
}
a1449endiab2341012endib

ia i ib comencen al principi de cada llista.

a1449endiab2341012endib

2 < 1? No: ia avança.

a12449endiab2341012endib

2 < 4: el 2 de b entra a a davant de ia, i ib avança.

a123449endiab2341012endib

3 < 4: el 3 de b entra a a davant de ia, i ib avança.

a123449endiab2341012endib

4 < 4? No: ia avança.

a123449endiab2341012endib

4 < 4? No: ia avança.

a1234449endiab2341012endib

4 < 9: el 4 de b entra a a davant de ia, i ib avança.

a1234449endiab2341012endib

10 < 9? No: ia avança.

a1234449endiab2341012endib

ia ha arribat a end(): el primer bucle s'acaba. A b encara en queden.

a123444910endiab2341012endib

El 10 va al final de a (inserir davant d'end()).

a12344491012endiab2341012endib

El 12 va al final de a (inserir davant d'end()).

a12344491012endiab2341012endib

ib és a end(): fet. a té els nou elements en ordre, i cada iterador ha avançat sempre endavant.

Cada volta del primer bucle avança un dels dos iteradors, i el segon bucle avança ib: en total, com a molt n+mn + m passos. Cost Θ(n+m)\Theta(n + m). És la mateixa idea que la fusió de cues.

6. splice: moure una llista sencera

a.splice(it, b) mou tots els elements de b dins d’a, davant de it. b queda buida.

list<int> x = {1, 2, 3};
list<int> y = {10, 20};
list<int>::iterator it = x.begin();
++it;               // it apunta al 2
x.splice(it, y);    // x = [1, 10, 20, 2, 3], y buida, it continua al 2
x abans123endity abans1020endx després de x.splice(it, y)1102023endity desprésend
Els nodes del 10 i del 20 passen de y a x, davant del 2. No es copia cap element: només canvien les fletxes dels extrems. y es queda només amb el final.

El gran avantatge: costa Θ(1)\Theta(1), sigui quina sigui la mida de les llistes. No es copia cap element: només es reenganxen els extrems (veuràs com al tema de punters). Per concatenar dues llistes: a.splice(a.end(), b).

Crear llistes noves

Si vols conservar la llista original i obtenir-ne una de nova, tens dues opcions:

// Una funció que la retorna:
// Pre: cert. Post: retorna una llista amb els elements positius de l, en el mateix ordre.
list<int> positius(const list<int>& l);

// Una acció amb un paràmetre de sortida:
// Pre: r és buida. Post: r conté els elements positius de l, en el mateix ordre.
void positius(const list<int>& l, list<int>& r);

La funció, per exemple, és un recorregut que va afegint al final:

list<int> positius(const list<int>& l) {
    list<int> r;
    for (int x : l) {
        if (x > 0) r.push_back(x);
    }
    return r;
}

Totes dues són correctes. La funció és més còmoda d’usar (list<int> p = positius(l);), i els compiladors actuals eviten copiar la llista que retornes. L’acció et dona més control: per exemple, pots afegir els resultats a una llista que ja tenies.

Errors habituals

Exercicis

ExerciciPer sobre de la mitjana

Escriu una funció que retorni quants elements d’una llista no buida són estrictament més grans que la mitjana de la llista. Amb [1,2,3,10][1, 2, 3, 10], la mitjana és 4 i n’hi ha un per sobre (el 10). Quin esquema fas servir? Quantes vegades has de recórrer la llista?

Mostra la solució

Calen dos recorreguts: la mitjana no es coneix fins que s’han vist tots els elements, i fins llavors no saps quins hi són per sobre.

// Pre: l no és buida. Post: retorna quants elements de l són més grans que la mitjana.
int per_sobre_mitjana(const list<int>& l) {
    double mitjana = double(suma(l)) / l.size();
    int n = 0;
    for (int x : l) {
        if (x > mitjana) ++n;
    }
    return n;
}

Tots dos són recorreguts (s’han de mirar tots els elements). Cost: Θ(n)+Θ(n)=Θ(n)\Theta(n) + \Theta(n) = \Theta(n).

Compte amb la mitjana: suma(l) / l.size() seria una divisió entera, i a més size() retorna un enter sense signe. Amb [−1,−2][-1, -2], la suma −3-3 es converteix en un nombre positiu enorme i la «mitjana» no té cap sentit. Si converteixes la suma a double abans de dividir, t’estalvies tots dos problemes.

ExerciciPosició d'un element

Escriu una funció que retorni la posició (començant per 0) de la primera aparició de x en una llista, o −1-1 si no hi és. Quin esquema fas servir?

Mostra la solució

Una cerca: quan el trobem, parem. Com que una llista no té posicions, les comptem nosaltres mentre avancem.

// Pre: cert. Post: retorna la posició (començant per 0) de la primera aparició de x a l,
//      o -1 si no hi és.
int posicio(const list<int>& l, int x) {
    int pos = 0;
    list<int>::const_iterator it = l.begin();
    while (it != l.end() and *it != x) {
        ++it;
        ++pos;
    }
    if (it == l.end()) return -1;
    return pos;
}

Cost: Θ(p)\Theta(p) si el troba a la posició pp, i Θ(n)\Theta(n) si no hi és. En el cas pitjor, Θ(n)\Theta(n).

ExerciciTreu els repetits seguits

Escriu una acció que esborri de la llista tots els elements que són iguals a l’anterior. Per exemple, [1,1,2,1,1,1,3,3][1, 1, 2, 1, 1, 1, 3, 3] ha de quedar [1,2,1,3][1, 2, 1, 3]. Ha de costar Θ(n)\Theta(n).

Mostra la solució

Fem servir dos iteradors: ant apunta a l’últim element que es queda, i it a l’element que estem mirant. Si *it és igual que *ant, l’esborrem (i erase ja ens deixa al següent); si no, es queda i passa a ser el nou ant.

// Pre: cert. Post: s'han esborrat els elements iguals a l'anterior.
void treu_repetits_seguits(list<int>& l) {
    if (l.empty()) return;
    list<int>::iterator ant = l.begin();
    list<int>::iterator it = ant;
    ++it;
    while (it != l.end()) {
        if (*it == *ant) it = l.erase(it);
        else {
            ant = it;
            ++it;
        }
    }
}
  • La comprovació de la llista buida és necessària: sense, ++it sobre begin() == end() seria avançar des d’end().
  • ant mai no s’esborra, així que sempre és vàlid.

Cada volta avança it o l’esborra (cosa que també el fa avançar): Θ(n)\Theta(n).

ExerciciCapicua sense size()

Escriu la funció capicua sense fer servir l.size() ni comptar els elements abans. Cada element s’ha de comparar com a molt una vegada. Pista: com saps que els dos iteradors s’han trobat?

Mostra la solució

Els dos iteradors s’acosten. Paren quan es troben (si la llista té un nombre senar d’elements, al mig) o quan es creuen (si en té un nombre parell). Com que només podem comparar si són iguals, ho comprovem després de cada pas:

bool capicua2(const list<int>& l) {
    list<int>::const_iterator ini = l.begin();
    list<int>::const_iterator fi = l.end();
    while (ini != fi) {
        --fi;
        if (ini == fi) return true;      // senar: s'han trobat al mig
        if (*ini != *fi) return false;
        ++ini;
    }
    return true;                         // parell (o buida): s'han creuat
}

Seguim [4,8,8,4][4, 8, 8, 4]: fi retrocedeix al 4 final; compara 4 amb 4; ini avança al primer 8. fi retrocedeix al segon 8; compara 8 amb 8; ini avança i ara apunta al segon 8, que és on és fi: ini == fi, el bucle acaba i retorna cert.

Amb [4,8,5,8,4][4, 8, 5, 8, 4], després de dues comparacions, ini és al 5 i fi al segon 8. A la tercera volta, fi retrocedeix al 5 i ini == fi: s’han trobat al mig, retorna cert.

ExerciciInverteix una llista

Escriu una acció que inverteixi una llista sense fer servir cap estructura auxiliar (ni piles ni altres llistes). Ha de costar Θ(n)\Theta(n).

Mostra la solució

Recorrem la llista a partir del segon element i cada element el passem al principi. El primer element original va quedant enrere i acaba l’últim.

// Pre: l = [a1, ..., an]. Post: l = [an, ..., a1].
void inverteix(list<int>& l) {
    if (l.empty()) return;
    list<int>::iterator it = l.begin();
    ++it;  // el primer ja és a lloc: serà l'últim quan els altres passin davant
    while (it != l.end()) {
        l.push_front(*it);
        it = l.erase(it);
    }
}

Amb [1,2,3,4][1, 2, 3, 4]: el 2 passa davant ([2,1,3,4][2, 1, 3, 4]), després el 3 ([3,2,1,4][3, 2, 1, 4]), després el 4 ([4,3,2,1][4, 3, 2, 1]).

Cada volta fa un push_front i un erase, tots dos Θ(1)\Theta(1): en total Θ(n)\Theta(n). (La STL té l.reverse(), que fa el mateix sense crear ni destruir cap element.)

ExerciciFusió amb splice?

Un company proposa fer la fusió de dues llistes ordenades així: a.splice(a.end(), b); a.sort();. Funciona? És una bona solució?

Mostra la solució

Funciona: posa tots els elements de b al final d’a (Θ(1)\Theta(1)) i després ordena (Θ((n+m)log⁡(n+m))\Theta((n + m) \log(n + m)), el cost d’ordenar).

No és la millor: no aprofita que les dues llistes ja estaven ordenades. La fusió amb dos iteradors costa Θ(n+m)\Theta(n + m), que és menys. Amb llistes d’un milió d’elements, la diferència és d’un factor 20 aproximadament (log⁡2106≈20\log_2 10^6 \approx 20).

A més, splice deixa b buida, i potser l’especificació demanava que b no canviés.