• No se han encontrado resultados

Estructures lineals i arborescents. Assignatura PRO2

N/A
N/A
Protected

Academic year: 2021

Share "Estructures lineals i arborescents. Assignatura PRO2"

Copied!
39
0
0

Texto completo

(1)

Estructures lineals

i arborescents

Assignatura PRO2

(2)
(3)

Índex

3 Tipus genèrics o parametritzats de dades 5

4 Estructures de dades lineals 7

4.1 Especificació de la classe genèrica Pila o stack . . . 7

4.2 Especificació de la classe genèrica Cua o queue . . . 9

4.3 Ús de les classes Pila i Cua . . . 11

4.3.1 Alçària d’una pila i longitud d’una cua . . . 13

4.3.2 Suma dels elements d’una pila i d’una cua . . . 14

4.3.3 Operacions de cerca en una pila de int . . . 14

4.3.4 Operacions de cerca en una pila de Estudiant . . . 15

4.3.5 Operacions de cerca en una cua de int . . . 16

4.3.6 Comprovar si dues piles són iguals . . . 17

4.3.7 Sumar k als elements d’una pila i una cua d’enters . . . 17

4.4 Llistes i iteradors, lists i iterators . . . 20

4.5 Especificació de la classe genèrica Llista . . . 21

4.6 Operacions de recorregut de llistes . . . 25

4.7 Operacions de cerca en llistes . . . 25

4.8 Modificació i generació d’una llista . . . 26

5 Estructures de dades arborescents 29 5.1 Especificació de la classe genèrica dels arbres binaris . . . 29

5.2 Ús d’arbres binaris: operacions de cerca i recorregut . . . 31

5.2.1 Alçària d’un arbre . . . 31

5.2.2 Mida d’un arbre . . . 32

5.2.3 Cerca d’un valor int en un arbre de int . . . 33

5.2.4 Modificació i generació d’arbres . . . 33

5.2.5 Recorreguts típics d’arbres . . . 34

(4)
(5)

Capítol 3

Tipus genèrics o parametritzats de dades

El mecanisme de templates de C++ permet fer servir tipus de dades com a paràmetres. D’aquesta forma, un tipus de dades pot aparèixer com a paràmetre en la definició d’una classe o mètode. Es pot, per tant, treballar amb estructures de dades genèriques, on podem guardar qualsevol tipus d’element, donat que aquest tipus és un paràmetre. En el moment de crear l’estructura només caldrà instanciar el tipus d’element que hi guardarem.

En aquest capítol no implementarem estructures de dades genèriques, sino que farem servir estructures ja implementades. Per fer servir correctament aquestes estructures ens cal conèixer una sèrie de recomanacions i saber com fer la instanciació.

Els vectors són un exemple d’estructura de dades genèrica que ja coneixem. En el seu cas, es pot consultar qualsevol element sense necessitat de modificar-lo, però és important tenir en compte, com veurem més endavant als exemples, que per algunes d’aquestes estructures, con-sultar tots els elements implica buidar l’estructura d’elements. Per això, si volem conservar l’estructura inicial, caldrà fer una còpia. Normalment, per les estructures genèriques que farem servir als nostres programes, i també pels objectes continguts dintre de les estructures, podrem fer servir l’operador d’assignació = per fer còpies. En els casos en que no sigui així es farà constar i es proporcionarà algun mètode alternatiu per fer còpies.

Una conseqüència de treballar amb estructures genèriques és que no pot ser genèric cap mètode que depengui de la classe dels components. Cap mètode genèric pot tenir en el seu codi ni lectures, ni escriptures dels elements, perquè encara no se sap a quina classe pertanyeran aquests elements. Això obliga a crear operacions fora de la classe genèrica per fer les operacions de lectura i escriptura d’estructures que continguin elements d’una classe concreta. Per cada classe d’elements, haurem d’implementar operacions de lectura i escriptura per l’estructura de dades contenidora. Aquestes operacions no són de la classe i no tenen paràmetre implícit.

(6)
(7)

Capítol 4

Estructures de dades lineals

Una estructura de dades és lineal si els seus elements formen una seqüència. Podem representar l’estructura com

e1, e2, · · · , en

on n ≥ 0. Si n = 0, l’estructura està buida. Si n = 1, l’estructura té un element que és al mateix temps el primer i l’últim, i no té ni antecessor ni successor. Si n = 2, hi ha dos elements, el primer element, sense antecessor i que té com a successor el segon, i el segon element, sense successor i que té com a antecessor el primer. Si n > 2, tots els elements a partir del segon fins al penúltim tenen antecessor i successor.

Les diferents estructures de dades lineals es diferencien per com es pot accedir als seus ele-ments.

4.1

Especificació de la classe genèrica Pila o stack

Una pila és una estructura lineal de dades molt usada en programació. Es diu així perquè és similar a una pila normal, per exemple de plats, on quan afegim un plat a la pila el posem a dalt de tot i quan traguem un plat de la pila agafem el darrer que s’ha posat, és a dir, el que està a dalt de tot. És una estructura de tipus LIFO, que en anglès és un acrònim de Last in, first out. El nom estàndard d’aquesta estructura en C++ i en anglès és stack. Farem servir el nom pila en el text d’aquests apunts quan parlem de piles en general i stack als programes d’exemple. Els noms de variables pila seran normalment p, q, etc.

La classe stack de C++ de fet és només una restricció d’una altra classe ja existent que és més potent i versàtil. L’especificació que donem seguidament està adaptada de l’implementació estàndard de la classe stack on s’han omès detalls que en aquest punt no tenen importància. Ob-serveu l’aparició del terme template a l’especificació de la classe stack. El nom T fa referència al tipus dels elements que contindrà la pila, que pot ser qualsevol. Per fer servir la pila hem de indicar quin tipus d’elements contindrà i allà on hi hagi un paràmetre T, posar una expressió del tipus triat.

(8)

template <class T> class stack{ // Tipus de mòdul: dades

// Descripció del tipus: Estructura lineal que conté elements de tipus T i que // permet consultar i eliminar només l’últim element afegit

private:

public:

// Constructores

stack();

/* Pre: cert */

/* Post: El resultat és una pila sense cap element */

stack(const stack & original); /* Pre: cert */

/* Post: El resultat és una còpia d’original */

// Destructora: Esborra automàticament els objectes locals en sortir d’un àmbit // de visibilitat

~stack();

// Modificadores

void push(const T& x); /* Pre: cert */

/* Post: El paràmetre implícit és com el paràmetre implícit original amb x afegit com a darrer element */

void pop();

/* Pre: El paràmetre implícit no està buit */

/* Post: El paràmetre implícit és com el paràmetre implícit original però sense el darrer element afegit al paràmetre implícit original */

// Consultores

T top() const;

/* Pre: El paràmetre implícit no està buit */

/* Post: El resultat és el darrer valor afegit al paràmetre implícit */

bool empty() const; /* Pre: cert */

(9)

4.2. ESPECIFICACIÓ DE LA CLASSE GENÈRICA CUA O QUEUE 9 1 1 1 1 1 1 1 1 1 1 2 2 3 2 3 4 2 3 2 3 5 2 3 5 6 2 2 3 8 7 6 5 3 5 6 7 2 3 5 6 7 8 9

Figura 4.1: Exemple d’evolució d’una pila

int size() const; /* Pre: cert */

/* Post: El resultat és el nombre d’elements del paràmetre implícit */ };

Per instanciar piles d’un tipus concret escriurem stack<tipus> nom_pila;, tal i com es feia amb els vectors. Per exemple stack<int> p; o stack<Estudiant> q;, etc.

A la figura 4.1 veiem un exemple d’evolució d’una pila d’enters. Afegim successivament els valors 1, 2, 3, i 4; traiem l’únic valor accessible, i afegim successivament els valors 5, 6, 7, 8 i 9.

4.2

Especificació de la classe genèrica Cua o queue

Una cua és una estructura lineal de dades molt usada en programació. Es diu així perquè és similar a una cua de gent a la vida real. Quan s’afegeix un element a la cua, es posa darrera de tots els elements que hi havia, i quan es treu un element de la cua s’agafa el primer que ha arribat, és a dir, el que porta més temps a la cua. És una estructura de tipus FIFO, que en anglès és un acrònim de First in, first out.

El nom estàndard d’aquesta estructura en C++ i en anglès és queue. Farem servir el nom cua en el text d’aquests apunts quan parlem de cues en general i queue als programes d’exemple. Els noms de variables cua seran normalment c, c1, c2, etc.

La classe queue de C++ de fet és només una restricció d’una altra classe ja existent que és més potent i versàtil. L’especificació que donem seguidament està adaptada de l’implementació estàndard de la classe queue on s’han omès detalls que en aquest punt no tenen importància. El rol del terme template i el tipus T a l’especificació de la classe queue és el mateix que el que vam comentar pel cas de la classe stack.

(10)

template <class T> class queue{ // Tipus de mòdul: dades

// Descripció del tipus: Estructura lineal que conté elements de tipus T i que // permet consultar i eliminar només el primer element afegit

private:

public:

// Constructores

queue();

/* Pre: cert */

/* Post: El resultat és una cua sense cap element */

queue(const queue &original); /* Pre: cert */

/* Post: El resultat és una cua còpia d’original */

// Destructora: Esborra automàticament els objectes locals en sortir d’un àmbit // de visibilitat

~queue();

// Modificadores

void push(const T& x); /* Pre: cert */

/* Post: El paràmetre implícit és com el paràmetre implícit original amb x afegit com a darrer element */

void pop();

/* Pre: El paràmetre implícit no està buit */

/* Post: El paràmetre implícit és com el paràmetre implícit original però sense el primer element afegit al paràmetre implícit original */

// Consultores

T front() const;

/* Pre: El paràmetre implícit no està buit */

/* Post: El resultat és el valor més antic afegit al paràmetre implícit */

bool empty() const; /* Pre: cert */

(11)

4.3. ÚS DE LES CLASSES PILA I CUA 11 1 1 2 1 2 3 2 3 2 3 4 2 3 4 5 3 4 5 3 4 5 6 3 4 5 6 7

Figura 4.2: Exemple d’evolució d’una cua

int size() const; /* Pre: cert */

/* Post: El resultat és el nombre d’elements del paràmetre implícit */ };

Per instanciar cues d’un tipus concret escriurem queue<tipus> nom_cua;, per exemple es-criurem queue<int> c; o queue<Estudiant> q;, etc.

A la figura 4.2 veiem un exemple d’evolució d’una cua d’enters. Els valors 1, 2 i 3 demanen torn, després s’avança, els valors 4 i 5 demanen torn, es torna a avançar i els valors 6 i 7 demanen torn.

4.3

Ús de les classes Pila i Cua

Comencem amb exemples senzills de l’ús de les classes Pila i Cua. De moment només oferirem accions i funcions en C++ i no tot el programa en C++, és a dir, ometrem l’acció main i les inclusions.

A l’hora de treballar amb piles i cues voldríem fer una distinció intuïtiva sobre tres tipus diferents d’algorismes. Hi ha uns algorismes que anomenarem algorismes per explorar, un segon tipus que anomenarem algorismes per modificar i un tercer tipus que anomenarem algorismes

(12)

per crear o generar. En el primer tipus d’algorismes ens mourem per l’estructura fent servir les operacions consultores, però no modificarem cap element de l’estructura. En aquesta mena d’algorismes no farem servir l’operació push de piles o cues. En el segon tipus d’algorismes modificarem un nombre qualsevol d’elements de l’estructura, potser només caldrà modificar un element, o potser caldrà modificar-los tots, en qualsevol cas, haurem de fer servir l’operació push per afegir elements tant si es tracta de piles com cues. En el tercer tipus d’algorismes també hauren de fer servir les operacions esmentades, perquè al crear o generar l’estructura des del no res, haurem d’anar afegint elements.

Comprovareu que a les operacions d’exploració i de creació que dissenyem recursivament passem per referència les piles o cues implicades, encara que conceptualment siguin parámetres purs d’entrada. Ho fem així per evitar que es faci una còpia del paràmetre per cada crida recursi-va. Amb això estalviem temps i memòria. Cal dir, però, que s’ha de tenir la precaució d’invocar les funcions amb una còpia del paràmetre original que, si no, perdria elements o fins i tot acabaria buit. No esmentem l’estat final de la pila a la Post perquè suposem que no serà rellevant desprès de l’execució de l’operació.

Ens resultarà útil poder referir-nos de forma concisa a l’estat inicial d’una estructura de dades, per diferenciar-lo de l’estat en qualsevol altre moment de l’execució de l’operació. Per fer-ho, a la precondició afegirem l’expressió nom_estructura=NOM_ESTRUCTURA, per exemple p=P, c=C, etc. Llavors, la pila original serà P i la pila actual, que pot haver sofert canvis causats pel codi de l’operació de la qual era paràmetre, serà p.

Abans de començar amb els exemples volem comentar un problema que es pot produir durant l’execució dels nostres programes. Si un programa que fa servir la classe stack o queue executa operacions incorrectes, com ara consultar o treure un element d’una pila o cua buida, pot passar que el programa es continui executant, treballant de forma imprevisible. Per evitar aquesta mena de problemas cal, a l’hora de compilar, fer servir el flag D_GLIBCXX_DEBUG, és a dir:

> g++ -c elmeuprograma.cc -D_GLIBCXX_DEBUG -I$INCLUSIONS

i enllaçar normalment. Seguint aquestes instruccions, en el moment que el programa intenti accedir incorrectament a una estructura buida, el programa s’interromprà donant un missatge d’error. D’aquesta manera ens serà més fàcil localitzar on s’ha produït l’error i corregir-lo.

Aquesta mena d’errors durant l’execució es poden produir si per exemple el nostre codi és incorrecte o fins i tot si el nostre codi és correcte però les dades no compleixen la precondició. Per exemple, si tenim una operació que té com a precondició que hi ha un paràmetre d’entrada pila que no està buit, és molt probable que consultem o traguem el cim de la pila sense cap com-provació. Si per error l’operació rep una pila buida i s’intenta accedir a la pila, si hem compilat amb D_GLIBCXX_DEBUG es produirà un error, però si hem copilat sense el flag, el programa pot continuar executant-se amb dades corruptes i produir un error més endavant. Llavors serà molt més difícil localitzar on s’ha produït l’error original, i per tant serà més difícil corregir-lo.

No us recomanem compilar sempre amb el flag D_GLIBCXX_DEBUG donat que la compilació és més lenta i el codi generat més ineficient perquè durant l’execució fa més comprovacions que el codi normal.

(13)

4.3. ÚS DE LES CLASSES PILA I CUA 13

4.3.1

Alçària d’una pila i longitud d’una cua

A continuació veurem exemples d’algorismes d’exploració on visitarem tots els elements de l’estructura. Calcularem propietats de l’estructura independents dels seus elements, per això no caldrà consultar els elements.

int alcaria_pila_int(stack<int>& p) /* Pre: p=P */

/* Post: El resultat és el nombre d’elements de P */ { int ret; if(p.empty()) ret=0; else{ p.pop(); ret=alcaria_pila_int(p)+1; } return ret; }

El mètode pop és void, és a dir, és una acció i per tant no retorna cap valor. Seria incorrecte (de fet, no compila) escriure:

ret=alcaria_pila_int(p.pop())+1;

en comptes de

p.pop();

ret=alcaria_pila_int(p)+1;

donat que alcaria_pila_int espera una expressió pila d’enters i no una expressió void. En aquest segon algorisme, que és iteratiu, la cua es passa per valor. La funció rep una còpia del paràmetre original i treballa tota l’estona amb la mateixa cua.

int longitud_cua_int(queue<int> c) /* Pre: c=C */

/* Post: El resultat és el nombre d’elements de C */ { int ret=0; while(not c.empty()){ ++ret; c.pop(); } return ret; }

(14)

4.3.2

Suma dels elements d’una pila i d’una cua

Presentem un altre exemple d’algorisme d’exploració, però ara calcularem propietats de l’estruc-tura que depenen dels seus elements, per això caldrà consultar-los.

int suma_pila_int(stack<int> p) /* Pre: p=P */

/* Post: El resultat és la suma dels elements de P */ { int ret=0; while(not p.empty()){ ret +=p.top(); p.pop(); } return ret; } int suma_cua_int(queue<int>& c) /* Pre: c=C */

/* Post: El resultat és la suma dels elements de C */ { int ret; if (c.empty()) ret=0; else{ int aux=c.front(); c.pop(); ret=suma_cua_int(c)+aux; } return ret; }

En aquesta mena d’exemples resulta més o menys igual de difícil resoldre els problemes de forma iterativa o recursiva. Més endavant es veurà que per altres exemples és més pràctic fer servir la recursivitat amb les piles i la iteració amb les cues.

4.3.3

Operacions de cerca en una pila de int

Tots els algorismes de cerca que oferirem són exemples d’algorismes d’exploració on no neces-sàriament visitarem tots els elements de l’estructura. Aquesta és la diferència bàsica entre una cercai un recorregut. En el cas d’un recorregut hem de mirar tots els elements de l’estructura, o si no s’han de mirar tots, com a mínim se sap abans de començar el recorregut quins elements s’han de mirar. En el cas de la cerca no se sap a priori. Si l’element que busquem hi és a l’es-tructura, només s’han de visitar els elements anteriors i aquest; si no hi és, haurem de mirar tots els elements per dir que no l’hem trobat. Fer un recorregut quan en realitat es tracta d’una cerca es considera un error greu.

(15)

4.3. ÚS DE LES CLASSES PILA I CUA 15

Oferim dues versions d’una cerca en una pila d’enters, la recursiva i la iterativa. Es pot observar que totes dues són més o menys iguals de difícils d’implementar.

bool cerca_rec_pila_int(stack<int>& p, int x) /* Pre: p=P */

/* Post: El resultat ens diu si x és un element de P o no */ {

bool ret;

if(p.empty()) ret=false;

else if (p.top() == x ) ret=true; else { p.pop(); ret=cerca_ret_pila_int(p,x); } return ret; }

bool cerca_iter_pila_int(stack<int> p, int x) /* Pre: p=P */

/* Post: El resultat ens diu si x és un element de P o no */ {

bool ret=false;

while(not p.empty() and not ret){ if (p.top() == x) ret = true; else p.pop();

}

return ret; }

4.3.4

Operacions de cerca en una pila de Estudiant

Ja s’ha vist com s’ha d’instanciar una pila genèrica per tal que sigui una pila d’enters. Oferim el mateix exemple per piles instanciades amb la classe Estudiant.

bool cerca_rec_pila_Estudiant(stack<Estudiant>& p, int x) /* Pre: p=P */

/* Post: El resultat ens diu si hi ha algun estudiant amb dni x a P */ {

bool ret;

if(p.empty()) ret=false;

else if (p.top().consultar_DNI() == x ) ret=true; else {

p.pop();

ret=cerca_rec_pila_Estudiant(p,x); }

(16)

return ret; }

bool cerca_iter_pila_Estudiant(stack<Estudiant> p, int x) /* Pre: p=P */

/* Post: El resultat ens diu si hi ha algun estudiant amb dni x a P */ {

bool ret=false;

while(not p.empty() and not ret){

if (p.top().consultar_DNI() == x) ret=true; else p.pop();

}

return ret; }

Cal observar que, com que els elements són objectes d’una determinada classe, s’han de fer servir les operacions de la mateixa classe per poder-los consultar i comparar.

4.3.5

Operacions de cerca en una cua de int

Oferim ara les dues versions, iterativa i recursiva de la cerca d’un enter en una cua d’enters. Es pot observar que totes dues versions són més o menys iguals de difícils d’implementar.

bool cerca_rec_cua_int(queue<int>& c, int x) /* Pre: c=C */

/* Post: El resultat ens diu si x és un element de C o no */ {

bool ret;

if(c.empty()) ret=false;

else if (c.front() == x ) ret=true; else { c.pop(); ret=cercar_rec_cua_int(c,x); } return ret; }

bool cerca_iter_cua_int(queue<int> c, int x) /* Pre: c=C */

/* Post: El resultat ens diu si x és un element de C o no */ {

bool ret=false;

while(not c.empty() and not ret){ if (c.front() == x) ret = true; else c.pop();

(17)

4.3. ÚS DE LES CLASSES PILA I CUA 17

return ret; }

Resultarà molt fàcil a partir dels exemples anteriors escriure les dues versions de la cerca per cues genèriques instanciades amb la classe Estudiant.

4.3.6

Comprovar si dues piles són iguals

Si ens parem a pensar, aquest és una altre exemple de cerca, en aquest cas amb dues piles, perquè en el moment que trobem elements diferents en la mateixa posició de les dues piles, ja podem determinar que les piles no són iguals, per tant no necessàriament haurem de visitar tots els elements de les dues piles.

bool piles_iguals(stack<int>& p1, stack<int>& p2 ) /* Pre: p1=P1, p2=P2 */

/* Post: El resultat ens indica si P1 i P2 són iguals */ {

bool ret;

if(p1.empty() and p2.empty()) ret=true;

else if (p1.empty() and not p2.empty()) ret=false; else if (not p1.empty() and p2.empty()) ret=false; else if (p1.top()!=p2.top()) ret=false;

else { p1.pop(); p2.pop(); ret=piles_iguals(p1,p2); } return ret; }

En aquest exemple potser seria més adient donar una postcondició més informativa, com ara, tots els elements de P1 són iguals a l’element de P2 que ocupa la seva mateixa posició, i viceversa.

4.3.7

Sumar k als elements d’una pila i una cua d’enters

L’algorisme de sumar un valor a tots els elements d’una pila és un exemple d’algorisme de modificació, s’haurà de fer servir la operació push. Si es fa com a funció serà un algorisme de generació o creació donat que s’han d’anar afegint elements a la pila resultat. Comentarem quines són els avantatges i els inconvenients de les versions acció i funció. Primer presentem la versió funció.

stack<int> sumar_k_pila_func(stack<int>& p, int k) /* Pre: p=P */

(18)

mateixa posició, més el valor k */ {

stack<int> res; if(not p.empty()){

int aux = p.top()+k; p.pop(); res=sumar_k_pila_func(p,k); res.push(aux); } return res; }

No ens cal un cas base explícit donat que quan p és buida, el resultat ha de ser una pila buida, que ja aconseguim al crear la pila res. Com es pot comprovar hem de fer servir l’operació push. Intentar resoldre iterativament aquest problema és menys interessant. Si us pareu a pensar, veureu que després d’aplicar-li un algorisme iteratiu, la pila resultat quedarà cap per vall, per tant cal tornar a recórrer la pila iterativament per tal que els elements quedin en l’ordre correcte.

Comparem ara amb la versió acció.

void sumar_k_pila_acc(stack<int>& p, int k) /* Pre: p=P */

/* Post: Cada element de p és la suma de l’element de P que ocupa la mateixa posició, més el valor k */

{

if(not p.empty()){ int aux = p.top()+k; p.pop();

sumar_k_pila_acc(p,k); p.push(aux);

} }

Tampoc no ens cal un cas base explícit donat que quan p està buida el resultat és la pròpia p. Ens podem demanar quina de les dues versions és més pràctica. Quina de les dues us sembla més eficient? O són totes dues iguals de eficients? Penseu una estona abans de continuar llegint. La versió acció és més eficient perque a la funció la línia res=sumar_k_pila_func(p,k); fa una còpia del resultat de la crida recursiva a la variable res i això es fa per a cada crida. En canvi a la versió acció ens estalviem fer aquestes còpies i la destrucció dels objectes locals dels resultats.

Ara resoldrem el mateix problema per cues tant en versió acció com funció.

void sumar_k_cua_acc(queue<int>& c, int k) /* Pre: c=C */

/* Post: Cada element de c és la suma de l’element de C a la mateixa posició i el valor k */

(19)

4.3. ÚS DE LES CLASSES PILA I CUA 19

{

queue<int> c_aux; while (not c.empty()){

int aux= c.front(); aux+=k; c.pop(); c_aux.push(aux); } c=c_aux; }

queue<int> sumar_k_cua_func(queue<int>& c, int k) /* Pre: c=C */

/* Post: Cada element del resultat és la suma de l’element de C a la mateixa posició i el valor k */

{

queue<int> res;

while (not c.empty()){ int aux= c.front(); aux+=k; c.pop(); res.push(aux); } return res; }

Quina de les dues versions és més eficient? En aquest cas les dues són igual d’eficients perquè a la versió acció hem de fer una còpia amb l’instrucció c=c_aux; per tal que el resultat quedi a la cua c, mentre que a la versió funció aquesta còpia es fa quan cridem la funció i assignem el resultat a una altra cua.

Comprovem, llavors, que si comparem codis iteratius d’acció i funció obtenim solucions molt similars quan a eficiència, però la diferència és notable amb codis recursius.

Per últim, podem obtenir una versió millor que les anteriors si fem servir el mètode size() de cua per estalviar-nos la còpia final de la cua quan implementem una acció.

void sumar_k_cua_acc(queue<int>& c, int k) /* Pre: c=C */

/* Post: Cada element de c és la suma de l’element de C a la mateixa posició i el valor k */

{

int i= c.size();

for(int j=1; j<=i; ++j){ int aux= c.front(); aux+=k;

(20)

c.push(aux); }

}

Fixeu-vos que tota l’estona fem servir la mateixa cua. Treiem el primer element i l’afegim modificat al final de la cua. La condició de fi no pot ser en aquest cas que la cua estigui buida donat que després de cada iteració sempre té el mateix nombre d’elements, uns modificats i altres no.

Resoldre aquest problema amb cues recursivament resulta menys interessant que fer-ho itera-tivament. Si us pareu a pensar, després d’aplicar-li un algorisme recursiu, la cua resultat quedarà a l’inrevés, per tant cal tornar a recórrer la cua recursivament per tal que els elements quedin en l’ordre correcte.

4.4

Llistes i iteradors, lists i iterators

En C++ direm contenidor a una estructura de dades on emmagatzemarem objectes d’una forma determinada. En general, els conenidors son classes genèriques que s’instancien de la manera que ja hem vist. Un exemple de contenidor que ara presentarem és la llista.

Una llista és una estructura de dades lineal que permet accedir a qualsevol element sense treure els anteriors. Això es fa mitjançant iteradors.

Un iterador ens permet desplaçar-nos per un contenidor i fer referència als seus elements. Depenent de les característiques del contenidor es podran fer servir diferents tipus d’iteradors. Pel cas de llistes d’enters, declarem una llista i un iterador així

list<int> l;

list<int>::iterator it;

Noteu que l’iterador no s’associa a la llista l sinó a la classe. Això comporta que es podria reutilitzar el mateix iterador amb diferent llistes.

Les llistes ofereixen un mètode begin() que retorna un iterador que referencia el primer ele-ment del contenidor, si és que existeix. Per exemple, podem fer que l’iterador anterior referenciï el primer element de la llista de la següent forma:

it = l.begin();

També hi ha iteradors constants que impedeixen modificar l’objecte referenciat per l’iterador. És obligatori fer servir iteradors constants si són per explorar una llista que sigui un paràmetre const. Si, per exemple, necessitem una llista d’Estudiant amb un iterador d’aquesta mena, cal fer:

list<Estudiant> l_est;

(21)

4.5. ESPECIFICACIÓ DE LA CLASSE GENÈRICA LLISTA 21

l.begin() Retorna un iterador que referencia el primer element d’l

l.end() Retorna un iterador que referencia un element fictici posterior al darrer d’l ++it Avança al següent element

--it Retrocedeix a l’anterior element *it Designa l’element referenciat per it it1=it2 Assigna l’iterador it2 a it1

it1==it2 Diu si els iteradors it1 i it2 són iguals o no it1!=it2 Diu si els iteradors it1 i it2 són diferents o no

Figura 4.3: Resum de les operacions amb iteradors

Amb l’iterador it2 podem moure’ns per la llista l_est i consultar el valor dels seus ele-ments, però no modificar-los.

Podem fer que un iterador es mogui per una llista amb la notació ++it i --it, que ens permet anar al següent element i al anterior respectivament. També existeixen aquests operadors en forma postfixa.

Podem desreferenciar un iterador, obtenint l’objecte que referencia, amb la notació *. Per exemple, si l’iterador it2 definit anteriorment ja s’ha mogut per l_est, l’element referenciat per it2 és *it2, i per consultar el seu dni, podrem fer (*it2).consultar_DNI(). No podrem, però, modificar *it2, ja que it2 ha estat definit com a constant.

Les llistes també ofereixen un mètode end que retorna un iterador que referencia a un ele-ment inexistent posterior al darrer eleele-ment de la llista. No és recomanable desreferenciar aquest iterador.

Es poden assignar iteradors amb it1=it2; i comparar-los per veure si són iguals o no (és a dir, si referencien al mateix element), amb it1==it2 i it1!=it2 respectivament. Un cas parti-cularment útil per recórrer llistes és comparar l’iterador amb el que ens movem amb l’iterador retornat pel mètode end que ens indica el final de l’estructura que estem tractant, per exemple fent (it != l.end()) on l és una llista amb un iterador it. A la figura 4.3 hi ha un resum de les operacions amb iteradors.

4.5

Especificació de la classe genèrica Llista

Com en el cas de l’estructures anteriors, només donem una part de l’especificació de la classe list. Hi ha altres operacions que no mencionem i algunes de les mencionades tenen altres variants que no farem servir i que per tant no apareixen en aquests apunts. Per exemple, només hi ha una versió de les operacions insert o splice. Aquesta darrera la farem servir bàsicament per concatenar llistes.

template <class T> class list {

(22)

// Descripció del tipus: Estructura lineal que conté elements de tipus T, que es // pot començar a consultar pels extrems, on des de cada element es pot accedir // a l’element anterior i posterior (si existeixen), i que admet afegir-hi // i esborrar-hi elements a qualsevol punt

private:

public:

// Constructores

list();

/* Pre: cert */

/* Post: El resultat és una llista sense cap element */

list(const list & original); /* Pre: cert */

/* Post: El resultat és una llista còpia d’original */

// Destructora: Esborra automàticament els objectes locals en sortir d’un àmbit // de visibilitat

~list();

// Modificadores

void clear(); /* Pre: cert */

/* Post: El paràmetre implícit és una llista buida */

void insert(iterator it, const T& x);

/* Pre: it referencia algun element existent al paràmetre implícit o és igual a l’end() d’aquest */

/* Post: El paràmetre implícit és com el paràmetre implícit original amb x davant de l’element referenciat per it al paràmetre implícit original */

iterator erase(iterator it);

/* Pre: it referencia algun element existent al paràmetre implícit, que no és buit */

/* Post: El paràmetre implícit és com el paràmetre implícit original sense l’element referenciat per l’it original; el resultat referencia l’element següent al que referenciava it al paràmetre implícit original */

void splice(iterator it, list& l);

(23)

4.5. ESPECIFICACIÓ DE LA CLASSE GENÈRICA LLISTA 23

és igual a l’end() d’aquest; el p.i. i l no són el mateix objecte */ /* Post: El paràmetre implícit té els seus elements originals i els

d’l inserits davant de l’element referenciat per it; l està buida */

// Consultores

bool empty() const; /* Pre: cert */

/* Post: El resultat indica si el paràmetre implícit té elements o no */

int size() const; /* Pre: cert */

/* Post: El resultat és el nombre d’elements del paràmetre implícit */ };

En aquesta llista s’haurien d’afegir les operacions de iteradors. Noteu que no hi ha una operació de consulta d’elements perquè farem servir els iteradors per desreferenciar elements. També cal tenir en compte que si s’elimina el darrer element d’una llista, l’iterador referenciarà l’end(). A la figura 4.4 veiem un exemple de com evoluciona una llista després d’aplicar dife-rents operacions. Hem afegit l’operació splice que no farem servir gaire sovint, però que ens pot ser útil per concatenar dues llistes, per exemple l1 i l2. Per aconseguir-ho haurem de es-criure l1.splice(l1.end(),l2);. Si consultem l’especificació, veurem que així l2 s’insereix a partir del final de l1.

Com passava amb les classes stack i queue, si un programa que fa servir la classe list executa operacions incorrectes, pot passar que el programa es continui executant, treballant de forma imprevisible. Per evitar aquesta mena de problemes cal, a l’hora de compilar, fer servir el flag D_GLIBCXX_DEBUG, és a dir:

> g++ -c elmeuprograma.cc -D_GLIBCXX_DEBUG -I$INCLUSIONS

i enllaçar normalment. Seguint aquestes instruccions, en el moment que el programa intenti acce-dir incorrectament a una llista, el programa s’interromprà donant un missatge d’error. D’aquesta manera ens serà més fàcil localitzar on s’ha produït l’error i corregir-lo. Entre els errors durant l’execució que ara fan interrompre el programa amb un missatge d’error explicatiu hi ha:

• Fer retrocedir un iterador a l’element begin() d’una llista, és a dir, fer --it quan per alguna llista L es compleix it==L.begin().

• Fer avançar un iterador a l’element end() d’una llista, és a dir, fer ++it quan per alguna llista L es compleix it==L.end().

• Desreferenciar un iterador a l’element end() d’una llista • Desreferenciar un iterador no assignat

(24)

1

5

2

3

4

1

5

2

3

1

2

3

4

1

2

3

4

1

5

4

1

5

4

begin

end

++it;

4

it=l.erase(it);

−−it;

it=l.begin();

*it=6;

1

4

2

2

6

5

l.insert(it,5);

++it;

(25)

4.6. OPERACIONS DE RECORREGUT DE LLISTES 25

4.6

Operacions de recorregut de llistes

Oferim un exemple d’algorisme d’exploració: sumar tots els elements d’una llista d’enters. Pas-sem la llista per referència constant perquè així estalviem que es faci una còpia del paràmetre per l’operació. Contràriament al que passa amb piles i cues, es pot explorar una llista sense modificar-la. Si la llista es passa per referència constant tots els seus iteradors han de ser també iteradors constants.

int suma_llista_int(const list<int>& l) /* Pre: cert */

/* Post: El resultat és la suma dels elements de l */ {

list<int>::const_iterator it; int s=0;

for (it=l.begin(); it != l.end(); ++it){ s+=*it;

}

return s; }

Noteu que si una llista l és buida, llavors l.begin() i l.end() són iguals.

4.7

Operacions de cerca en llistes

Oferim una versió d’una cerca senzilla en una llista d’enters.

bool pert_llista_int(const list<int>& l, int x) /* Pre: cert */

/* Post: El resultat indica si x hi és o no a l */ {

bool b = false;

list<int>::const_iterator it= l.begin(); while ( it != l.end() and not b){

if (*it == x) b= true; else ++it;

}

return b; }

Repetim l’exemple per llistes d’Estudiant. Així veiem com treballar quan la llista conté objectes en comptes de tipus simples.

bool pert_llista_Estudiant(const list<Estudiant>& l, int x) /* Pre: cert */

(26)

{

bool b =false;

list<Estudiant>::const_iterator it = l.begin(); while (it != l.end() and not b){

if ((*it).consultar_DNI() == x) b= true; else ++it;

}

return b; }

4.8

Modificació i generació d’una llista

Primer modifiquem una llista sumant un valor k a tots els elements.

void suma_llista_k(list<int>& l, int k) /* Pre: l=L */

/* Post: El valor de cada element d’l és el valor corresponent a L més k */ {

list<int>::iterator it= l.begin(); while (it != l.end()){

*it+=k; ++it; } }

També seria vàlida aquesta solució, encara que no és preferible ja que “mou” més informació.

void suma_llista_k(list<int>& l, int k) /* Pre: l=L */

/* Post: El valor de cada element d’l és el valor corresponent a L més k */ {

list<int>::iterator it= l.begin(); while (it != l.end()){

int aux = (*it) + k; it=l.erase(it); l.insert(it,aux); }

}

Si volem conservar la llista original i que la llista modificada sigui nova, podem obtenir una solució similar en forma de funció, on haurem de fer servir l’operació insert per generar la llista resultat.

list<int> suma_llista_k(const list<int>& l, int k) /* Pre: cert */

(27)

4.8. MODIFICACIÓ I GENERACIÓ D’UNA LLISTA 27

/* Post: Cada element del resultat és la suma de k i l’element d’l a la seva mateixa posició */

{

list<int>::const_iterator it=l.begin(); list<int> l2;

list<int>::iterator it2=l2.begin();

while (it != l.end()) { l2.insert(it2,*it+k); ++it;

}

return l2; }

Noteu que per crear o modificar una llista no podem fer servir iteradors constants. També és destacable el fet que l’iterador it2 sempre val l2.end(), ja que cada nou element a l2 s’afegeix davant seu.

Per últim, si obtenim la nova llista amb una acció, ens estalviarem l’assignació al resultat quan fem la crida corresponent

void suma_llista_k(const list<int>& l, list<int> &l2, int k) /* Pre: l2 és buida */

/* Post: Cada element de l2 és la suma de k i l’element d’l a la seva mateixa posició */

{

list<int>::const_iterator it=l.begin(); list<int>::iterator it2=l2.begin();

while (it != l.end()) { l2.insert(it2,*it+k); ++it;

} }

En resum, si no volem conservar la llista original, la versió preferible és la primera, que és una acció. La segona també és una acció, però és més ineficient perquè a cada iteració esborra i inserta elements. En cas contrari, la millor solució és la quarta.

(28)
(29)

Capítol 5

Estructures de dades arborescents

Un arbre és una estructura de dades no lineal, donat que els seus elements poden tenir més d’un element successor. Cada element d’un arbre es diu node. Un arbre no buit, és a dir, que tingui almenys un element, té un node distingit anomenat arrel, que és l’unic consultable individual-ment. Aquest node arrel està connectat amb zero o més arbres, que es denominen fills. Cada un dels fills és consultable individualment. Una fulla és un node sense successors. Un camí és una successió de nodes que van de l’arrel a una fulla. L’alçària d’un arbre és la longitud del camí més llarg. Podem veure representats gràficament els conceptes anteriors a la figura 5.1. Consulteu-la abans de continuar la lectura.

Hi ha diferents tipus d’arbres. En primer lloc mencionarem l’arbre general. Es diu així perquè cada subarbre no buit pot tenir un nombre indeterminat, fins i tot zero, de fills no buits, i cap fill buit. Si un subarbre no buit no té cap fill, aquest subarbre consta d’un únic node que és una fulla de l’arbre que conté el subarbre. Un arbre general pot ser un arbre buit o un arbre no buit que no conté arbres buits com a subarbres.

Un altre tipus d’arbre és l’arbre n-ari on tots els subarbres no buits tenen exactament el mateix nombre de fills, siguin aquests fills arbres buits o no. El valor n seria un paràmetre de l’operació constructora de la classe i podríem declarar arbres n-aris de diferents aritats.

En aquest capítol tractarem sobre un tipus concret d’arbre, l’arbre binari. Sobre els arbres generals i n-aris es tractarà en capítols posteriors.

5.1

Especificació de la classe genèrica dels arbres binaris

Els arbres binaris són un cas particular dels arbre n-aris, on n = 2. A partir d’ara en aquest capítol quan parlem d’arbres, ens referirem a arbres binaris.

template <class T> class Arbre{

// Tipus de mòdul: dades

// Descripció del tipus: Arbre genèric que o bé és buit o bé tot subarbre seu té // exactament 2 fills

(30)

1 2 3 4 5 6 7 8 9 10 subarbre dret segon subarbre segon fill subarbre esquerre fill esquerre primer subarbre primer fill fill dret fulla fulla fulla fulla cami fulla arrel

Figura 5.1: Termes comuns relatius a arbres

private:

public:

// Constructores

Arbre();

/* Pre: cert */

/* Post: El resultat és un arbre sense cap element */

Arbre(const Arbre& original); /* Pre: cert */

/* Post: El resultat és una còpia d’original */

// Destructora: Esborra automàticament els objectes locals en sortir d’un àmbit // de visibilitat

~Arbre();

(31)

5.2. ÚS D’ARBRES BINARIS: OPERACIONS DE CERCA I RECORREGUT 31

void a_buit(); /* Pre: cert */

/* Post: El paràmetre implícit no té cap element */

void plantar(const T& x, Arbre& a1, Arbre& a2);

/* Pre: a1 = A1, a2 = A2 i el p.i. és buit però no és el mateix objecte que a1 ni a2 */

/* Post: El paràmetre implícit té x com a arrel, A1 com a fill esquerre i A2 com a fill dret, a1 i a2 són buits */

void fills(Arbre &fe, Arbre &fd);

/* Pre: El paràmetre implícit no és buit i li diem A, fe i fd són buits i no són el matex objecte */

/* Post: fe és el fill esquerre d’A, fd és el fill dret d’A, el p.i. és buit */

// Consultores

T arrel() const;

/* Pre: El paràmetre implícit no és buit */

/* Post: El resultat és l’arrel del paràmetre implícit */

bool es_buit() const; /* Pre: cert */

/* Post: El resultat indica si el paràmetre implícit és buit o no */ };

Per instanciar arbres d’un tipus concret escriurem Arbre<tipus> nom_arbre;, per exemple Arbre<int> a;o Arbre<Estudiant> b;, etc.

A la figura 5.2 veiem que l’arrel de l’arbre és 1, i té dos fills. El subarbre esquerre té com a arrel 2 i el subarbre dret té com a arrel 3. Tots els nodes tenen dos fills. Si els dos fills són arbres buits, els nodes es diuen fulles. Els nodes que contenen 4, 8, 9, 10 i 7 són fulles. Els arbres buits no estan representats.

5.2

Ús d’arbres binaris: operacions de cerca i recorregut

De forma similar als casos anteriors, també podem fer una distinció entre algorismes d’explo-ració, algorismes de modificació i algorismes de creació o generació. En els dos darrers casos haurem de fer servir l’operació plantar, en el primer cas no. Comencem amb els exemples.

5.2.1

Alçària d’un arbre

Un senzill exemple d’exploració d’un arbre. Només es calcula una propietat de la estructura de l’arbre, no es consulta en cap moment el contingut dels nodes, és a dir, no es fa servir l’operació

(32)

1

2 3

4 5 6 7

8 9 10

Figura 5.2: Exemple d’un arbre binari

arrel.

int alcaria(Arbre<int>& a) /* Pre: a=A */

/* Post: El resultat és la longitud del camí més llarg de l’arrel a una fulla de l’arbre A, a és buit */ { int x; if (a.es_buit()) x=0; else{ Arbre<int> a1; Arbre<int> a2; a.fills(a1,a2); int y=alcaria(a1); int z=alcaria(a2); if (y>=z) x=y+1; else x=z+1; } return x; }

5.2.2

Mida d’un arbre

Un altre exemple molt similar.

int mida(Arbre<int>& a) /* Pre: a=A */

(33)

5.2. ÚS D’ARBRES BINARIS: OPERACIONS DE CERCA I RECORREGUT 33 { int x; if (a.es_buit()) x=0; else{ Arbre<int> a1; Arbre<int> a2; a.fills(a1,a2); int y=mida(a1); int z=mida(a2); x=y+z+1; } return x; }

5.2.3

Cerca d’un valor int en un arbre de int

Un exemple una mica diferent, perquè tot i que també és tracta d’explorar l’arbre, no necessària-ment visitarem tots els nodes de l’arbre. A més, també cal consultar el contingut dels nodes que visitem.

bool cerca(Arbre<int>& a, int x) /* Pre: a=A */

/* Post: El resultat indica si x és a l’arbre A o no, a és buit */ {

bool b;

if (a.es_buit()) b=false; else if (a.arrel()==x) b=true; else{ Arbre<int> a1; Arbre<int> a2; a.fills(a1,a2); b=cerca(a1,x); if (not b) b=cerca(a2,x); } return b; }

5.2.4

Modificació i generació d’arbres

Farem dues versions de la suma d’un valor k a tots els nodes d’un arbre d’int. Primer oferim la versió funció. L’arbre resultat s’ha d’anar generant. Els arbres acabats de crear són buits, per això no ens cal un cas base explícit.

Arbre<int> suma(Arbre<int> &a, int k) /* Pre: a=A */

(34)

/* Post: El valor de cada node del resultat és la suma del valor del node corresponent d’A i el valor k, a és buit */

{ Arbre<int> b; if (not a.es_buit()){ int n=a.arrel()+k; Arbre<int> a1; Arbre<int> a2; a.fills(a1,a2); a1=suma(a1,k); a2=suma(a2,k); b.plantar(n,a1,a2); } return b; }

A la versió acció, en el cas base, quan l’arbre és buit, l’arbre modificat és ell mateix i no cal fer res, per aquest motiu només hi ha el cas recursiu.

void suma(Arbre<int> &a, int k) /* Pre: a=A */

/* Post: El valor de cada node d’a és la suma del valor del node corresponent d’A i el valor k, a és buit */ { if (not a.es_buit()){ int n=a.arrel()+k; Arbre<int> a1; Arbre<int> a2; a.fills(a1,a2); suma(a1,k); suma(a2,k); a.plantar(n,a1,a2); } }

Com als exemples recursius amb piles, l’acció és més eficient que la funció perquè ens estal-viem fer les còpies dels resultats de les crides recursives degudes a les assignacions a1=suma(a1,k); i a2=suma(a2,k);.

5.2.5

Recorreguts típics d’arbres

En aquesta secció explicarem els mètodes més habituals per visitar els nodes d’un arbre, amb intenció de fer recorreguts o cerques. Quan es visita un node es fa alguna cosa amb ell, com modificar-lo, calcular la seva aportació a una funció de l’arbre, etc. El que diferencia els diversos mètodes és l’ordre de visita als nodes.

(35)

5.2. ÚS D’ARBRES BINARIS: OPERACIONS DE CERCA I RECORREGUT 35

Hi ha dos tipus bàsics de recorreguts, els recorreguts en profunditat i els recorreguts en amplada.

Recorreguts en profunditat

Com que un arbre buit no té nodes, qualsevol recorregut no hi farà res. Per una altra part, els recorreguts en profunditat d’un arbre no buit es defineixen com:

• preordre:

1. visitar l’arrel

2. recórrer l’arbre esquerre (en preordre) 3. recórrer l’arbre dret (en preordre)

El recorregut en preordre de l’arbre de la figura 5.2 és 1, 2, 4, 5, 8, 3, 6, 9, 10 i 7.

• inordre:

1. recórrer l’arbre esquerre (en inordre) 2. visitar l’arrel

3. recórrer l’arbre dret (en inordre)

El recorregut en inordre de l’arbre de la figura 5.2 és 4, 2, 8, 5, 1, 9, 6, 10, 3, i 7.

• postordre:

1. recórrer l’arbre esquerre (en postordre) 2. recórrer l’arbre dret (en postordre) 3. visitar l’arrel

El recorregut en postordre de l’arbre de la figura 5.2 és 4, 8, 5, 2, 9, 10, 6, 7, 3, i 1.

Noteu que els algoritmes presentats fins ara en aquest capítol es poden classificar en algun d’aquests tipus, encara que en alguns d’ells l’ordre de les visites no és rellevant.

Com a exemple més general, mostrem funcions que transformen arbres d’enters a llistes d’enters corresponents als recorreguts en preordre, inordre i postordre. És evident que recórrer un arbre buit produeix el mateix resultat en tots els casos, una llista buida.

En el cas del preordre, cal afegir l’arrel davant de la llista resultat de recórrer l’arbre esquerre. Per últim fem servir el mètode splice de la classe list per concatenar-hi la llista resultat de recórrer l’arbre dret.

(36)

list<int> preordre(Arbre<int> &a) /* Pre: a=A */

/* Post: El resultat conté els nodes d’A en preordre, a és buit */ { list<int> l; if (not a.es_buit()) { int node=a.arrel(); Arbre<int> a1; Arbre<int> a2; a.fills(a1,a2); l=preordre(a1); list<int> m; m=preordre(a2); l.insert(l.begin(),node); l.splice(l.end(),m); } return l; }

Observem que les instruccions l=preordre(a1); i m=preordre(a2); fan que s’hagin de copiar llistes, cosa que fa que aquesta funció sigui menys eficient que una acció equivalent. No ens hem de preocupar de l’eficiència de splice perquè funciona en temps constant quan hi ha dues llistes, l i m en aquest cas, i un iterador, l.end().

En comptes de donar també la versió acció del recorregut en preordre, donarem directament la versió acció del recorregut en inordre.

En el cas de l’inordre afegim l’arrel al final de la llista resultat de recórrer l’arbre esquerre i desprès afegim la llista resultat de recórrer l’arbre dret.

void inordre(Arbre<int> &a, list<int> & l) /* Pre: a=A, l=L */

/* Post: l conté L seguit dels nodes d’A en inordre, a és buit */ { if (not a.es_buit()){ int node=a.arrel(); Arbre<int> a1; Arbre<int> a2; a.fills(a1,a2); inordre(a1,l); l.insert(l.end(),node); inordre(a2,l); } }

Com podeu observar al codi anterior, no cal ni fer còpies de llistes a cada crida recursiva, que a més són costoses, ni tampoc cal fer servir l’splice. A més, noteu que la primera crida a l’operació ha de ser amb una llista buida.

(37)

5.2. ÚS D’ARBRES BINARIS: OPERACIONS DE CERCA I RECORREGUT 37

Al postordre primer concatenem les llistes resultat de recórrer respectivament el subarbre esquerre i el subarbre dret i posteriorment afegim l’arrel de l’arbre al final de la llista resultant. Donem només la versió acció. Els comentaris finals de l’anterior exemple també s’apliquen aquí.

void postordre(Arbre<int> &a, list<int> & l) /* Pre: a=A, l=L */

/* Post: l conté L seguit dels nodes d’A en postordre, a és buit */ { if (not a.es_buit()) { int node=a.arrel(); Arbre<int> a1; Arbre<int> a2; a.fills(a1,a2); postordre(a1,l); postordre(a2,l); l.insert(l.end(),node); } } Recorreguts en amplada

Un altre tipus de recorregut típic és el recorregut per nivells o recorregut en amplada. Donat un arbre, diem que el nivell d’un node és la distància a l’arrel. Mostrem una versió d’aquest recorregut on escrivim els nodes en una llista: primer s’escriu l’arrel de l’arbre (és a dir, el node de nivell 0), després tots els nodes que estan al nivell 1, d’esquerra a dreta, després tots els nodes que estan a nivell 2, també d’esquerra a dreta, etc. Aquest recorregut per nivells aplicat a l’arbre de la figura 5.2 és 1, 2, 3, 4, 5, 6, 7, 8, 9, i 10.

void nivells(Arbre<int> &a, list<int> & l) /* Pre: a=A, l és buida */

/* Post: l conté els nodes d’A en ordre creixent respecte al nivell on es troben, i els de cada nivell en ordre d’esquerra a dreta, a és buit */

{ if(not a.es_buit()){ queue <Arbre<int> > c; int node; c.push(a); while(not c.empty()){ a=c.front(); node=a.arrel(); l.insert(l.end(),node); Arbre<int> a1; Arbre<int> a2; a.fills(a1,a2);

(38)

if (not a2.es_buit()) c.push(a2); c.pop();

} } }

Aquest algorisme és molt diferent dels anteriors. Per començar és iteratiu en comptes de re-cursiu, i requereix una estructura de dades auxiliar de tipus queue per guardar, en ordre correcte, els subarbres no buits amb elements que encara no s’han afegit a la llista resultat.

Per declarar la cua s’han de separar amb un espai els dos caràcters > darrera d’int, com al cas de les matrius. Fixeu-vos que després d’inserir l’element node amb l’iterador it que val l.end()no cal tocar l’iterador, que segueix senyalant l.end().

Observeu que aquest algorisme no és gaire eficient perquè cada vegada que cridem l’operació front de la cua es fa una còpia de l’arbre obtingut. De la mateixa manera, cada vegada que afegim un arbre a la cua amb l’operació push també se’n fa una còpia. En un capítol posterior mesurarem aquestes ineficiències i més endavant revisarem l’algorisme amb l’intenció d’evitar-les.

Oferim també la versió recursiva del recorregut per nivells. Cal una operació auxiliar per introduir el paràmetre cua necessari per emmagatzemar tots els arbres pendents de tractar. S’hi apliquen les mateixes consideracions d’eficiència de la versió iterativa.

void nivells(Arbre<int> &a, list<int> & l) /* Pre: a=A, l és buida */

/* Post: l conté els nodes d’A en ordre creixent respecte al nivell on es troben, i els de cada nivell en ordre d’esquerra a dreta */

{ if(not a.es_buit()){ queue <Arbre<int> > c; c.push(a); nivells_rec_aux(c,l); } }

A continuació, la funció auxiliar recursiva. Una postcondició precisa, encara que sigui infor-mal, resulta una mica complicada d’escriure.

void nivells_rec_aux(queue<Arbre<int> > &c, list<int> & l) /* Pre: c=C, C no conté cap arbre buit , l=L */

/* Post: l és L seguit dels nodes dels arbres de C en ordre creixent

respecte al nivell al qual es troben; els nodes del mateix nivell, si són d’arbres diferents, apareixen en l’ordre en què hi són a C, si són d’un mateix arbre apareixen en ordre d’esquerra a dreta */

{

if(not c.empty()){ Arbre<int> a;

(39)

5.2. ÚS D’ARBRES BINARIS: OPERACIONS DE CERCA I RECORREGUT 39 a=c.front(); c.pop(); int node=a.arrel(); Arbre<int> a1; Arbre<int> a2; a.fills(a1,a2);

if(not a1.es_buit()) c.push(a1); if(not a2.es_buit()) c.push(a2); l.insert(l.end(),node);

nivells_rec_aux(c,l); }

}

Fixem-nos que el cas base correspon a la cua c buida. Com no queda cap arbre per tractar, no cal modificar la llista l. Si la cua no és buida, resolem la situació recursivament, després d’afegir l’arrel de l’arbre a la llista l i encuar els seus dos subarbres, si no són buits, per ser tractats al seu moment. Essencialment, la idea que apliquem és que l’arrel del primer arbre ha de ser el primer node del recorregut i la resta de nodes del primer arbre s’han de tractar després de les arrels dels altres arbres de C, perquè hi són a un nivell inferior.

Referencias

Documento similar

El útil de más empleo, tanto para podar co- mo para cortar esquejes y demás necesario pa- ra injertar, es la tijera de fiodar (fig.. Conviene tener una gran- de, de 2o a 25

así mesmo, cando a achega a realiza un terceiro, e por terceiro enténdese calquera persoa distinta do beneficiario do patrimonio, incluídos os pais, titores ou curadores,

1. LAS GARANTÍAS CONSTITUCIONALES.—2. C) La reforma constitucional de 1994. D) Las tres etapas del amparo argentino. F) Las vías previas al amparo. H) La acción es judicial en

De la intersección de la líneas de acción de esta fuerzas se construye el polígono de fuerzas que nos dará la línea de empujes correspondiente a las posiciones de E y R

laborales más afectadas por las olas de calor son aquellas más precarizadas, peor remuneradas y con menor consideración social, aunque se trate de trabajos esenciales para la

La oferta existente en el Departamento de Santa Ana es variada, en esta zona pueden encontrarse diferentes hoteles, que pueden cubrir las necesidades básicas de un viajero que

Resolución do 16 de outubro de 2017, conxunta da Consellería de Educación e Ordenación Universitaria e da Consellería de Economía, Emprego e Industria, pola que

Unha das principais razóns esgrimidas pola literatura para xustificar a introdu- ción de regras fiscais nunha unión monetaria era precisamente esa: que a súa ine- xistencia podía