M3

Page 1

Contenidors seqüencials Jordi Álvarez Canal P06/05001/00577 Mòdul 3



© FUOC • P06/05001/00577 • Mòdul 3

Contenidors seqüencials

Índex

Introducció ..............................................................................................

5

Objectius ...................................................................................................

7

1. Piles .......................................................................................................

9

1.1. Operacions ......................................................................................

9

1.2. Implementació basada en un vector .............................................. 12 1.2.1. Definició de la representació ............................................... 12 1.2.2. Implementació de les operacions ....................................... 14 1.2.3. Anàlisi de costos .................................................................. 14 1.2.4. Codificació en Java .............................................................. 15 1.2.5. Exemple d’ús de la col·lecció .............................................. 16 2. Cues ....................................................................................................... 19 2.1. Operacions ...................................................................................... 19 2.2. Implementació basada en un vector .............................................. 20 2.2.1. Definició de la representació ............................................... 21 2.2.2. Implementació de les operacions ....................................... 23 3. Representacions encadenades ........................................................ 25 3.1. Referències i continguts .................................................................. 25 3.2. Referència nul·la ............................................................................. 28 3.3. Encadenament de dades ................................................................. 28 3.4. Exemple: implementació encadenada de Cua ................................ 30 3.4.1. Definició de la representació ............................................... 30 3.4.2. Implementació de les operacions ....................................... 31 3.5. Gestió de la memòria i recollida d’escombraries ............................ 32 3.6. Representació amb vector i representació encadenada ............................................................ 33 4. Llistes ................................................................................................... 35 4.1. Posicions ......................................................................................... 36 4.2. Recorreguts ..................................................................................... 37 4.3. Operacions ...................................................................................... 38 4.4. Implementació del TAD Llista ........................................................ 40 4.4.1. Definició de la representació ............................................... 41 4.4.2. Implementació de les operacions ....................................... 42 4.5. Recorregut dels elements d’un contenidor: TAD Iterador ............... 46 4.5.1. Implementació .................................................................... 48 4.6. Exemple d’ús del TAD Llista ........................................................... 50 5. Representacions amb vector: redimensionament .................... 53


© FUOC • P06/05001/00577 • Mòdul 3

6. Els contenidors seqüencials a la Java Collections Framework..................................................................... 57 Resum ........................................................................................................ 59 Exercicis d’autoavaluació .................................................................... 61 Solucionari ............................................................................................... 63 Glossari ..................................................................................................... 63 Bibliografia .............................................................................................. 64 Annex ......................................................................................................... 65

Contenidors seqüencials


© FUOC • P06/05001/00577 • Mòdul 3

5

Contenidors seqüencials

Introducció

El concepte de seqüència és cabdal en la programació estructurada. Recordeu els esquemes de recorregut i cerca en una seqüència; i com molts algorismes es poden desenvolupar com l’aplicació d’algun d’aquests patrons, o la seva

En l’assignatura Fonaments de programació, el concepte de seqüència es tracta com una de les nocions bàsiques que permeten l’elaboració d’algorismes.

combinació. Si tenim en compte la importància del tractament seqüencial de les dades, sembla clar que, en moltes ocasions, ens interessarà també emmagatzemar conjunts de dades de manera seqüencial i, posteriorment, accedir-hi també de manera seqüencial. Així, per exemple, sigui quin sigui el tipus de col·lecció que fem servir per a resoldre un problema, molt sovint ens interessarà fer el tractament seqüencial dels elements emmagatzemats. D’altra banda, també ens trobarem en situacions en què guardar els elements de manera seqüencial pot ser la manera natural de representar-los. Casos concrets en el món real poden ser la llista de la compra, les cartes d’una baralla, els cotxes en una línia de muntatge, etc. El conjunt de tipus abstractes de dades (TAD) que ens permetran representar els elements de manera seqüencial són els que es presenten en aquest mòdul. Dit això, cal fer una aclariment important abans de continuar. Ja hem presentat el marc general en què definirem i treballarem amb els contenidors o TAD.

En el mòdul “Tipus abstractes de dades” es presenta el marc general dels TAD.

En aquest mòdul es fa patent la diferència entre especificació i implementació del contenidor. L’especificació la proporciona la interfície Java, que és on tenen accés els usuaris del contenidor. La implementació es correspon amb el codi Java que s’executa quan els usuaris del contenidor fan servir les seves operacions; i ha de romandre sempre amagada per a aquests usuaris. Així doncs, quan parlem de contenidors seqüencials ens podem referir a dues coses diferents: • El contenidor presenta una interfície adequada perquè els usuaris hi puguin treballar de manera seqüencial. Per exemple, una aplicació que representés una cadena de muntatge de cotxes que cada un dels robots de la cadena podria fer servir de contenidor. • La implementació del contenidor és en sí mateixa seqüencial. Això vol dir que les dades estan emmagatzemades físicament a la memòria de l’ordinador de manera seqüencial. En aquest mòdul, presentarem contenidors que proporcionen una interfície dissenyada per al treball seqüencial (és a dir, pertanyents al primer grup). Addicio-

Implementació seqüencial en sí mateixa Un exemple el tenim en la implementació ConjuntVectorImpl del mòdul “Tipus abstractes de dades”. En aquesta implementació, els elements estan emmagatzemats físicament l’un al costat de l’altre, ja que s’emmagatzemen en un vector.


© FUOC • P06/05001/00577 • Mòdul 3

6

Contenidors seqüencials

nalment, la manera més natural i senzilla d’implementar aquests contenidors serà mitjançant implementacions seqüencials (és a dir, totes les implementacions que es veuen en el mòdul són també implementacions seqüencials). A partir d’ara, sempre que fem referència a contenidor seqüencial, estarem fent referència a un contenidor amb una interfície dissenyada per al treball seqüencial. Quan vulguem fer referència a les característiques de la implementació parlarem d’implementació seqüencial. Si reviseu la bibliografia de l’assignatura trobareu que hi ha força contenidors seqüencials i diferents versions –amb més o menys operacions– de cadascun; l’especificació de cada una de les operacions també pot variar lleugerament d’una font a una altra. Això és així perquè no existeixen contenidors en què l’especificació estigui definida de manera universal, com passa amb les lleis de la física. Per tant, cada autor o dissenyador de biblioteques de contenidors defineix el seu conjunt de contenidors de la manera que creu més adequada, amb les operacions adients per als usos de la biblioteca.

Implementacions d’un contenidor seqüencial Si bé normalment les implementacions d’un contenidor seqüencial seran implementacions seqüencials, ens podem trobar amb implementacions seqüencials per a contenidors que no ho siguin. Penseu, per exemple, en la col·lecció Conjunt presentada en el mòdul “Tipus abstractes de dades”: la implementació és seqüencial, però, en canvi, la interfície no està dissenyada per al treball seqüencial.

La biblioteca de contenidors de l’assignatura ha estat dissenyada tenint com a objectiu principal la didàctica. Per aquest motiu, hem decidit proporcionar una biblioteca amb els elements indispensables tant pel que fa a la quantitat de contenidors com a les operacions que proporcionen. En aquest mòdul, veurem els contenidors seqüencials més comuns: les piles, les cues i les llistes. La introducció d’aquests contenidors ens servirà també per a presentar algunes qüestions que ens acompanyaran en la resta de mòduls: les alternatives per a emmagatzemar dades en la memòria de l’ordinador, les construccions com el patró iterador que ens permeten accedir fàcilment als elements d’un contenidor sense haver-ne de conèixer ni la implementació ni bona part de la interfície, i d’altres. En aquest mòdul, com en els següents, primer es presenta cadascun dels tipus de contenidor de manera intuïtiva, després se’n descriu més formalment el comportament, se n’explica a fons la implementació i, per acabar, se’n veuen exemples d’ús.

Les col·leccions del JDK Aquesta política és completament diferent de la usada per Sun en el disseny de les col·leccions que acompanyen el Java; fa servir una jerarquia de col·leccions força més complexa i multitud d’operacions i variants d’aquestes per a cada col·lecció.


© FUOC • P06/05001/00577 • Mòdul 3

7

Objectius

Els materials didàctics d’aquest mòdul proporcionen els coneixements fonamentals perquè l’estudiant assoleixi els objectius següents: 1. Conèixer la interfície i saber usar els TAD seqüencials bàsics: Pila, Cua i Llista. 2. Saber decidir, d’entre els TAD seqüencials bàsics, quin és el més adequat per a resoldre un problema. 3. Entendre les implementacions presentades per als TAD Pila, Cua i Llista; i les diferències bàsiques entre una representació amb vector i una representació encadenada. 4. Entendre el concepte de posició com a noció auxiliar dels TAD posicionals. Saber usar el TAD auxiliar Posicio. 5. Conèixer el concepte d’iterador i saber usar el TAD corresponent per a fer tractaments seqüencials dels elements d’una col·lecció. 6. Saber proporcionar, a partir d’una col·lecció, implementacions d’iterador sobre la base de la seva representació. 7. Comprendre la relació entre els diferents elements del llenguatge Java i l’ús de la memòria que se’n deriva; i comprendre també el sistema de gestió de la memòria que proporciona aquest llenguatge. 8. Ser capaços de dissenyar i codificar algorismes (per exemple, noves operacions dels TAD vistos en el mòdul) fent servir tant representacions amb vector com encadenades.

Contenidors seqüencials



© FUOC • P06/05001/00577 • Mòdul 3

9

Contenidors seqüencials

1. Piles

Una pila és un contenidor en el qual els elements s’insereixen i s’esborren d’acord amb el principi “el darrer que hi entra és el primer que en

LIFO és la sigla de l’expressió anglesa last in, first out.

surt”, conegut per la sigla LIFO.

Figura 1

Imagineu-vos, per exemple, una pila de plats: únicament podem agafar el darrer plat que hem afegit a la pila. També fem servir piles quan juguem a cartes; fins i tot hi ha molts jocs de cartes en què es fa servir més d’una pila, tal com es veu en la figura 1. Després d’haver esmentat aquests dos exemples, segur que vosaltres mateixos sou capaços de trobar algunes situacions més de la vida quotidiana en què s’usen piles. Ja deveu saber que la CPU (és a dir, la unitat central de processament, de l’expressió anglesa central process unit) disposa d’una pila en què les instruccions de codi màquina que la mateixa CPU executa poden posar-hi, consultar-hi i obtenir-

L’estructura interna de la CPU s’estudia en l’assignatura Estructura i tecnologia de computadors.

hi dades. D’una forma semblant, els llenguatges de programació estructurats (incloent els orientats a objectes i en concret Java) fan ús d’una pila per a implementar el mecanisme de crida a procediments. En la figura 2, es veu

Figura 2

una pila de crides d’un programa. Com podeu observar, en aquest moment de l’execució, el programa d’exemple crida a l’operació afegir de la implementació ConjuntVectorImpl del TAD Conjunt. A la vegada, aquesta operació està cridant a l’operació hiEs, que crida al mètode

Pila de crides del programa d’exemple de l’apartat 3 del mòdul “Tipus abstractes de dades” en un moment de la seva execució

privat cercarPosicioElement. Sense necessitat d’endinsar-nos tant en els budells de l’ordinador, el botó “En-

Vegeu el TAD Conjunt definit en el mòdul “Tipus abstractes de dades” d’aquesta assignatura.

rere” que proporcionen la majoria dels navegadors d’Internet fa ús d’una pila de pàgines visitades. Sempre que visitem una pàgina nova, afegim a la pila de pàgines la que fins ara era l’actual; de forma que quan tirem enrere únicament cal agafar la pàgina que hi ha a dalt de tot de la pila (que és la darrera que havíem visitat).

1.1. Operacions

En aquest subapartat presentarem quines són les operacions que proporciona la col·lecció Pila que trobareu a la biblioteca de l’assignatura. Recordeu que les col·leccions de la biblioteca de l’assignatura han estat definides com a tipus paramètrics:

a

Vegeu el tractament de les operacions en el mòdul “Tipus abstractes de dades” d’aquesta assignatura.


© FUOC • P06/05001/00577 • Mòdul 3

10

col·lecció Pila<E> esten Contenidor<E> és

constructor() Crea una pila. @pre cert. @post Retorna una pila buida.

void empilar(<E> elem) Afegeix un element al cim de la pila. @pre cert. @post La pila final és la pila inicial afegint-hi elem al cim.

<E> desempilar() Elimina l’element del cim de la pila (el darrer que s’ha afegit a la pila), i el retorna com a resultat. @pre La pila té com a mínim un element. @post La pila és equivalent a la pila abans d’afegir-hi l’element eliminat. El valor retornat ($return) és l’element esborrat.

<E> cim() Retorna l’element del cim de la pila (el darrer que s’ha afegit a la pila). @pre La pila té com a mínim un element. @post La pila és la mateixa que abans de realitzar l’operació. El valor retornat ($return) és l’element que hi ha al cim de la pila (el darrer element inserit). Aquestes quatre operacions proporcionen el comportament bàsic per construir i treballar amb una pila (juntament amb l’operació estaBuit, definida a Contenidor). A continuació, veureu el funcionament de les operacions de la pila mitjançant exemples. Com podreu comprovar, les operacions poden actuar en dues direccions diferents: modificar l’estat del contenidor i calcular un resultat a partir de l’estat del contenidor.

Si una operació modifica l’estat del contenidor, direm que es tracta d’una operació constructora o modificadora; i si no el modifica, direm que és una operació consultora. La diferència entre una operació constructora i una de modificadora rau en el fet que la primera és imprescindible per a arribar a un estat determinat del contenidor i la segona, no.

Vegem amb més detall aquesta diferència. L’estat d’una pila sempre serà el resultat d’una seqüència de crides als mètodes de pila. Per exemple, l’estat de la pila p de la figura 3, quan té els elements 8, 2 i 7, es pot haver obtingut a partir de la seqüència següent:

Contenidors seqüencials

Observació Fixeu-vos que, a més de les operacions descrites, Pila proporciona també les operacions proporcionades per Contenidor (estaBuit, nombreElements i elements).


© FUOC • P06/05001/00577 • Mòdul 3

11

Contenidors seqüencials

p = <crida al constructor>(); p.empilar(7); p.cim(); p.empilar(4); p.desempilar(); p.empilar(2); p.empilar(8);

Normalment, podem arribar al mateix estat mitjançant diverses seqüències de crides diferents. Per exemple, també podem arribar a la pila esmentada si eliminem la crida a cim. I també si eliminem alhora les crides p.empilar(4) i p.desempilar().

Observeu, doncs, com les operacions cim i desempilar són prescindibles a l’hora d’obtenir la pila de la figura 3. Per tant, les úniques operacions necessàries per a construir aquesta pila són el constructor i empilar.

Estenent aquesta idea d’una pila concreta (un estat concret) a qualsevol pila o estat, direm que el conjunt d’operacions constructores serà aquell conjunt mínim d’operacions que ens permetrà, a partir d’una seqüència de crides a operacions d’aquest conjunt, construir qualsevol estat per a un contenidor.

Vegem la classificació de les operacions de la pila segons aquest criteri:

• Operacions constructores. Són constructor i empilar. Una combinació adequada de crides a aquestes dues operacions ens permet construir qualsevol pila.

• Operacions modificadores. És l’operació desempilar. Aquesta operació modifica la pila traient-ne l’element del cim. Com que sempre serem capaços de construir qualsevol pila cridant únicament a constructor i a empilar, aquesta operació és modificadora (i no constructora).

• Operacions consultores. Són estaBuit i cim, junt amb les altres operacions definides a Contenidor (nombreElements i elements). Aquestes operacions ens permeten consultar l’estat de la pila sense modificar-lo.

Una de les característiques del contenidor pila és que, si mirem el contenidor com una seqüència d’elements, les operacions que ens permeten afegir, esborrar i consultar elements de la seqüència treballen sempre sobre el mateix extrem. Això proporciona el comportament LIFO (“el darrer que hi entra és el primer que en surt”) característic de les piles. Redefinint aquest comportament podem obtenir altres contenidors seqüencials.

Excepció L’operació elements definida a Contenidor és una excepció a aquest principi que comentarem més endavant en aquest mòdul.


© FUOC • P06/05001/00577 • Mòdul 3

12

Contenidors seqüencials

Figura 3

1.2. Implementació basada en un vector La implementació més senzilla d’una pila és usar un vector per a guardar-ne els elements. Això té una implicació important, tal com ja vam veure, amb ConjuntVectorImpl: si usem un vector per a representar els elements, necessitem definir el màxim d’elements que es guardaran en el contenidor. Per tant, estem parlant d’un contenidor afitat.

1.2.1. Definició de la representació Per a definir com s’emmagatzemen els elements de la pila en el vector, podem col·locar el vector en posició vertical al costat d’una de les piles de cartes que hem vist abans, tal com s’aprecia en la figura 4. Figura 4

En el vector tindrem una part que estarà plena amb els elements de la pila i una part que estarà lliure i que farem servir si es van afegint nous elements. Com és habitual en aquest tipus de representacions amb vector, necessitem un atribut enter que ens digui quina part del vector està plena i quina està lliure (de fet, aquest atribut ens indicarà el nombre d’elements guardats). Tal com s’aprecia en la figura 4, guardarem cada element de la pila en una posició del vector, de manera que l’element més “antic” de la pila estarà emmagatzemat a la posició 0 del vector, i l’element més “nou” en la posició més gran de la part ocupada.

1.2.2. Implementació de les operacions A partir de la representació anterior, passem a veure com implementem les operacions de la col·lecció pila. Donem una llista conceptual d’accions que fa

Vegeu el TAD ConjuntVectorImpl en el mòdul “Tipus abstractes de dades” d’aquesta assignatura.


© FUOC • P06/05001/00577 • Mòdul 3

13

Contenidors seqüencials

cada operació. Aquestes accions es poden correspondre amb una instrucció de Java o ser més complexes, i ens ajuden a veure amb molt de detall com s’implementen les operacions amb la representació triada. Posteriorment, es pot completar la descripció de l’operació amb l’anàlisi del codi Java. Addicionalment, al costat de les operacions i de cada una de les accions, hi trobareu el cost asimptòtic de l’operació, que s’especifica fent servir la notació O.

El concepte de cost asimptòtic s’explica en el mòdul “Complexitat algorísmica”.

Com ja sabeu, el cost asimptòtic mesura l’eficiència de les operacions del TAD i ens permet fer-ne una anàlisi aproximada. En aquest primer TAD es detallen els costos de les accions internes de cada una de les operacions. Addicionalment, es dedica un apartat a l’anàlisi i explicació dels costos. col·lecció PilaVectorImpl<E> implementa Pila<E>, ContenidorAfitat<E> • constructor() O(DEFAULT_MAX) – Crida al constructor amb DEFAULT_MAX com a nombre d’elements màxim (O(DEFAULT_MAX)). • constructor(int max) O(max) – Crea el vector d’elements (O(max)). – Assigna el nombre d’elements actual a 0 (O(1)). • boolean estaBuit() O(1) – Comprova si el nombre d’elements és 0 (O(1)). • boolean estaPle() O(1) – Comprova si el nombre d’elements és igual al màxim (O(1)). • int nombreElements() O(1) – Consulta el nombre d’elements (O(1)). • void empilar(E elem) O(1) – Assigna elem a la primera posició lliure del vector d’elements (O(1)). – Incrementa el nombre d’elements actual (O(1)). • E cim() O(1) – Retorna l’element de la darrera posició plena del vector (O(1)). • E desempilar() O(1) – Guarda en una variable auxiliar el cim de la pila (O(1)). – Assigna ‘null’ a la darrera posició plena del vector (O(1)). – Decrementa el nombre d’elements actual (O(1)). Magnituds del cost: DEFAULT_MAX = Nombre màxim d’elements per defecte.

L’operació elements es descriu i s’explica més endavant.


© FUOC • P06/05001/00577 • Mòdul 3

14

Contenidors seqüencials

La implementació PilaVectorImpl implementa dos comportaments complementaris: d’una banda, el comportament de Pila i, de l’altra, el comportament de ContenidorAfitat. Per aquest motiu, la classe implementa aquestes dues interfícies. D’altra banda, es fa servir la sobrecàrrega per a definir dos constructors: un que no té cap paràmetre i un altre que accepta un paràmetre enter. Aquest segon permet especificar la mida màxima de la pila creada mitjançant el paràmetre. Llavors, el primer constructor únicament proporciona un valor per defecte per a aquest paràmetre. La implementació del constructor (amb paràmetre) i d’empilar és equivalent a la que havíem vist per al constructor i l’operació afegir de ConjuntPilaImpl. Les operacions cim i desempilar són també força senzilles i tenen a veure amb l’accés i la gestió de la darrera posició plena del vector.

1.2.3. Anàlisi de costos Com podeu comprovar a partir dels costos de les operacions, PilaVectorImpl és una implementació realment eficient. Totes les operacions tenen un cost constant excepte els constructors, que tenen un cost proporcional al màxim d’elements que pot guardar la pila. Analitzem en detall el cost de dues de les operacions: • constructor(int max). En primer lloc, crea un vector de mida max en què es guardaran els elements de la pila. Això suposa demanar-li a Java el tros de memòria corresponent. L’execució d’aquesta petició es podria fer en realitat en temps constant (O(1)). El llenguatge Java però, quan crea un vector, sempre inicialitza totes les posicions del vector a ‘null’. Per a això, necessita fer un recorregut de les posicions del vector, cosa que resulta en un cost O(max). Posteriorment, s’assigna l’atribut corresponent al nombre d’elements actual a 0. Es tracta d’una única instrucció d’assignació que, per tant, es realitza en temps constant (O(1)). El cost asimptòtic de l’operació serà el màxim de la seqüència d’accions que realitza. Per tant, l’operació té un cost de O(max). • desempilar(). En primer lloc guardem en una variable auxiliar el cim de la pila. Com que disposem del nombre d’elements guardats, podem accedir directament al cim de la pila (en temps constant, O(1)). Així doncs, aquesta acció correspon a una instrucció d’assignació, que per tant realitzarem en temps constant (O(1)). A continuació, cal assignar ‘null’ a la darrera posició plena del vector. L’accés és, de nou, en temps constant. Assignar-hi ‘null’, ho fem també en temps constant. Per acabar, reduim el nombre d’elements actual. Això es tradueix en una instrucció de resta i assignació que realit-

L’ús de la sobrecàrrega L’ús de la sobrecàrrega per a definir diversos constructors, alguns dels quals proporcionen valors per defecte per als paràmetres, és força útil i habitual.

Vegeu l’operació afegir de ConjuntPilaImpl en el mòdul “Tipus abstractes de dades” d’aquesta assignatura.


15

© FUOC • P06/05001/00577 • Mòdul 3

zem en temps constant. Igual que abans, el cost asimptòtic de l’operació serà el màxim de la seqüència d’accions que realitza. Com que totes tenen un cost constant, el cost de l’operació també ho serà.

1.2.4. Codificació en Java A continuació podeu veure com es tradueixen a llenguatge Java les dues operacions que acabem de veure:

PilaVectorImpl.java package uoc.ei.tads; public class PilaVectorImpl<E> implements Pila<E>, ContenidorAfitat<E> { public static final int MAXIM_ELEMENTS_PER_DEFECTE = 256; protected int n; protected E[] elements; public PilaVectorImpl() { this(MAXIM_ELEMENTS_PER_DEFECTE); } public PilaVectorImpl(int max) { elements = (E[])new Object[max]; n = 0; } public int nombreElems() { return public boolean esBuit() { return public boolean esPle() { return public E cim() { return

n++; } public E desempilar() { E aux = elements[n-1]; elements[n-1] = null; n--; return aux; } ... }

( n == 0 ); } (n == elements.length); }

elements[n-1]; }

public void empilar(E elem) { elements[n] = elem;

n; }

Contenidors seqüencials


© FUOC • P06/05001/00577 • Mòdul 3

16

Contenidors seqüencials

En la implementació del mètode desempilar s’assigna ‘null’ a la posició ocupada per l’element desempilat. Aquesta assignació pot semblar supèrflua. De fet, si no la féssim, la implementació de desempilar seguiria garantint el seu contracte.

Aquesta assignació és important perquè el llenguatge Java (a diferència d’altres llenguatges com ara el C++) s’encarrega de gestionar de manera automàtica la memòria, reciclant els espais que s’han deixat de fer servir. Java fa aquesta feina comptant les referències a cada un dels trossets de memòria ocupats per objectes (com ara els objectes referenciats al vector elements del tipus PilaVectorImpl).

En eliminar la referència a l’element desempilat, estem dient a Java que la pila corresponent ja no usa aquest element. Llavors, si no hi ha cap altra referència a l’objecte desempilat en tota la Java Virtual Machine, Java reciclarà la memòria ocupada per aquest objecte. Per raons d’eficiència, Java no garanteix que aquest procés es realitzi immediatament després que el nombre de referències a l’objecte sigui 0.

1.2.5. Exemple d’ús de la col·lecció Com ja hem comentat anteriorment, el TAD Pila és un TAD amb força usos en el món de la informàtica. Vegem-ne un exemple. Moltes impressores, en el procés d’imprimir un document van dipositant els fulls impresos en una safata. Un cop acabada la impressió, si la cara impresa dels fulls és la de sota, podem agafar tot el conjunt de fulls de la safata i aquest estarà perfectament ordenat. En canvi, hi ha impressores (per exemple, moltes impressores d’injecció) en què la cara impresa és la de dalt. Això implica que, quan agafem el document imprès de la safata, hàgim de reordenar tots els fulls. Per a evitar això, normalment el sistema operatiu disposa d’una opció d’impressió que permet imprimir les pàgines d’un document en ordre invers. En aquest exemple implementarem aquesta funcionalitat: a partir d’un document que representarem per un conjunt de pàgines, en volem obtenir un altre d’equivalent al primer, amb les seves pàgines en ordre invers. Es demana definir una classe Document que permeti representar un document com una seqüència de pàgines. Per simplificar, representarem una pàgina com un String. Dins aquesta classe, cal definir un mètode que mostri el document per la sortida estàndard (System.out). Aquest mètode ha de tenir la signatura següent: public void imprimir(boolean ordreInvers) Si el booleà és ‘fals’, el document es mostrarà començant per la primera pàgina. Si el booleà és ‘cert’, es mostrarà en ordre invers.

Recollida d’escombraries El procés de recollir la memòria ocupada per tots aquells objectes que s’han deixat d’usar es coneix en anglès com a garbage collection (literalment, ‘recollida d’escombraries’). En el subapartat 3.5 trobareu una explicació més detallada d’aquest mecanisme.


© FUOC • P06/05001/00577 • Mòdul 3

17

Contenidors seqüencials

Solució Proposem definir una classe Pagina que contingui únicament un String amb el text de la pàgina. Definim una classe Document, que guarda les pàgines d’aquest com un vector de Pagina. A la classe Document, hi definim: • El constructor, que crearà un document buit. • Una operació per a afegir una pàgina al final del document. • Redefinim el mètode toString de la classe Object amb l’objectiu de posar en un String tot el document. A partir d’aquí afegim el mètode imprimir esmentat a l’enunciat, que haurà de ser capaç de mostrar el document començant per la primera pàgina o bé en ordre invers, depenent del valor del paràmetre. En cas que el booleà sigui ‘cert’, ens fa falta capgirar el Document. Amb aquest objectiu, definim un mètode auxiliar protegit anomenat capgirar que crea un nou Document amb les mateixes pàgines que el document original però en ordre invers. Per a implementar el mètode capgirar fem el següent: • Posem les pàgines del document en una pila (començant per la primera). • Creem un document buit, en què anem afegint les pàgines de la pila fins que està buida. Com que la pila segueix l’estratègia LIFO (“el darrer que hi entra és el primer que en surt”), aquest algorisme ens permet fàcilment obtenir un document en què les pàgines estiguin en ordre invers a com estan en el document original. A continuació, teniu un fragment del codi Java: Document.java

package uoc.ei.exemples.modul3; import uoc.ei.tads.*; public class Document { Pagina[] pagines; int numPagines; ... protected Document capgirar() { Document documentCapgirat = new Document(pagines.length); Pila<Pagina> pila = new PilaVectorImpl<Pagina>(pagines.length);

En els recursos electrònics trobareu el codi Java complet de la classe Document, juntament amb les classes Pagina i ProvaDocument.


© FUOC • P06/05001/00577 • Mòdul 3

18

for(int i = 0;i<numPagines;i++) pila.empilar(pagines[i]); while(!pila.esBuit()) documentCapgirat.afegir(pila.desempilar()); return documentCapgirat; } public void imprimir(boolean ordreInvers) { Document docOrdreAdequat = ordreInvers ? capgirar() : this; System.out.println(docOrdreAdequat.toString()); } ... }

Contenidors seqüencials


© FUOC • P06/05001/00577 • Mòdul 3

19

Contenidors seqüencials

2. Cues

En aquest apartat estudiarem el contenidor Cua. De la mateixa manera que la pila, l’ordre dels elements en una cua està completament determinat pel seu ordre d’inserció. El principi que hem fet servir és, però, diferent. En una cua, “el primer que hi entra és el primer que en surt”, principi

FIFO és la sigla de l’expressió anglesa first in first out.

conegut per la sigla FIFO.

Com passa amb les piles, trobem cues en moltes situa-

Figura 5

cions del món real. Normalment, quan aneu a una tenda a comprar, us situeu en una cua de persones que estan esperant. La primera persona de la cua a qui es despatxa és la que porta més temps esperant. Un cop despatxada, aquesta persona s’elimina de la cua i es despatxa el següent que porta més temps esperant. En la imatge, s’aprecia una cua de persones que esperen per comprar una entrada per a un concert. L’ús de cues és força habitual al món real per a organitzar peticions que han de ser ateses per un recurs. En el camp dels ordinadors, aquesta situació també és freqüent. Pen-

Persones fent cua per comprar una entrada per a un concert.

seu, per exemple, en els diferents processos que un ordinador executa suposadament alhora. Si un ordinador disposa únicament d’un processador, en cada moment podrà executar un sol procés. Els processos es posen en una cua, i el processador va seleccionant els processos a executar d’aquesta cua. Un cop executats durant un període de temps curt (segurament de mil·lisegons), el procés es torna

En les assignatures de sistemes operatius s’estudia aquesta tècnica amb detall. La situació descrita és una simplificació en què no es tenen en compte prioritats. Normalment, els processos poden tenir diferents prioritats (depenent de la seva criticitat). En el mòdul “Cues amb prioritat”d’aquesta assignatura, s’estudien aquestes cues.

a posar a la cua i es selecciona el següent. D’una forma similar, el sistema operatiu gestiona l’enviament d’impressions a una impressora mitjançant una cua. En la figura 6, es pot observar un exemple de cua d’impressora. Figura 6

2.1. Operacions Les operacions són anàlogues a les que definíem en l’apartat anterior per a les piles, però amb uns altres noms i amb els comportaments adaptats al principi FIFO.

Sistemes peer to peer Els sistemes peer to peer d’intercanvi de fitxers també fan servir cues per a decidir, en cada moment, a qui envia informació cada un dels nodes. Si bé afegeixen a la simplicitat de les cues un mecanisme força complex de prioritats.


© FUOC • P06/05001/00577 • Mòdul 3

20

col·lecció Cua<E> esten Contenidor<E> és constructor() Crea una cua. @pre cert. @post Retorna una cua buida. void encuar(<E> elem) Afegeix un element a la cua. @pre cert. @post La cua resultant és la cua inicial a la qual s’ha afegit elem. <E> desencuar() Elimina l’element que porta més temps a la cua, i el retorna com a resultat. @pre La cua té com a mínim un element. @post A la cua resultant, queden tots els elements que tenia excepte el més antic. Els elements estan en el mateix ordre. <E> cap() Retorna l’element que porta més temps a la cua. @pre La cua té com a mínim un element. @post La cua és la mateixa que abans de realitzar l’operació. El valor retornat ($return) és l’element que porta més temps a la cua. Al igual que en el cas de la Pila, podem classificar les operacions de la Cua en constructores, modificadores i consultores: • Operacions constructores: el constructor i encuar. • Operacions modificadores: desencuar. • Operacions consultores: estaBuit i cap, junt també amb les altres operacions definides a contenidor (nombreElements i elements). Si mirem una cua com una seqüència ordenada d’elements amb dos extrems, l’operació que ens permet afegir-ne treballa sobre un extrem de la seqüència, i les operacions que ens permeten esborrar i consultar elements treballen sobre l’altre extrem. Això proporciona el comportament FIFO característic de les cues. Figura 7

2.2. Implementació basada en un vector A l’igual que amb una pila, la implementació més senzilla d’una cua és amb un vector per a guardar-ne els elements. En aquest apartat estudiarem aquesta implementació. Parlem, per tant, d’una representació afitada d’una cua.

Contenidors seqüencials

Les operacions del contenidor De la mateixa manera que Pila, Cua també proporciona les operacions a Contenidor (estaBuit, nombreElements i elements).


© FUOC • P06/05001/00577 • Mòdul 3

21

Contenidors seqüencials

2.2.1. Definició de la representació Podem intentar definir una representació com les que hem fet servir fins ara per a PilaVectorImpl i ConjuntVectorImpl. És a dir, a partir del vector en què guardarem

Vegeu PilaVectorImpl en l’apartat 1 d’aquest mòdul i ConjuntVectorImpl en el mòdul “Tipus abstractes de dades” d’aquesta assignatura.

els elements, i amb l’ajuda d’un atribut enter que ens indica el nombre d’elements emmagatzemats, dividirem el vector en dues parts: 1) La part plena del vector. 2) La part amb posicions del vector lliures. La part plena començaria a la posició 0 (on guardaríem l’element més antic), i l’atribut enter ens serviria per a distingir on acabaria la part plena i on començaria la lliure, tal com es veu en la figura 8. Figura 8

Aquesta representació té un problema d’eficiència. Observeu com tots dos extrems de la cua es veuen modificats per alguna operació: l’operació encuar afegeix un element a un dels extrems, mentre que l’operació desencuar treu un element de l’altre extrem (a diferència del que passava amb la pila, on totes les operacions que hi modificaven l’estat actuaven sobre el mateix extrem). En aquesta situació, l’operació de desencuar comportaria moure tots els elements una posició, tal com es pot apreciar en la figura 9. Aquest moviment comporta clarament un recorregut per tots els elements de la cua i, per tant, un cost lineal respecte al nombre d’elements de la cua. Figura 9

Un cost lineal no sempre serà dolent. La linealitat pot ser acceptable segons la situació i la magnitud en què es manifesti. Ara bé, sempre que hi hagi una representació equivalent amb la qual puguem obtenir costos asimptòtics més bons, descartarem la representació del cost lineal (o, en general, del cost asimptòtic més dolent).

El cost de la linealitat s’estudia en el mòdul “Disseny d’estructures de dades. Biblioteques de col·leccions” d’aquesta assignatura.


© FUOC • P06/05001/00577 • Mòdul 3

22

Podríem intentar invertir l’ordre en què es guarden els elements al vector, desant el darrer element afegit a la posició 0, i el més antic a la posició més propera a la zona lliure. Això permetria tenir una implementació per a desencuar en temps constant. Ara bé, no solucionaria el problema, ja que llavors seria encuar la que tindria un cost lineal. Deixem el raonament com a exercici per a l’estudiant. El problema és fer correspondre de manera fixa un dels extrems de la seqüència amb la posició 0 del vector d’elements. Sempre que executem l’operació que modifica aquest extrem, caldrà un desplaçament dels elements, amb el corresponent cost lineal. Podem solucionar aquest problema fent que tots dos extrems de la part plena del vector es puguin desplaçar. Ho fem introduint un nou atribut enter anomenat primer, que ens indica la primera posició del vector ocupada, tal com es mostra en la figura 10. Figura 10

Cada cop que es desencua un element de la cua, s’incrementa l’atribut primer. Aquesta solució proporciona implementacions en temps constant per a encuar i per a desencuar. Tot i així, encara hi ha un problema: què passa si encuem i desencuem molts cops (com probablement passi en molts dels usos d’una cua)? Arribarà un moment que la darrera posició ocupada del vector esdevindrà la darrera posició real del vector. En aquesta situació, ens agradaria aprofitar les posicions de la part inicial del vector que no estan ocupades (abans de la posició primer). Podem aprofitar aquestes posicions si considerem el vector com una estructura circular en la qual la posició 0 seria la posició immediatament posterior a la darrera posició plena, tal com es veu en la figura 11, en què hi ha representada una cua equivalent a la de la figura 10. Figura 11

Contenidors seqüencials


© FUOC • P06/05001/00577 • Mòdul 3

23

Implementar la circularitat en un vector és força fàcil i no complica massa el codi resultant. Únicament cal aplicar el mòdul segons la mida del vector d’elements. Així doncs, a partir d’una posició p en un vector v, la següent posició del vector serà sempre: p + 1 % elements.length Utilitzant aquesta estratègia de la representació circular, aconseguim implementacions en temps constant per a totes les operacions de consulta i modificació de la cua i, a més, sempre podem aprofitar la totalitat del vector en què guardem els elements.

2.2.2. Implementació de les operacions col·lecció CuaVectorImpl<E> implementa Cua<E>, ContenidorAfitat<E> • constructor() O(DEFAULT_MAX) – Crida al constructor amb DEFAULT_MAX com a nombre d’elements màxim (O(DEFAULT_MAX)). • constructor(int max) O(max) – Crea el vector d’elements (O(max)). – Assigna el nombre d’elements actual i primer a 0 (O(1)). • boolean estaBuit() O(1) – Comprova si el nombre d’elements és 0 (O(1)). • boolean estaPle() O(1) – Comprova si el nombre d’elements és igual al màxim (O(1)). • int nombreElements() O(1) – Consulta el nombre d’elements (O(1)). • void encuar(E elem) O(1) – Assigna elem a la primera posició lliure del vector d’elements després de la part plena del vector (O(1)). – Incrementa el nombre d’elements actual (O(1)). • E cap() O(1) – Retorna la primera posició de la part plena del vector (O(1)). • E desencuar() O(1) – Guarda en una variable auxiliar el cap de la cua (O(1)). – Assigna ‘null’ a la primera posició plena del vector (O(1)). – Passa primer a la següent posició (O(1)). – Decrementa el nombre d’elements actual (O(1)).

Contenidors seqüencials


24

© FUOC • P06/05001/00577 • Mòdul 3

Magnituds del cost: DEFAULT_MAX = Nombre màxim d’elements per defecte. CuaVectorImpl.java package uoc.ei.tads; public class CuaVectorImpl<E>

implements Cua<E>, ContenidorAfitat<E> {

public static final int MAXIM_ELEMENTS_PER_DEFECTE = 256; protected E[] elements; protected int n; private int

primer;

public CuaVectorImpl() { this(MAXIM_ELEMENTS_PER_DEFECTE); } public CuaVectorImpl(int max) { elements = (E[])new Object[max]; n = 0; primer = 0; } private int posicio(int posicio) { return posicio % elements.length; } private int seguent(int posicio) { return (posicio+1)==elements.length ? 0 : posicio + 1; } public void encuar(E elem) { int darrer = posicio(primer + n); elements[darrer] = elem; n++; } public E desencuar() { E elem = elements[primer]; elements[primer] = null; primer = seguent(primer); n--; return elem; } ... }

La implementació és una mica més complexa que la de la col·lecció Pila a causa de la representació circular. Amb l’objectiu de mantenir el codi el més clar possible, s’han fet servir dos mètodes privats, posicio i seguent, que s’ocupen de la circularitat.

Contenidors seqüencials


© FUOC • P06/05001/00577 • Mòdul 3

25

3. Representacions encadenades

El llenguatge Java diferencia dos grups de tipus diferents: els tipus bàsics, proporcionats directament pel mateix llenguatge, i equivalents als tipus que podem trobar en llenguatges estructurats no orientats a objectes com el C o el Pascal (int, char i boolean); i les classes. Java dóna un tractament diferent a aquests dos grups de tipus diferents. Quan en un programa Java es defineix una variable d’un tipus bàsic, el compilador Java reserva espai per a un objecte d’aquest tipus bàsic i associa la variable amb el tros de memòria que s’ha reservat. Sempre que es modifica o es consulta aquesta variable, s’està accedint al tros de memòria associat a la variable. En canvi, quan es defineix una variable el tipus de la qual correspon a una classe A, el compilador de Java reserva espai per a una adreça de memòria, i associa la variable amb l’espai reservat. Observeu que, en aquest cas, l’espai reservat per a la variable no és l’objecte mateix, sinó únicament una adreça de memòria. Allà s’emmagatzemarà l’adreça d’alguna instància de A. A aquesta adreça se l’anomena habitualment referència.

3.1. Referències i continguts El concepte de referència és semblant al concepte d’apuntador d’altres llenguatges com el C++, C, Pascal i d’altres. Igual que es fa habitualment en aquests llenguatges, direm que una referència “apunta” a un objecte quan l’adreça continguda en l’espai de memòria corresponent a la referència és la d’aquest objecte. A diferència del que passa en la majoria de llenguatges que permeten treballar amb apuntadors, en Java les referències queden amagades, i als ulls del programador sempre s’està treballant directament amb l’objecte apuntat. A continuació, en el bloc de codi següent, presentem un programa d’exemple amb el qual es pot observar aquest comportament diferent per a les variables de tipus bàsics i les de tipus definits. El programa imprimeix els nombres primers entre el 2 i un número màxim que es demana a l’usuari interactivament. Les línies de codi del mètode main s’han numerat per a facilitar-ne l’explicació posterior que en farem.

Contenidors seqüencials


© FUOC • P06/05001/00577 • Mòdul 3

26

Primers.java package uoc.ei.exemples.modul3.referencia; ... public class Primers { ... 1 public static void main(String[] args) { 2

String str;

3

long maxim;

4

try {

5

str = Utilitats.llegirString("",System.in);

6

try {

7

maxim = Long.parseLong(str);

8

for(long i=2; i<maxim; i++)

9

if (esPrimer(i))

10

System.out.print(i+" ");

11

...

12 13

} }

14 } }

Fixeu-vos en les variables maxim i str. La primera és instància d’un tipus bàsic (long), i la segona d’una classe (o tipus definit). En la figura 12 podem observar el comportament diferenciat per als dos casos, amb una representació esquemàtica de la memòria de l’ordinador després d’haver-se executat les línies 3, 5 i 7.

Figura 12

Contenidors seqüencials


© FUOC • P06/05001/00577 • Mòdul 3

27

Contenidors seqüencials

Variable maxim: • En la línia 3 es declara la variable maxim. Això associa l’esmentada variable a un espai de memòria en què s’emmagatzemarà un valor de tipus long (el valor de la variable). • En la línia 7 es dóna valor a maxim. – En primer lloc, s’avalua l’expressió Long.parseLong(str). Això retornarà un valor de tipus long. – A continuació, es realitza l’assignació a maxim (maxim = ...). Aquesta assignació resulta de copiar aquest valor en l’espai de memòria reservat per a maxim. En canvi, per a la variable str la història és diferent: • En la línia 2 es declara. Això associa la variable a un espai de memòria on s’emmagatzemarà una adreça de memòria. • En la línia 5 es dóna valor a str. – En primer lloc, s’avalua l’expressió següent, que retornarà un objecte de tipus string. Aquest objecte estarà emmagatzemat en la memòria, posicionat en alguna adreça (que per a facilitar l’explicació anomenarem @XXX). Utilitats.llegirString("",System.in). – A continuació es realitza l’assignació a str (str = ...). Aquesta assignació resulta en posar, en l’espai de memòria reservat per a str, l’adreça @XXX.

Queda clar que en l’espai de memòria corresponent a variables de tipus bàsics es guarda directament el contingut (o valor) de la variable. En canvi, per a variables que tenen per tipus una classe, en l’espai de memòria es guarda una referència al contingut (o valor). És molt important tenir clara aquesta distinció, ja que a partir d’ara estudiarem bastants estructures de dades en què es fa ús intensiu de referències.

Tots els llenguatges que permeten treballar amb apuntadors o referències disposen d’alguna construcció per a demanar memòria al sistema operatiu. En el llenguatge Java, aquesta petició la realitza la construcció new, que en primer lloc demana la memòria necessària al sistema operatiu, i en segon lloc s’encarrega d’executar el mètode constructor corresponent.

Declaració de variables Moltes vegades, en Java, la declaració d’una variable es fa en el mateix moment en què se li dóna valor. En els exemples precedents ho hem fet en línies diferents perquè quedés més clar què correspon a la declaració i què a la creació d’objectes (i assignació de valor a una variable).


© FUOC • P06/05001/00577 • Mòdul 3

28

Contenidors seqüencials

3.2. Referència nul·la Tots els llenguatges que treballen amb apuntadors, encara que sigui de manera emmascarada com el Java, ofereixen al programador la possibilitat de dir que una variable no està associada, en un moment determinat, a cap objecte. En Java, això es fa mitjançant el valor especial ‘null’. Quan el valor d’una variable és ‘null’, l’espai de memòria associat a la variable no conté cap adreça de memòria vàlida (que apunti a un contingut), sinó que conté una marca que indica que la variable no té cap objecte associat.

3.3. Encadenament de dades Fins ara havíem construït estructures de dades fent servir una tècnica com l’agregació i aprofitant la facilitat de construir vectors d’objectes.

Vegeu la tècnica de l’agregació en l’assignatura Programació orientada a objectes.

La possibilitat de treballar amb adreces de memòria ofereix nous recursos i molta flexibilitat per a la definició de noves estructures de dades. A continuació presentem un exemple senzill: la definició d’una classe Persona, on guardem una referència a la mare i al pare d’aquella persona. Persona.java package uoc.ei.exemples.modul3.referencia; public class Persona { String nom; Persona pare; Persona mare; public Persona(String nom) { this.nom = nom; mare = null; pare = null; } public Persona(String nom,Persona mare,Persona pare){ this.nom = nom; this.mare = mare; this.pare = pare; } ... }

Noteu que estem fent referència a la classe Persona dins de la mateixa definició de classe Persona. D’aquesta tècnica se’n diu, en general, recursivitat; i el seu ús és molt habitual en la definició d’estructures de dades. D’aquelles estructures de dades que contenen referències recursives en direm estructures de dades recursives.

El tema de la recursivitat s’explica més en profunditat en el mòdul “Arbres” d’aquesta assignatura.


© FUOC • P06/05001/00577 • Mòdul 3

29

Contenidors seqüencials

Les estructures de dades recursives ens permeten definir estructures de dades que estiguin compostes d’elements encadenats entre si. Continuant amb l’exemple anterior, podem crear una “família” de Persona tal com es mostra en el següent codi: Persona.java

Unes velles conegudes desconegudes Probablement, vosaltres mateixos, en algun moment deveu haver definit estructures de dades recursives sense saber que tenien aquest nom.

public class Persona { ... public static void main(String[] args) { Persona aviPere = new Persona("Pere"); Persona aviaPaquita = new Persona("Paquita"); Persona mare = new Persona("Carme",aviPere, aviaPaquita); Persona onclePere = new Persona("Pere",aviPere, aviaPaquita); Persona pare = new Persona("Jordi"); Persona joan = new Persona("Joan",mare,pare); ... } }

En la figura 13 teniu una representació gràfica dels objectes Persona creats. A la part esquerra en podeu veure una representació conceptual i a la part dreta en teniu un esquema parcial més proper a la memòria de l’ordinador. A partir d’una Persona, per exemple en Joan, podem accedir a la seva mare i al seu pare, i des d’aquests als avis. I així successivament fins que trobem una Persona que no té definit mare ni pare (és a dir, el valor d’aquests atributs sigui ‘null’). Figura 13

De tot aquest grup d’objectes en diem estructura de dades encadenada. I de cada un dels objectes que la composen en diem node.


© FUOC • P06/05001/00577 • Mòdul 3

30

Contenidors seqüencials

3.4. Exemple: implementació encadenada de Cua L’ús d’encadenaments ens serà molt útil a l’hora de definir noves estructures de dades. Addicionalment, totes aquelles implementacions de col·leccions que es basen en una representació amb un vector, es poden traslladar a una representació que, en lloc de fer servir vectors, faci servir nodes i encadenaments.

Per a abreujar, anomenarem representacions encadenades aquest tipus de representacions amb nodes i encadenaments.

Vegem a continuació, a tall d’exemple, la representació encadenada del TAD Cua.

Vegeu el TAD Cua definit en l’apartat 2 d’aquest mòdul didàctic.

3.4.1. Definició de la representació Tota representació encadenada d’una col·lecció està definida a dos nivells diferents: • La representació del node, que ens permetrà emmagatzemar un element de la col·lecció i proporcionarà els encadenaments necessaris per a accedir a altres nodes. • La representació de la col·lecció mateixa, que normalment consistirà en un o més encadenaments, que ens permetran accedir als nodes on estan emmagatzemats els elements; i la informació addicional com, per exemple, el nombre d’elements de la col·lecció. El primer pas, i també el més important, per a proporcionar la representació és definir els encadenaments necessaris (tant per als nodes com per a la col·lecció). Per a això, caldrà estudiar les operacions que cal implementar i proporcionar la configuració d’encadenaments que permeti una implementació més eficient de les operacions de la col·lecció. En el cas de la representació encadenada del TAD Cua, tenim que: 1) Hi haurà una seqüència de nodes, amb un primer i un darrer. 2) En un moment determinat, únicament necessitarem accés al primer i al darrer node. 3) En el moment d’encuar, necessitarem accés al darrer node. 4) En el moment de desencuar, necessitarem accés al primer node. Hem de tenir en compte també que, després de desencuar, el segon node passa a ser el nou primer.

Encapsulació Sovint, els nodes queden totalment ocults a l’usuari de la col·lecció, que únicament accedirà a l’objecte Col·lecció i a les seves operacions.


© FUOC • P06/05001/00577 • Mòdul 3

31

Contenidors seqüencials

D’aquests quatre punts podem concloure que els nodes estaran encadenats seqüencialment (punt 1), de manera que des del primer es pugui accedir al segon, del segon al tercer, i així fins al darrer. És a dir, que volem tenir encadenaments cap al següent node (punt 4). A més, en la representació de la col·lecció, ens caldrà tenir encadenaments per a accedir en temps constant al primer i al darrer.

Vegeu els encadenaments cap als següents nodes en l’apartat 4 d’aquest mòdul didàctic.

A partir de tot això, una bona representació és la que es mostra en la figura 14. Figura 14

D’aquesta mena d’estructures en què cada node té un encadenament que l’encadena amb el següent, se’n diu habitualment llista simplement enllaçada, i és força útil per a implementar un gran nombre de col·leccions amb estructura seqüencial mitjançant representacions encadenades.

3.4.2. Implementació de les operacions Un cop definida la representació, vegem la descripció de la implementació de les operacions més rellevants. Noteu que, mentre que les implementacions amb vector implementaven la interfície ContenidorAfitat, no té sentit que les implementacions encadenades ho facin, ja que no existeix cap fita màxima per al nombre d’elements. Tingueu en compte, però, que existeix un límit físic quant al nombre de elements d’una implementació encadenada determinat per la memòria disponible (determinada pel sistema operatiu). En Java, si en algun moment s’arriba a aquest límit, la Java Virtual Machine llança una RuntimeException, i provoca normalment la finalització de l’execució de l’aplicació. col·lecció CuaEncadenadaImpl<E> implementa Cua<E> • constructor() – Inicialitza primer i darrer a ‘null’. • void encuar(E elem) O(1) – Creem un nou node amb ‘elem’ i ‘null’ com a següent. – Si el primer és ‘null’ llavors primer = ‘nou node’. – si no, darrer.seguent = ‘nou node’. – Assignar darrer al ‘nou node’. – Incrementar el nombre d’elements de la cua.

La falta de memòria en els diferents llenguatges La no-disponibilitat de memòria es tracta de manera diferent segons els llenguatges de programació. Així, per exemple, en C o en C++, les funcions que obtenen memòria del sistema operatiu retornen l’equivalent a ‘null’ i és responsabilitat del programador tractar aquesta situació.


© FUOC • P06/05001/00577 • Mòdul 3

32

Contenidors seqüencials

• E desencuar() O(1) – Guardem l’element del primer node en una variable auxiliar. – Fem que el primer de la cua passi a ser el següent del primer actual. – Si el nou primer és ‘null’, llavors fem que el darrer sigui ‘null’.

Podeu trobar la implementació encadenada de Cua en llenguatge Java com a recurs electrònic en els exemples d’aquest mòdul.

– Disminuïm el nombre d’elements actual. – Retornem l’element que hem guardat al principi.

3.5. Gestió de la memòria i recollida d’escombraries El primer pas de la descripció de l’operació encuar de l’apartat anterior fa referència a la creació d’un node. Aquesta creació correspon a la construcció new del llenguatge Java, que com ja hem comentat anteriorment, en primer lloc, demanarà la memòria necessària al sistema operatiu i, en segon lloc, executarà el constructor corresponent. En l’operació encuar es demana memòria al sistema operatiu per a nous nodes que s’encuaran al final de la cua. D’altra banda, en l’operació desencuar es deixen de fer servir nodes que s’havien creat mitjançant encuar. Aquests nodes ja no estaran accessibles a partir de cap encadenament ni de l’objecte Cua ni dels nodes pertanyents a la cua; i, de fet, ja no els necessitarem més. En la figura 15 es pot observar com, partint de la cua que ens ha servit d’exemple en la figura que mostra la representació encadenada, després d’una operació de desencuar tenim un node que ja no forma part de la cua. Figura 15

Aquest node ja no és necessari. A més, ni tan sols és accessible per a la mateixa representació de cua. Per tant, és desitjable que la memòria ocupada per tots aquells nodes que ja no farem servir torni al sistema operatiu. Si no fos així, després de moltes operacions d’encuar i desencuar, podríem esgotar la memòria de l’ordinador únicament amb aquests nodes que ja no formen part de la cua. Cada llenguatge de programació defineix el seu propi mecanisme per a reutilitzar la memòria ocupada per objectes escombraria. Però tots es poden classificar segons dues grans línies: 1) La gestió de la memòria és responsabilitat del programador. En aquest cas, el llenguatge ha d’oferir alguna construcció perquè el programador retorni la

Objectes escombraria D’aquests nodes, o més en general objectes, que en algun moment s’han creat però que ja no usarem més, en direm objectes escombraria (en anglès, garbage).


© FUOC • P06/05001/00577 • Mòdul 3

33

Contenidors seqüencials

memòria ocupada al sistema operatiu quan un objecte ja no s’hagi d’usar més en l’execució del programa. Llenguatges com el Pascal, C i C++ ofereixen les construccions corresponents, i assumeixen que el programador s’encarrega de cridar-les en els moments adequats. Així, C++ ofereix els operadors new per crear objectes (agafar memòria) i delete, per destruir-los (retornar la memòria al sistema operatiu). 2) La gestió de la memòria és responsabilitat del llenguatge de programació. En aquest cas, el llenguatge de programació ha de ser capaç de detectar quan un objecte no és accessible; i si no ho és, retornar la memòria ocupada al sistema operatiu. Aquest tipus de llenguatges necessiten, doncs, un mecanisme de “recollida d’escombraries” (en anglès, garbage collection). El programador no necessita executar el mecanisme de recollida d’escombraries en cap moment, sinó que s’executa com un procés transparent i paral·lel a l’execució del nostre programa. El Java i els llenguatges funcionals, com per exemple, el Lisp, incorporen mecanismes de recollida d’escombraries. Així doncs, el Java únicament disposa de l’operador new, sense cap contrapartida que retorni la memòria al sistema operatiu. Els llenguatges en què la gestió depèn del programador tenen el desavantatge que poden patir més fàcilment problemes de pèrdua de memòria perquè el programador s’ha oblidat d’alliberar “objectes escombraria”. En canvi, tenen l’avantatge que la gestió de l’espai no suposa cap sobrecost d’eficiència.

3.6. Representació amb vector i representació encadenada Com heu pogut comprovar, existeix més d’una implementació per a un mateix TAD. En concret, moltes vegades, us trobareu amb el dilema d’haver de triar entre una implementació amb vector i una implementació encadenada. La decisió a vegades no és ni fàcil i ni clara. Els punts següents posen de manifest avantatges i desavantatges de les dues representacions: • En una representació amb vector, necessitem conèixer el nombre d’elements màxim que cal guardar en el moment de crear el vector. En canvi, en una representació encadenada podem anar agafant l’espai necessari sobre la marxa. • Les representacions amb vector ens permeten accedir directament a qualsevol element per l’índex de la posició en què estan emmagatzemats. En canvi, en una representació encadenada accedim a un node a partir d’un encadenament que tenim en un altre node. • En una representació amb vector, malgastem espai en estar ocupant memòria per a les posicions del vector que estan lliures (no sempre estarem

Redimensionament de vectors La representació amb vector es pot fer més “intel·ligent”, de manera que no calgui conèixer el nombre d’elements màxim per guardar de bon principi, com veurem en l’apartat 5 d’aquest mòdul.


© FUOC • P06/05001/00577 • Mòdul 3

34

ocupant la mida màxima). En canvi, en una representació encadenada, no hi ha posicions lliures. • En una representació encadenada, necessitem espai per als encadenaments; mentre que en un vector no tenim aquests encadenaments. • En una representació amb vector, nosaltres mateixos gestionem i controlem l’espai lliure (això afegeix complexitat, però a vegades pot interessar), mentre que en una representació encadenada la gestió correspon al llenguatge de programació, que la delega normalment al sistema operatiu. A partir d’aquests punts, i segons les condicions i restriccions del problema que intentem resoldre, tindrem elements per a decidir quina de les dues representacions és més adient en cada moment.

Contenidors seqüencials


© FUOC • P06/05001/00577 • Mòdul 3

35

Contenidors seqüencials

4. Llistes

Ja heu estudiat les col·leccions Pila i Cua. Totes dues són col·leccions seqüencials en les quals únicament es poden consultar i modificar els extrems. Moltes vegades ens interessarà treballar amb estructures seqüencials en què puguem accedir i modificar la seqüència d’elements en qualsevol part. A aquest tipus de col·leccions, les anomenem llistes ; i com les piles i les cues, a grans trets, es corresponen amb el que entenem en el món real pel terme “llista”. Per exemple, quan anem a comprar al mercat, normalment fem una llista amb les coses que cal comprar (sobretot si som desmemoriats o hem de comprar una quantitat molt gran d’elements). Un cop al mercat, actuarem segons el següent algorisme: 1) Revisarem la llista. 2) Decidirem la següent parada on comprarem. 3) Comprarem els elements de la llista que puguem en aquesta parada. 4) Eliminarem aquests elements de la llista. Repetirem aquest procediment mentre quedin elements a la llista. També podríem usar una pila o una cua per a emmagatzemar els elements que cal comprar; però l’algorisme seria bastant més ineficient, ja que tindríem accés només a l’element d’un dels extrems, de manera que no sabríem si hi ha més elements per comprar en aquesta parada, i potser ens caldria tornar-hi després. Un editor de text Un exemple més relacionat amb la informàtica de l’ús de llistes el podem trobar en un editor de text. Quan editem un document, estem editant una llista de línies de text. Cada una de les línies de text està constituïda per una llista de caràcters. Els editors de text acostumen a tenir un cursor, que és el lloc per on podem modificar tant la llista de caràcters (inserint o esborrant caràcters) com la llista de línies de text (inserint, per exemple, una nova línia).

Així doncs, molts cops ens serà útil poder disposar d’una col·lecció que correspongui a una seqüència sense restriccions d’accés, i que com ja hem dit, anomenarem llista. Els elements de la llista tindran un ordre determinat segons com s’hagin inserit a la llista: hi haurà un primer i un darrer, i cada element excepte el darrer tindrà un següent. Addicionalment, ens interessarà realitzar les operacions següents: • Inserir nous elements en qualsevol punt de la llista. • Esborrar qualsevol dels elements de la llista. • Recórrer la seqüència d’elements emmagatzemats.

Organitzacions alternatives dels elements Una alternativa per a fer servir una cua o una pila seria agrupar els elements per parades. Però això treu flexibilitat a l’hora de construir la llista. Què passa si la persona que fa la llista no és la mateixa que va a comprar i no coneix les parades?


© FUOC • P06/05001/00577 • Mòdul 3

36

Contenidors seqüencials

Per a aconseguir això d’una manera neta, la col·lecció Llista s’ajudarà de dos tipus addicionals: el tipus Posicio i el tipus Recorregut.

4.1. Posicions El primer d’aquests tipus ens permetrà guardar o fer referència al context posicional d’un element en la llista. El concepte de posicio és força semblant al de node que hem vist en l’apartat anterior: una posició tindrà un element següent i un anterior (que podran ser ‘null’ si es tracta del darrer o primer element). En canvi, ambdós conceptes tenen una diferència essencial: mentre que un node és una representació física totalment lligada a qüestions d’implementació, una posició és un TAD desvinculat de tota representació i de tota implementació. Com veurem més endavant, podrem implementar una posició amb un node. Però no necessitem saber com està definit ni quins encadenaments té per a treballar amb posicions. Aquest és el principal avantatge de treballar amb posicions: desvincular els algorismes que usin llistes (o altres col·leccions posicionals que veurem més endavant) sense necessitat de conèixer la implementació que hi ha al darrere. El TAD Llista oferirà un conjunt d’operacions que permetran modificar o accedir als elements de la llista en funció d’informació posicional. Així podrem, per exemple, inserir un element després d’una posició determinada, o esborrar una posició determinada. En la figura 16 podeu veure una representació conceptual d’una llista d’enters amb 6 elements. Observeu que, per a realitzar aquestes operacions posicionals sobre la llista, no en tenim prou amb la informació de llista: “insereix després del 5”, o “esborra el 3”. En el primer cas, la llista mateixa hauria de buscar el 5; i en el segon cas no sabria a quin 3 ens referim. Els valors guardats dintre de les posicions de la llista són simples enters i no contenen informació posicional en absolut. En canvi, sí és factible realitzar la següent operació: “insereix després de la posició X” (per exemple, la tercera posició). Figura 16

Lectura recomanada El TAD Posicio i l’enfocament posicional d’algunes de les col·leccions de la biblioteca està inspirat en l’aproximació de Goodrich i Tamassia (2001). En aquesta obra podreu trobar força material complementari sobre el tema. L’enfocament, però, és lleugerament diferent al nostre.


© FUOC • P06/05001/00577 • Mòdul 3

37

Contenidors seqüencials

El TAD Posicio té una única operació que permet recuperar l’element emmagatzemat. La biblioteca de TAD de l’assignatura ha estat dissenyada de manera que la resta d’operacions posicionals són responsabilitat de la col·lecció o del TAD Recorregut. tipus Posicio<E> és E getElem() Retorna l’element guardat a la posició. @pre Cert. @post L’element retornat és l’element guardat a la posició.

4.2. Recorreguts El TAD Recorregut ens permet recórrer les posicions d’una col·lecció posicional (com, per exemple, el TAD Llista) en un cert ordre. En el cas de les llistes, el recorregut més habitual comença per la primera posició, continua amb la segona, i segueix avançant posicions fins a arribar a la darrera. En l’exemple inicial de la llista de la compra, crearíem un recorregut sobre la llista de coses per comprar cada cop que arribéssim a una parada. El recorregut ens permetria recórrer de manera ordenada les posicions de la llista de la compra. Llavors per a cada posició: 1) accediríem al valor de la posició i determinaríem si el podem comprar en aquella parada, 2) en cas afirmatiu, el compraríem i faríem servir l’operació posicional corresponent del TAD Llista per a esborrar la posició de la llista, 3) avançaríem a la posició següent. Un recorregut és un objecte completament a part de la col·lecció que recorre i té un estat propi que consisteix en: a) una referència a la col·lecció que s’està recorrent, i b) l’element actual. D’aquesta manera, si ens interessés, podríem mantenir múltiples recorreguts independents en la mateixa col·lecció.

La col·lecció no té cap coneixement dels recorreguts que hi ha en cada moment, ni tampoc del seu estat. Per aquest motiu, s’ha d’anar molt en compte quan s’esborren elements d’una col·lecció: si s’esborra l’element actual d’algun recorregut actiu, aquest recorregut quedarà en un estat inconsistent!

Com la resta de TAD, el TAD Posicio està definit a la biblioteca de TAD de l’assignatura com una interfície Java.


© FUOC • P06/05001/00577 • Mòdul 3

38

tipus Recorregut<E> és boolean hiHaSeguent() Comprova si hi queden posicions per recórrer. @pre Cert @post retorna ‘cert’ si queden posicions per recórrer i ‘fals’, en cas contrari. Posicio<E> seguent() Retorna la següent posició. @pre hiHaSeguent() @post La següent posició del recorregut.

4.3. Operacions Els TAD auxiliars Posicio i Recorregut permeten accedir seqüencialment a les posicions d’una llista. Partint d’això, el TAD Llista proporciona un conjunt d’operacions que permeten: a) crear un recorregut per a una llista, i b) realitzar diferents operacions d’actualització sobre la llista, a partir d’una posició. col·lecció Llista<E> és constructor() Crea una llista buida. @pre Cert. @post Retorna una llista sense cap element. Posicio<E> afegirAlPrincipi(E elem) Afegeix un element al principi de la llista i retorna la nova posició. @pre Cert. @post L’enumeració seqüencial dels elements de la llista és elem + l’enumeració dels elements d’old(this). Posicio<E> afegirAlFinal(E elem) Afegeix un element al final de la llista i retorna la nova posició creada. @pre Cert. @post L’enumeració seqüencial dels elements de la llista és l’enumeració dels elements d’old(this) + elem. Posicio<E> afegirAbansDe(Posicio<E> pos,E elem) Afegeix un element a la llista just abans de la posició pos i retorna la nova posició creada. @pre pos és una posició vàlida de la llista. @post L’enumeració seqüencial dels elements de la llista és l’enumeració dels elements d’old(this) fins a pos (exclosa) + elem + l’enumeració de old(this) a partir de pos (inclosa).

Contenidors seqüencials


© FUOC • P06/05001/00577 • Mòdul 3

39

Posicio<E> afegirDesprésDe(Posicio<E> pos,E elem) Afegeix un element a la llista just després de la posició pos i retorna la nova posició creada. @pre pos és una posició vàlida de la llista. @post L’enumeració seqüencial dels elements de la llista és l’enumeració dels elements d’old(this) fins a pos (inclosa) + elem + l’enumeració de old(this) a partir de la posició següent a pos. E esborrarPrimer() Esborra el primer element de la llista i retorna el seu valor. @pre La llista no està buida. @post L’enumeració seqüencial dels elements de la llista és l’enumeració dels elements de old(this) menys el primer element. E esborrar(Posicio<E> pos) Esborra la posició pos, retornant el seu valor. @pre pos és una posició vàlida de la llista. @post L’enumeració seqüencial dels elements de la llista és l’enumeració dels elements de old(this) excloent pos. E esborrarSeguent(Posicio<E> pos) Esborra la posició següent a pos i en retorna el valor emmagatzemat. @pre pos és una posició vàlida de la llista que no és la darrera posició. @post L’enumeració seqüencial dels elements de la llista és l’enumeració dels elements de old(this) excloent la posició següent a pos. E reemplacar(Posicio<E> pos,E elem) Reemplaça l’element emmagatzemat a pos per elem i retorna l’antic. @pre pos és una posició vàlida de la llista. @post L’enumeració seqüencial dels elements de la llista és l’enumeració dels elements d’old(this) reemplaçant l’element emmagatzemat a pos per elem. void intercanviar(Posicio<E> pos1,Posicio<E> pos2) Intercanvia els elements emmagatzemats a pos1 i pos2. @pre pos1 i pos2 són posicions vàlides de la llista. @post L’enumeració seqüencial dels elements de la llista és l’enumeració dels elements de old(this) intercanviant els elements emmagatzemats a pos1 i pos2. Recorregut<E> posicions() Retorna un recorregut de les posicions de la llista. El node actual inicial és el primer node de la llista (en cas que la llista no estigui buida). @pre Cert. @post El recorregut retornat és tal que l’enumeració seqüencial dels elements retornats per crides successives al mètode següent és igual a l’enumeració seqüencial dels elements de la llista.

Contenidors seqüencials

Redundància en les operacions del TAD Observeu que hi ha una certa redundància en les operacions d’aquest TAD: les quatre operacions d’afegir no són totes imprescindibles per a crear una llista. En el disseny d’aquest TAD s’ha donat prioritat, sobretot, a la claredat en la interpretació del que fan les operacions i la seva facilitat d’ús.


© FUOC • P06/05001/00577 • Mòdul 3

40

Contenidors seqüencials

En diverses de les operacions presentades en l’especificació anterior, la precondició conté la condició que els paràmetres que fan referència a posicions facin referència a posicions vàlides de la llista. Què vol dir això? Simplement,

El sistema de DBC de l’assignatura també s’encarrega de comprovar les posicions vàlides.

que es tracta d’alguna o algunes de les posicions que en aquell moment conté la llista, i que en resulten descartades tant les posicions que no són de la llista, com aquelles que en algun moment ho han estat, però ja no en formen part.

4.4. Implementació del TAD Llista

La representació més natural per a implementar el TAD Llista s’obté utilitzant una representació encadenada. En un primer moment, se’ns podria acudir fer

Vegeu el TAD Cua definit en el subapartat 3.4 d’aquest mòdul didàctic.

servir la mateixa representació que hem fet servir per a la implementació encadenada del TAD Cua. Recordem que, en aquesta representació, els nodes tenen un únic encadenament cap al següent node.

Sempre que tenim una representació candidata, l’hem de validar imaginantnos com implementarem les operacions del TAD amb aquesta representació. Revisant les operacions del TAD Llista, comprovem els aspectes següents:

• L’operació afegirAbansDe(Posicio<E>,E) necessita accedir a la posició anterior des d’una posició concreta.

• L’operació esborrar(Posicio<E>) necessita encadenar l’element anterior d’una posició concreta amb la següent (per tal de, tot traient la posició de la llista, mantenir la continuïtat de la llista).

És a dir, el conjunt d’operacions que hem definit per al TAD Llista requereix que, a partir d’una posició, puguem accedir tant a la posició anterior com a la següent. Una estructura simplement enllaçada com la del subapartat 3.4 permet accés en temps constant a la posició següent. No disposem, en canvi, de cap encadenament al node anterior; de manera que, per a accedir-hi, seria necessari, en primer lloc, anar al principi de la llista i recórrer-la tota (usant els encadenaments cap al següent dels nodes) fins a trobar el node que tingui la nostra posició com a següent. Això té un cost lineal respecte de la mida de la llista. Com a alternativa podem fer servir una representació encadenada més completa que disposi també per a cada node d’un encadenament al node anterior, a part de l’encadenament al següent. Això ens permetrà accedir al node anterior únicament seguint aquest encadenament i, per tant, en temps constant. Aquesta estructura s’anomena comunament llista doblement enllaçada. En la figura 17, es mostra com s’accedeix al node anterior amb llistes simplement enllaçades i amb llistes doblement enllaçades.

Versió afitada del TAD Llista És possible implementar una variant afitada del TAD Llista fent servir una representació amb vector, de manera que els elements del vector simulin nodes encadenats. Per a més informació, podeu consultar la bibliografia recomanada. Podeu revisar, per exemple, el capítol 7 de l’obra de Sahni (2000).


© FUOC • P06/05001/00577 • Mòdul 3

41

Contenidors seqüencials

Figura 17

4.4.1. Definició de la representació Com tota representació encadenada, hi haurà dues parts ben diferents, que són la representació del node i la representació de la col·lecció: • La representació del node consistirà en l’element guardat en el node o una referència a l’element guardat en el node, i dos encadenaments –un cap al node següent i un altre cap a l’anterior. • La representació de la col·lecció consistirà en dos atributs, que són el nombre d’elements guardats a la llista, i un encadenament al darrer element d’aquesta. Recordem que en la representació encadenada de Cua, la representació de la col·lecció tenia dos encadenaments: un al primer element i un altre al darrer. Aquí també necessitem accés ràpid al primer i al darrer element. Llavors, com podem garantir accessos en temps constant al primer i darrer element amb un únic encadenament al darrer? Ho podem fer usant una representació circular. En la representació circular, reaprofitem l’encadenament cap al següent del darrer node (que normalment hauria de ser ‘null’) per apuntar al primer node. Així doncs, tal com es veu en la figura 18, per a accedir al primer node únicament cal accedir al següent del darrer.

Recordeu que en el subapartat 3.2.1 ja hem fet servir una representació circular per a implementar el TAD Cua fent servir un vector.


© FUOC • P06/05001/00577 • Mòdul 3

42

Contenidors seqüencials

Figura 18

4.4.2. Implementació de les operacions L’algorísmia necessària per a implementar les operacions de Llista amb la representació que acabem de veure és semblant a la que ja hem vist per a la representació encadenada de Cua, però afegeix la necessitat de gestionar els encadenaments quan afegim o esborrem un node d’una posició intermèdia de la llista. Vegem la descripció de la implementació de les operacions més representatives: col·lecció LlistaDoblementEncadenada<E> implementa Llista<E> constructor() O(1) Posa darrer a ‘null’. Posa el nombre d’elements a 0. Posicio<E> afegirDesprésDe(Posicio<E> pos,E elem) O(1) Convertim pos en un NodeDoblementEncadenat(casting) Si el node és ‘null’ Creem node auxiliar amb elem i ‘null’ com a següent i anterior. Posem com a següent i anterior el mateix node auxiliar (representació circular). Assignem a darrer el node creat. Si no Creem node auxiliar amb elem, anterior = node (pos) i Següent = següent de node (pos). Fem que el següent de node (pos) sigui el node auxiliar creat. Fem que l’anterior del següent de l’auxiliar sigui el node auxiliar creat. fsi si node (pos) és el darrer Assignem a darrer el node auxiliar creat. Incrementem el nombre d’elements de la llista. Retornem el node creat. E esborrar(Posicio<E> pos) O(1) Convertim pos en NodeDoblementEncadenat(casting)

L’operació posicions es comenta en l’apartat següent. La resta d’operacions són semblants a les descrites, i en podeu trobar la implementació en el codi font Java disponible com a recurs electrònic.


© FUOC • P06/05001/00577 • Mòdul 3

43

Si el nombre d’elements és 1 Assignem darrer a ‘null’. Si no Sigui ant el node anterior al node corresponent a pos, i seg el següent. Assignem seg a l’encadenament següent d’ant. Assignem ant a l’encadenament anterior de seg. Assignem ‘null’ als encadenaments anterior i següent del node (pos). Si el node esborrat és el darrer Assignem ant al darrer. fsi fsi incrementem el nombre d’elements de la llista. retornem l’element guardat en el node esborrat. E reemplacar(Posicio<E> pos,E elem) O(1) Convertim pos en NodeDoblementEncadenat (casting). Posem elem com a element guardat al node. En la figura 19 teniu una representació gràfica del procés realitzat per a afegir un nou element en una posició determinada de la llista. En primer lloc, es crea el node que guardarà l’element i s’inicialitza; en segon lloc, s’integra a la llista. Figura 19

Contenidors seqüencials


© FUOC • P06/05001/00577 • Mòdul 3

44

Contenidors seqüencials

En la figura 20, podeu veure el procés contrari corresponent a l’esborrament d’una posició determinada de la llista. Figura 20

La implementació amb llistes doblement enllaçades permet que el cost temporal de totes les operacions del TAD Llista sigui constant (O(1)). Ara bé, és clar que això té el sobrecost espacial d’haver de mantenir dos encadenaments per node. En el cas que les llistes siguin llargues o que tinguem un nombre de llistes realment gran, aquest sobrecost espacial pot ser rellevant.

Recordeu En el cas de l’esborrament, un cop el node ja no es referencia des de cap altre node ni variable, pot ser recollit pel recol·lector d’escombraries.

D’altra banda, no sempre necessitarem les operacions afegirAbansDe i esborrar, que són les que necessiten l’encadenament a l’anterior per a mantenir el seu cost constant. En cas que aquestes dues operacions no siguin necessàries i el sobrecost espacial sigui rellevant, podem fer servir una implementació encadenada basada en una llista simplement enllaçada. No detallarem aquí aquesta implementació, ja que és una simplificació de la ja comentada, però sí que la trobareu a la classe LlistaEncadenada de la biblioteca de TAD per si necessiteu fer-la servir. Les operacions afegirAbansDe i esborrar es proporcionen igualment, però en aquesta implementació tenen un cost lineal.

La mateixa biblioteca fa ús de LlistaEncadenada en diverses ocasions per a implementar altres TAD.


© FUOC • P06/05001/00577 • Mòdul 3

45

Contenidors seqüencials

Tingueu en compte, en revisar les diferents implementacions en el codi font (cosa molt recomanable!), que la descripció algorísmica que s’ha fet amb anterioritat de la implementació de les operacions pot estar repartida en el codi font en diversos mètodes, amb l’objectiu de minimitzar el codi redundant i incrementar-ne la reusabilitat. Addicionalment, no cal dir que s’ha fet ús de l’orientació a objectes, amb la qual cosa la classe LlistaDoblementEncadenada s’ha definit com a subclasse de LlistaEncadenada, i reutilitza bona part del comportament definit en aquesta classe. Els següents blocs de codi mostren tot el codi corresponent a l’algorisme de l’operació afegirDespresDe de LlistaDoblementEncadenada. Hi ha dos blocs de codi diferents: un per al comportament definit en LlistaEncadenada i un altre per a l’especialització proporcionada en LlistaDoblementEncadenada. LlistaEncadenada.java package uoc.ei.tads; public class LlistaEncadenada<E> implements Llista<E> { ... public Posicio<E> afegirAlFinal(E elem) { darrer = novaPosicio(darrer, elem); return darrer; } public Posicio<E> afegirDespresDe(Posicio<E> node,E elem) { Posicio<E> nouNode; if (darrer==node) nouNode = afegirAlFinal(elem); else nouNode = novaPosicio((NodeEncadenat<E>)node,elem); return nouNode; } protected NodeEncadenat<E> novaPosicio(NodeEncadenat<E> node, E elem) { ... } ... }

LlistaDoblementEncadenada.java package uoc.ei.tads; public class LlistaDoblementEncadenada<E> extends LlistaEncadenada<E> {


© FUOC • P06/05001/00577 • Mòdul 3

46

Contenidors seqüencials

... protected NodeEncadenat<E> novaPosicio(NodeEncadenat<E> node, E elem) { NodeDoblementEncadenat<E> nouNode = null; if (node == null) { nouNode = new NodeDoblementEncadenat<E>(elem); nouNode.setSeguent(nouNode); nouNode.setAnterior(nouNode); darrer = nouNode; } else { NodeDoblementEncadenat<E> actual = (NodeDoblementEncadenat<E>)node; NodeDoblementEncadenat<E> seguent = (NodeDoblementEncadenat<E>)actual.getSeguent();

nouNode = new NodeDoblementEncadenat<E>(seguent,elem,(NodeDoblementEncadenat<E>)node); node.setSeguent(nouNode); seguent.setAnterior(nouNode); } n++; return

nouNode;

} }

L’operació afegirDespresDe es defineix a la classe LlistaEncadenada; però tant la creació del node com la gestió dels encadenaments es fa en el mètode auxiliar novaPosicio, que està redefinit a LlistaDoblementEncadenada.

4.5. Recorregut dels elements d’un contenidor: TAD Iterador Una de les construccions més usades en la programació estructurada és el recorregut de seqüències d’elements. D’altres assignatures ja coneixem dos algo-

En l’assignatura Fonaments de programació, s’estudien els esquemes de recorregut i de cerca.

rismes bàsics per al tractament seqüencial: l’esquema de recorregut i el de cerca. En aquesta assignatura estudiem principalment dues coses: d’una banda diferents formes de representar col·leccions d’elements en forma de TAD i, de l’altra, com usar els TAD resultants sense haver de preocupar-se per com han estat implementats. Una de les operacions que voldrem fer més habitualment sobre una col·lecció serà recórrer els seus elements. El TAD Recorregut ens permet recórrer les posicions d’una col·lecció posicional com és Llista (i a partir de les posicions podem accedir als elements que hi estan emmagatzemats). Però no totes les col·leccions són posicionals (per exemple, Pila i Cua no ho són); i molts cops ens convindrà també recórrer els elements d’aquest tipus de col·leccions. D’al-

TAD posicional Un TAD posicional és aquell que ofereix la possibilitat de realitzar operacions a partir d’un paràmetre que indica la posició.


© FUOC • P06/05001/00577 • Mòdul 3

47

Contenidors seqüencials

tra banda, molts cops estarem interessats a accedir únicament als elements (en lloc d’accedir a la posició).

Per totes aquestes qüestions, resulta força útil que les biblioteques de col·leccions com la de l’assignatura proporcionin alguna forma de recórrer els elements d’una col·lecció. Una forma força habitual i flexible és mitjançant una abstracció independent de la col·lecció equivalent al TAD Recorregut, però que únicament ofereixi accés als elements i “amagui” qualsevol informació posicional.

El TAD Iterador ens permetrà recórrer qualsevol col·lecció d’una manera totalment homogènia; sense haver de saber quin tipus de col·lecció és. Evidentment, el coneixement sobre el tipus de col·lecció estarà contingut en cada implementació d’Iterador; i en disposarem d’una per cada tipus de col·lecció.

tipus Iterador<E> és boolean hiHaSeguent() @pre Cert @post Retorna ‘cert’ si hi ha següent element, i ‘fals’, en cas contrari. E seguent() Retorna el següent element del recorregut. El primer element del recorregut ens el retorna la primera crida a aquesta operació. @pre hiHaSeguent() @post Retorna el següent element del recorregut.

Podem crear un iterador per recórrer una col·lecció a partir del mètode elements() definit en el TAD abstracte Contenidor i implementat per cada una de les diferents implementacions de la col·lecció. Aquesta operació, tot i que no hi havíem fet quasi referència fins ara, està disponible per a totes les col·leccions de la biblioteca de TAD de l’assignatura.

S’ha de tenir en compte que pot ser força perillós modificar una col·lecció mentre s’està recorrent amb un iterador. Concretament, esborrar la posició actual d’un recorregut provocarà molt probablement una situació incoherent entre l’iterador amb què es fa el recorregut i la col·lecció.

Una forma d’evitar aquests problemes és fer que els iteradors recorrin els elements d’una col·lecció en un moment donat; és a dir, fer una “foto” de la col·lecció. Aquesta “foto” es pren normalment en el moment de creació de l’iterador, de manera que quedi desvinculat de la representació de la col·lecció i, així, aquesta es pugui modificar sense problemes. Fer aquesta “foto”, evidentment, consumeix recursos tant temporals com espacials.

Recordeu que la classe Contenidor és l’arrel de la jerarquia de col·leccions de la biblioteca de col·leccions de l’assignatura.


© FUOC • P06/05001/00577 • Mòdul 3

48

Contenidors seqüencials

Una altra possibilitat per a evitar el problema és sincronitzar una col·lecció amb els seus iteradors, de manera que la col·lecció els “notifiqui” les modificacions que s’hi van produint. Aquesta solució afegeix complexitat al disseny de la biblioteca, i a més, pot suposar un cost temporal important si el nombre d’iteradors d’una col·lecció és gran. Les dues solucions anteriors suposen un cost afegit que no necessitarem en la majoria de casos. Per això, gran part de les biblioteques de col·leccions (i entre aquestes, la biblioteca de l’assignatura) deixen la responsabilitat de no fer modificacions en una col·lecció quan aquesta s’està iterant als usuaris de la biblioteca. En cas que calgui fer modificacions en la col·lecció mentre s’està recorrent, l’orientació a objectes ofereix a l’usuari de la biblioteca eines per a implementar fà-

En el mòdul “Disseny d’estructura de dades” s’aprofundeix en el disseny de les biblioteques de col·leccions i se’n tracta l’extensibilitat, entre altres aspectes.

cilment algun dels mecanismes ja comentats. Això serà possible, però, sempre que la biblioteca s’hagi dissenyat de manera que es pugui estendre fàcilment.

4.5.1. Implementació Vegem a continuació un exemple d’implementació del TAD Iterador per a una col·lecció, una LlistaEncadenada. La implementació està feta sobre una classe anomenada LlistaEncadenadaAmbIterador que és subclasse de LlistaEncadenada i que trobareu com a recurs electrònic en els exemples del mòdul. Si ho fem així, és perquè no volem modificar la classe LlistaEncadenada de la biblioteca de TAD de l’assignatura, que usa una implementació d’Iterador força genèrica basada en el TAD Recorregut (i que és reutilitzada també per altres col·leccions). Com és el primer contacte amb el concepte d’iterador, s’ha preferit partir d’una versió totalment didàctica. A continuació, comentem alguns aspectes d’aquesta implementació: • En tractar-se d’una implementació d’un TAD auxiliar, i com que la idea és obtenir una instància sempre a partir d’un objecte de tipus col·lecció (mètode elements()), hem “amagat” la classe que implementa Iterador. Hem fet això definint IteradorLlista com una classe interna (inner class) protegida dins mateix de LlistaEncadenadaAmbIterador. • Quan es defineix el mètode elements() de la col·lecció, es crea una instància d’IteradorLlista. Aquesta instància s’ha creat utilitzant el constructor d’aquest (visible dins la implementació de la col·lecció). • El mètode toString de la col·lecció s’ha redefinit com a exemple d’ús del TAD Iterador. • La implementació de l’iterador té un estat definit per l’atribut següent, que ens indica l’element següent que cal visitar. Com que LlistaEncadenada fa servir una representació circular, l’iterador necessita conèixer també el darrer element, de manera que quan es “visiti” el darrer element sabem que

Classe interna en Java Una classe interna (o inner class) és una classe que està definida dins una altra classe i n’és membre, igual que els mètodes o atributs que la darrera tingui definits. Les regles de visibilitat de la classe interna són les mateixes que per als altres membres. D’aquesta manera, si en una classe A definim una classe interna protegida B, aquesta només estarà disponible dins de A i de les seves subclasses.


© FUOC • P06/05001/00577 • Mòdul 3

49

no n’hem de “visitar” cap més (quan l’hem visitat posem següent a ‘null’ de manera que hiHaSeguent sàpiga que ja s’han visitat tots els elements). LlistaEncadenadaAmbIterador.java

package uoc.ei.exemples.modul3.llista; import ... public class LlistaEncadenadaAmbIterador<E> extends LlistaEncadenada<E> { public Iterador<E> elements() { return new IteradorLlista<E>(this); } public String toString() { StringBuffer sb = new StringBuffer(); Iterador<E> iter = elements(); while (iter.hiHaSeguent()) { sb.append(iter.seguent()); if (iter.hiHaSeguent()) sb.append(", "); } return sb.toString(); } protected static class IteradorLlista<EI> implements Iterador<EI> { private NodeEncadenat<EI> darrer; private NodeEncadenat<EI> seguent; IteradorLlista(LlistaEncadenadaAmbIterador<EI> ll) { this.darrer = ll.darrer; if (darrer! = null) seguent = darrer.getSeguent(); } public boolean hiHaSeguent() { return seguent!= null; } public EI seguent() throws ExcepcioPosicioInvalida { NodeEncadenat<EI> aux = seguent; seguent = seguent==darrer ? null : seguent.getSeguent(); return aux.getElem(); } } }

Contenidors seqüencials


50

© FUOC • P06/05001/00577 • Mòdul 3

Contenidors seqüencials

4.6. Exemple d’ús del TAD Llista Els contenidors seqüencials són molt versàtils i es poden utilitzar en multitud de situacions diferents en la programació d’aplicacions. Principalment –sobretot en el cas de la llista–, s’utilitza per a ajudar a implementar una col·lecció de nivell d’abstracció més alt. A continuació, veurem un exemple i comprovarem com la reutilització pot fer que la implementació d’una col·lecció doni lloc, en alguns casos, a una quantitat força reduïda de codi. Ja hem fet servir el TAD Conjunt com a exemple per a introduir el concepte de TAD, i el format que usaríem al llarg d’aquest text per a definir-los. Abans n’hem donat una implementació basada en una representació amb vector. Ara, ja disposem de representacions encadenades. Per tant, podem donar-ne una implementació basada en aquest tipus de representacions. Però, de fet, podem anar una mica més enllà: no és necessari tornar a definir un nou tipus de node, ni tornar a programar el codi que ens gestioni els encadenaments quan haguem d’inserir o esborrar nodes. Podem reutilitzar una de les implementacions del TAD Llista. Així doncs, vegem com podem implementar el mateix TAD Conjunt amb les operacions afegir, hiEs i esborrar, utilitzant el TAD Llista. El TAD Llista ens servirà per a emmagatzemar els elements del conjunt, de manera que únicament haurem d’implementar les operacions de Conjunt en funció de les operacions de Llista. En cap cas, cal detallar com es representa la llista; aquesta és una feina que ja s’ha fet en les diferents implementacions del

a

TAD Llista de què ja disposem.

Una cosa important que hem de decidir és quina implementació del TAD Llista usarem. Per a això, cal estudiar si necessitem usar les operacions afegirAbansDe o esborrar de Llista o bé podem implementar les operacions de Conjunt sense usar-les.

Si cal fer servir alguna d’aquestes dues operacions de Llista, serà millor utilitzarem LlistaDoblementEncadenada, ja que ofereix temps constant per a aquestes operacions. Si no és necessari fer-les servir, serà millor LlistaEncadenada, ja que ofereix temps constant en la resta d’operacions i només té un encadenament per node. Vegem com podem traduir les operacions de Conjunt en operacions de Llista: • afegir(E elem). En cas que elem no hi sigui (crida a hiEs), afegim elem al final de la llista. • hiEs(E elem). Creem un iterador dels elements de la llista (crida a l’operació elements de Llista). Fem una cerca d’elem. Si l’hem trobat retornem ‘cert’ i, si no, ‘fals’.

Vegeu el TAD Conjunt definit en el mòdul “Tipus abstractes de dades” d’aquesta assignatura.


© FUOC • P06/05001/00577 • Mòdul 3

51

• esborrar(E elem). Creem un recorregut (Llista.posicions) de les posicions de la llista. Cerquem una posició que emmagatzemi l’element. Si la trobem, l’esborrem mitjançant Llista.esborra. La implementació que acabem de descriure fa servir l’operació esborra de Llista. Però amb una implementació una mica més intel·ligent de l’operació esborra de Conjunt ho podem evitar: únicament cal tenir la precaució, mentre es cerca la posició, d’anar guardant la posició anterior. Si la cerca acaba amb èxit, usarem Llista.esborrarSeguent sobre la posició anterior en lloc de Llista.esborrar sobre la posició trobada. En el següent bloc de codi, podeu comprovar com la implementació queda molt més reduïda. ConjuntLlistaImpl.java

package uoc.ei.exemples.modul3.llista; import ... public class ConjuntLlistaImpl<E> implements Conjunt<E> { private Llista<E> llistaDeElements; public ConjuntLlistaImpl() { llistaDeElements = new LlistaEncadenada<E>(); } public void afegir(E elem) { if (!hiEs(elem)) llistaDeElements.afegirAlFinal(elem); } public boolean hiEs(E elem) { boolean trobat = false; Iterador<E> iter = llistaDeElements.elements(); while (!trobat && iter.hiHaSeguent()) trobat = elem.equals(iter.seguent()); return trobat; } public E esborrar(E elem) { E elementEsborrat = null; boolean trobat = false; Recorregut<E> rec = llistaDeElements.posicions(); Posicio<E> anterior = null,actual = null; while (!trobat && rec.hiHaSeguent()) { anterior = actual; actual = rec.seguent(); trobat = actual!= null && elem.equals(actual.getElem()); }

Contenidors seqüencials


© FUOC • P06/05001/00577 • Mòdul 3

52

Contenidors seqüencials

if (trobat) elementEsborrat = llistaDeElements.esborrarSeguent(anterior); return elementEsborrat; } }

Com podeu comprovar, els algorismes d’aquesta implementació són senzills i clars. Es basen principalment en l’ús de les operacions proporcionades pel TAD Llista i els TAD auxiliars Posicio, Recorregut i Iterador, combinades amb els esquemes algorísmics de recorregut i cerca que ja coneixeu. L’ús de biblioteques de col·leccions com la de l’assignatura (i de fet, de biblioteques en general) potencia la capacitat de reutilització i incrementa la productivitat i la fiabilitat, en evitar que els desenvolupadors hagin de reinventar cada cop representacions semblants i hagin de codificar cada vegada la gestió de tots els elements que hi intervenen.

Vegeu els esquemes algorísmics de recorregut i cerca en l’assignatura Fonaments de programació.


© FUOC • P06/05001/00577 • Mòdul 3

53

Contenidors seqüencials

5. Representacions amb vector: redimensionament

Un inconvenient important de les representacions amb vector és que cal donar una mida del vector de bon principi. Això té dues conseqüències poc desitjables: d’una banda, necessitem conèixer el màxim nombre d’elements que es guardaran en la col·lecció; i de l’altra, si el nombre d’elements que hi ha a la col·lecció és lluny d’aquest màxim, estarem malbaratant força espai.

Existeix una estratègia que, tot i que treballa amb representacions amb vector, ens permet anar adaptant el nombre d’elements guardats a la col·lecció. Consisteix a crear un vector més gran quan el vector estigui ple (i s’hi vulgui inserir un

No tots els llenguatges de programació permeten aquesta solució. Java la permet.

nou element), i traspassar-hi tots els elements, per a finalment descartar el vector antic i continuar treballant amb el nou. El redimensionament es realitza de manera totalment transparent per a l’usuari de la col·lecció. Les representacions que usen aquesta tècnica s’anomenen vectors extensibles (extendable array). En la figura 21, podem veure de manera esquemàtica els tres passos necessaris per al redimensionament del vector.

Figura 21

Ara bé, el redimensionament del vector té un cost lineal sobre el nombre d’elements de la col·lecció, cosa que pot tenir un efecte sobre el cost de les operacions del TAD. Per exemple, podem aplicar la tècnica al TAD Cua, proporcionant una implementació alternativa a CuaVectorImpl que faci servir un vector extensible. Podem partir de la implementació de CuaVectorImpl i modificar-la lleugerament.

En primer lloc, cal decidir on fer el redimensionament. En principi, només caldrà fer el vector més gran quan afegim elements a la col·lecció, cosa que en

Vegeu la implementació de CuaVectorImpl en l’apartat 3 d’aquest mòdul didàctic.


54

© FUOC • P06/05001/00577 • Mòdul 3

Contenidors seqüencials

aquest cas es fa en l’operació encuar. A la implementació d’encuar caldrà afegirhi una sentència condicional que redimensioni el vector si està ple. Però el cost de l’operació d’encuar era constant; i ara, amb el redimensionament, passa a ser lineal. Ara bé, l’operació de redimensionament no s’executarà cada cop que encuem un element. L’objectiu és que quan el vector es redimensioni, es faci de tal forma que s’hi pugui encuar un nombre raonable d’elements sense necessitat de tornar-lo a redimensionar. Llavors, la majoria de cops que s’executi l’operació d’encuar es farà amb cost constant; mentre que, de tant en tant, es farà amb cost lineal. Mentim si diem que encuar té cost constant (el seu cost ha passat a ser lineal). Però si diem simplement que el seu cost és lineal, tot i dir la veritat, donarem una imatge equivocada de la nostra implementació als usuaris potencials.

Per això existeix la noció de cost amortitzat / amortització de cost, que ens pot ser útil per a expressar millor el cost d’una operació sempre que ens trobem amb aquesta situació en què moltes execucions tenen un cost més baix, i algunes d’aïllades tenen un cost superior.

L’amortització del cost consisteix a repartir la suma dels costos d’un conjunt d’execucions entre totes aquestes execucions. D’aquesta manera, en l’operació d’encuar aconseguiríem repartir el cost lineal del redimensionament entre les altres execucions (de cost constant). Si després de realitzar aquest repartiment, el cost resultant de les execucions d’encuar es manté constant, podrem afirmar que encuar té un cost amortitzat constant (O(1)). Però abans de fer tal afirma-

L’amortització del cost El concepte d’amortització del cost és similar al d’amortització de capital, en què repartim el capital gastat en una compra entre un conjunt d’anys durant els quals usem l’objecte comprat.

ció, caldrà assegurar-se matemàticament que el cost es manté constant. Vegem de quina manera, per al cas concret d’encuar. El cost d’una operació de redimensionament és lineal; és a dir, O(n). I això vol dir que, en una operació de redimensionament, es fan k × n operacions simples (per a alguna constant k). El cost d’una operació d’encuar sense redimensionament és O(1), per tant, es tracta de k’ operacions simples (per a alguna constant k’). Per a estar completament segurs que podem repartir les k × n operacions del redimensionament entre els elements encuats amb cost constant i mantenir aquest cost constant, és necessari que tinguem com a mínim n operacions d’encuar sense redimensionar per cada una amb redimensionament. D’aquesta manera, tal com s’aprecia en la figura 22, podem acumular en cada operació d’encuar amb cost constant (k’ operacions bàsiques) una altra constant (k operacions bàsiques), amb la qual cosa el seu cost serà k + k’. Totes dues són constants independents de la mida de les dades. Per tant, seguim tenint cost constant. És a dir, hem assimilat un redimensionament amb cost lineal amb n operacions d’encuar de cost constant.

a

Equivalència Dir que l’operació d’encuar té un cost amortitzat O(1) és equivalent a dir que executar n cops l’operació té un cost O(n).


© FUOC • P06/05001/00577 • Mòdul 3

55

Figura 22

Per a aconseguir que hi hagi n operacions d’encuar sense redimensionament per cada una amb redimensionament, cal que el redimensionament dupliqui com a mínim la mida del vector. Amb això, quedaran n posicions del nou vector lliures, que garantiran un mínim de n elements encuats addicionals sense redimensionament. Per tant, podrem dir que el cost amortitzat d’encuar és constant en una implementació amb vector extensible sempre que cada redimensionament dupliqui

a

la mida del vector. Activitat

Raoneu per què, en realitat, n’hi ha prou de garantir que el nombre de posicions lliures després del redimensionament sigui proporcional a n.

A continuació teniu un fragment de la implementació de Cua que usa un vector extensible en què es mostra el mètode encuar modificat i el mètode redimensionar: CuaRedimensionableImpl.java

package uoc.ei.exemples.modul3.redimensionament; import ... public class CuaRedimensionableImpl<E> implements Cua<E> { ... public void encuar(E elem) { if (esPle()) redimensionar(); int darrer = posicio(primer + n); elements[darrer] = elem; n++; }

Contenidors seqüencials


© FUOC • P06/05001/00577 • Mòdul 3

56

private void redimensionar() { // creació del nou vector // (amb el doble de capacitat) E[] auxElements = (E[])new Object[elements.length*2]; // còpia dels elements d'un a l'altre Iterador<E> it = elements(); int i = 0; while (it.hiHaSeguent()) { auxElements[i] = it.seguent(); i++; } // substitució del vector antic ple // pel nou amb més capacitat primer = 0; elements = auxElements; } }

Contenidors seqüencials


© FUOC • P06/05001/00577 • Mòdul 3

57

Contenidors seqüencials

6. Els contenidors seqüencials a la Java Collections Framework

Tot i que la nostra assignatura disposa de la seva pròpia biblioteca de TAD, hi ha força més biblioteques, cadascuna amb les seves peculiaritats. Una de les més usades és la Java Collections Framework (JCF), que forma part del JDK des de la versió 1.2 (aquest és el motiu de que sigui tan popular). Tot i no tractarse, principalment per motius didàctics, de la biblioteca que utilitzarem en l’assignatura, és interessant comentar-la a nivell d’ús. D’una banda, serà un recurs important per a tots aquells que continueu desenvolupant aplicacions en llenguatge Java. I de l’altra, teniu l’oportunitat de veure una altra biblioteca de col·leccions, amb un altre disseny. Per aquest motiu, al final de cada un dels mòduls en què introduïm nous tipus de col·lecció, farem un comentari sobre les classes i interfícies equivalents a la

Comentari sobre la JCF Els comentaris d’aquest apartat acostumaran a ser breus i a donar una idea general de les classes de la JCF. En cap cas intentaran ser una guia d’ajuda a la programació amb la JCF. Per a això, podeu recórrer a la documentació Javadoc del JDK.

JCF. En aquest mòdul, començarem amb els contenidors seqüencials. La JCF fa ús de la dualitat interfície/classe ja comentada. Així que parlarem, en primer lloc, de les interfícies relacionades amb les col·leccions seqüencials i, després, de les implementacions ofertes. Totes les interfícies que representen col·leccions estenen una interfície arrel anomenada Collection, que proporciona un conjunt de mètodes moderadament extens que permeten: • Afegir un element aïllat o un conjunt d’elements a la col·lecció. • Esborrar un element concret, un conjunt d’ells, o tots els de la col·lecció. • Obtenir un iterador per a recórrer tots els elements de la col·lecció. • Consultar si hi ha un element o un conjunt d’elements. • Consultar si la col·lecció és buida. • Saber el nombre d’elements de la col·lecció. • Copiar tots els elements de la col·lecció en un vector (Object[]). Algunes d’aquestes operacions són opcionals (consulteu el Javadoc). Això vol dir que una implementació concreta per a un tipus de col·lecció pot implementar l’operació o no. En cas que l’operació no estigui implementada, s’acostuma a llançar una excepció de tipus OperationNotSupportedException. La interfície Collection de la JCF ofereix moltes més operacions que la interfície Contenidor de la nostra biblioteca. Això serà una constant per a totes les interfícies comentades en aquest apartat al llarg dels diferents mòduls. Una de les decisions de disseny més apreciables en la JCF ha estat la d’oferir força mètodes si es consideraven útils (enfront de la compacitat i simplicitat de la biblioteca de TAD de l’assignatura, on hem intentat compaginar la usabilitat amb la didàctica).

La dualitat interfície/classe de la JCF es comenta en el mòdul “Tipus abstractes de dades” d’aquesta assignatura.


© FUOC • P06/05001/00577 • Mòdul 3

58

Contenidors seqüencials

Una interfície bàsica en la JCF és Iterator, equivalent al nostre Iterador. Aquesta interfície, a part dels dos mètodes necessaris per a fer el recorregut d’elements (que aquí es diuen hasNext i next), proporciona una operació opcional (no totes les col·leccions la suporten) que permet esborrar de la col·lecció l’element actual. La interfície bàsica per a treballar amb contenidors seqüencials és List. A part de les operacions ja comentades per a Collection, proporciona operacions que permeten mapar elements en posicions indexades per un enter (com si es tractés d’un vector). Podem obtenir l’element emmagatzemat en una posició de-

Interfícies en la JCF No mencionem interfícies per als TAD Pila i Cua, ja que la JCF no en té cap. Ofereix únicament la interfície List.

terminada, o la primera o la darrera posició en què s’emmagatzema un element. També podem obtenir una subllista a partir d’un interval de dues posicions. Val la pena remarcar que, en totes aquestes operacions, les posicions s’especifiquen amb enters, i no a partir d’un TAD auxiliar Posicio com en la biblioteca de l’assignatura. A part d’obtenir un Iterator per a recórrer els elements de la llista, List ofereix la possibilitat d’obtenir un ListIterator. Aquesta interfície és una extensió d’Iterator especial per a les llistes que permet treballar d’una manera més posicional (tot i que sense treballar de manera explícita amb posicions). ListIterator permet fer recorreguts en les dues direccions (amb previous i next), i ofereix una sèrie d’operacions que permeten afegir un element abans de l’element actual, i esborrar aquest o modificar-lo. Totes les implementacions de col·leccions de la JCF tenen com a classe arrel AbstractCollection. De la mateixa forma, hi ha altres classes que comencen per Abstract (en l’àmbit d’aquest mòdul: AbstractList i AbstractSequentialList). Aquestes classes són abstractes i no implementen cap col·lecció concreta. El seu objectiu és implementar de manera general tot el comportament que puguin, evitant re-

Convencions El fet que aquestes classes abstractes comencin per la paraula Abstract és una convenció seguida pels dissenyadors de la JCF, però no pel món Java en general.

codificar tot aquest codi en les implementacions concretes. És a dir, senzillament, fer servir l’orientació a objectes (generalització + abstracció). Les implementacions de col·leccions seqüencials més usades proporcionades a la JCF són: • ArrayList. Es tracta d’una implementació de List basada en un vector extensible. • LinkedList. Implementació de List realitzada amb un llista doblement enllaçada. A part de les operacions de List, proporciona operacions per consultar, esborrar i afegir elements a qualsevol dels dos extrems de la llista. Això

Vegeu l’annex d’aquest mòdul.

permet fer servir una LinkedList com una cua, una pila o una doble cua. • Vector. Implementació d’un vector extensible. Classe mantinguda per raons històriques. A partir de la versió 1.2 del JDK es va modificar per a implementar List. A part dels mètodes d’aquesta interfície, en proporciona uns quants més, heretats dels seus inicis. • Stack. Esten Vector amb les 5 operacions del TAD Pila.

Compatibilitat del JDK El fet que el JDK tingui una història de diversos anys fa que es mantinguin classes i mètodes per raons de compatibilitat (si no, les aplicacions antigues deixarien de funcionar amb les noves versions de JDK).


© FUOC • P06/05001/00577 • Mòdul 3

59

Resum

En aquest mòdul hem presentat les col·leccions seqüencials bàsiques: Pila, Cua i Llista. Aquestes tres col·leccions organitzen els elements seqüencialment, però cadascuna permet accedir a ells d’una manera diferent. Aquest fet diferencia el seu comportament, i fa que cadascuna sigui aplicable a un conjunt de situacions també diferenciat. S’ha introduït el concepte de representació encadenada, amb tots els elements necessaris per a entendre-la i implementar-la en el llenguatge usat en aquest text. S’han presentat els elements bàsics per a la gestió de la memòria (apuntador/referència, allotjament i alliberament de memòria) i s’ha vist com estan representats en el llenguatge usat en aquest text. En relació amb tot això, s’ha fet una introducció del sistema que fa servir Java i altres llenguatges per a gestionar la memòria. Pel que respecta a les implementacions, s’han estudiat les implementacions amb vector clàssiques per a Pila i Cua. En la introducció de les representacions encadenades, s’ha fet servir com a exemple una implementació encadenada de Cua. Això també ens ha servit per a tenir un exemple de TAD amb dues implementacions (a part de l’exemple més de joguina dels naturals en el mòdul “Tipus abstractes de dades”). Posteriorment, s’ha fet servir una representació encadenada per a implementar el TAD Llista. Tant la presentació d’aquest TAD com la seva implementació han servit per a introduir elements bàsics en el model de col·lecció que farem servir en aquest curs i en la biblioteca de col·leccions de l’assignatura: els conceptes de posició, recorregut i iterador. En l’apartat 5 del mòdul, s’explica el redimensionament dinàmic de vectors, una tècnica possible en el llenguatge Java i que permet eliminar la mancança més important de les representacions amb vector: haver de conèixer d’entrada el nombre d’elements màxim a guardar en una col·lecció. Aquesta tècnica comporta la realització d’una operació de redimensionament que pot ser força costosa i malbarata el cost asimptòtic en el pitjor dels casos (O) de les operacions del TAD afectades. Això, però, no reflecteix la realitat que aquesta operació de redimensionament només es fa de tant en tant. En aquestes situacions, és força més útil parlar del cost amortitzat, que també s’explica en aquest apartat. Per acabar, es descriu breument el conjunt de classes que corresponen a col·leccions seqüencials de la Java Collections Framework, inclosa en el JDK.

Contenidors seqüencials



© FUOC • P06/05001/00577 • Mòdul 3

61

Exercicis d’autoavaluació 1. Proporcioneu una implementació amb vector de la col·lecció Pila semblant a la proporcionada en l’apartat 2, però amb la diferència que l’element més antic de la pila estigui en la posició N del vector (essent N el darrer element del vector). 2. Imagineu la següent situació: disposeu de dues bobines de 50 DVD cada una. Una d’elles la useu per a guardar pel·lícules que compreu en poc espai. L’altra la useu com a bobina auxiliar, de manera que quan busqueu un títol aneu agafant DVD de la primera bobina un a un; i els poseu a la segona bobina. Així, fins que trobeu el títol cercat. Desenvolupeu o contesteu els punts següents: a) Quin TAD dels vistos és el més adient per a representar cada una de les bobines de DVD? b) Podeu proporcionar una representació ad hoc per a aquest problema, de manera que minimitzem l’espai lliure malbaratat en una representació amb vector? En cas afirmatiu, definiu-la. c) Definiu un TAD DobleBobina i decidiu quines operacions són necessàries per a implementar l’algorisme de cerca d’un DVD descrit al principi de l’enunciat. Implementeu tant les operacions com l’algorisme esmentat. 3. Implementeu el TAD Pila fent servir una representació encadenada (d’una forma semblant a com es fa en el subapartat 3.4 amb el TAD Cua). Un cop fet això, proporcioneu una implementació equivalent fent servir la classe LlistaEncadenada de la biblioteca de TAD de l’assignatura. Compareu les dues implementacions. Comenteu quina preferiu i per què. 4. Pretenem representar expressions matemàtiques. Les expressions que volem representar consisteixen en dos operands que poden ser o bé constants reals o bé altres expressions, i un operador d’entre els quatre bàsics: +, –, × i ÷. Podeu assumir una mida màxima quant al nombre d’elements (operadors + operands) d’una expressió. a) Digueu quin TAD dels vistos en el mòdul és el més adient per a representar les expressions. b) Quin problema tindríeu si no poguéssiu assumir una mida màxima en el nombre d’elements de l’expressió? Com ho solucionaríeu? c) Quina és la manera més pràctica de representar les expressions mitjançant aquest TAD? d) Definiu un algorisme que, a partir d’una expressió emmagatzemada en una instància del TAD triat, l’avaluï i retorni el resultat. 5. Implementeu la col·lecció llista de manera que l’espai lliure sigui gestionat per la mateixa implementació. La implementació que proporcioneu pot ser afitada (implementeu la interfície ContenidorAfitat). Pista: podeu gestionar l’espai lliure com una pila de nodes. Un cop fet això, compareu (i mesureu) el temps emprat en les operacions de la vostra implementació de llista en front de la implementació proporcionada a la biblioteca de TAD de l’assignatura. Quina implementació és més eficient? Raoneu el motiu. 6. Una cadena de supermercats vol dissenyar un sistema que a partir d’un client que està a punt de pagar, decideixi de manera automàtica la cua en què s’ha de posar. Cada supermercat de la cadena disposa de una caixa ràpida (només per a clients amb 10 elements com a màxim) i N caixes en què els clients poden pagar qualsevol nombre d’elements. Se us demana que definiu i implementeu un TAD anomenat CuaSupermercat amb les operacions següents: • void clientEnEspera(int numClient). A partir d’un client identificat amb un enter, l’encua al sistema seleccionant la caixa en què el client s’ha d’esperar. • boolean hiHaClientDisponible(int numCaixa). Retorna cert si hi ha un client esperant ser atès a la caixa indicada. • int atendreClient(int numCaixa). Desencua un client per a una caixa determinada. S’executarà quan la caixera de la caixa en qüestió pugui atendre un nou client. 7. Contesteu a les preguntes següents: a) És el concepte de posició exclusiu de TAD implementats mitjançant representacions encadenades? b) Per què no és aplicable el concepte de posició als TAD Pila i Cua? c) A partir de les respostes anteriors, té sentit definir un TAD Vector que representi un vector d’elements i que faci servir posicions? d) En cas que hàgiu contestat afirmativament a l’apartat c, proposeu un mínim de dues operacions en què podria ser útil el concepte de posició. e) En cas que hàgiu contestat afirmativament a l’apartat c, proposeu una representació del TAD Vector en Java i proporcioneu una implementació del TAD Posicio ad hoc. 8. Resoleu els apartats següents: a) Implementeu un algorisme en Java que, a partir d’ una instància del TAD Llista<int>, n’ordeni els elements de més petit a més gran. Feu servir únicament recorreguts i les operacions posicionals del TAD. b) Quin cost té l’algorisme implementat?

Contenidors seqüencials


© FUOC • P06/05001/00577 • Mòdul 3

62

c) És possible proporcionar un algorisme més eficient sense fer servir cap estructura de dades addicional? d) Adapteu l’algorisme proporcionat perquè serveixi per a llistes en què els elements puguin ser qualsevol classe que implementi la interfície Comparable que proporciona el JDK. 9. Volem proporcionar una implementació del TAD Conjunt presentat en el mòdul “Tipus abstractes de dades” que faci servir com a representació una de les col·leccions seqüencials de la biblioteca de classes de l’assignatura. a) Quina col·lecció us sembla més adient i per què? b) Quin cost té cadascuna de les operacions? c) Definiu i implementeu una nova operació en el TAD Conjunt anomenada unió que rebi dos conjunts com a paràmetre, i retorni un Conjunt que sigui la seva unió sense modificar els dos conjunts d’entrada. Quin cost té aquesta operació? Hi ha alguna forma de millorar-lo? 10. Contesteu els apartats següents: a) Expliqueu amb les vostres paraules la diferència entre els TAD Recorregut i Iterador (si és que n’hi ha alguna). b) Proporcioneu una implementació del TAD Iterador per a la representació del TAD Vector que heu definit en l’apartat e de l’exercici 7. c) Proporcioneu una implementació del TAD Recorregut per a la mateixa representació del TAD Vector. Podeu usar la delegació per a reaprofitar la implementació d’Iterador proporcionada en l’apartat b? d) Seria possible utilitzar delegació en el sentit contrari (implementar Iterador basant-se en la implementació de Recorregut)? Pista: examineu les implementacions d’Iterador que es defineixen en la biblioteca de TAD de l’assignatura. 11. Una empresa disposa d’un nombre N de treballadors de manteniment. Aquests treballadors reben tasques a realitzar concretes (canviar llums, instal·lar endolls, obrir envans...). Un cop una tasca arriba al departament de manteniment, el cap del departament estima l’esforç en intervals de 15 minuts, i assigna la tasca a un treballador que no n’estigui realitzant cap. Si tots els treballadors estan ocupats, l’assigna a aquell treballador que, segons l’estimació de la tasca, quedarà lliure abans. a) Dissenyeu un TAD que automatitzi la feina del cap del departament de manteniment. Definiu-hi operacions de manera que el cap de manteniment pugui assignar automàticament les tasques que li arribin. (Per dissenyar un TAD, entenem únicament definir les seves operacions i el seu comportament mitjançant l’especificació.) b) Proposeu una representació per a implementar aquest TAD. Podeu reutilitzar alguns dels TAD estudiats en aquest mòdul? Si és així, quins i com? c) Proporcioneu una implementació del TAD basada en la representació que heu proposat. d) Afegiu i implementeu al TAD les operacions següents: • Iterador treballadorsOcupats(). Retorna un iterador que permet iterar sobre els treballadors que estan ocupats (realitzant una tasca). • Iterador treballadorsLliures(). Retorna un iterador que permet iterar sobre els treballadors que estan lliures. e) Té algun sentit proporcionar alguna operació que permetés treballar amb instàncies del TAD Recorregut? 12. En l’apartat d de l’exercici 11, heu definit operacions que retornen instàncies del TAD Iterador. És possible que un treballador comenci una tasca o bé acabi la tasca que està realitzant mentre esteu iterant sobre el conjunt de treballadors lliures o ocupats. Contesteu a les següents preguntes: a) Quina solució heu proporcionat a aquesta situació? b) Quines possibles solucions se us acudeixen? I quina us sembla millor en aquesta situació concreta? c) Implementeu variants d’Iterador per al TAD de l’exercici 11 per a almenys una de les solucions esmentades que no hàgiu triat en l’apartat d. 13. Examineu, a la documentació Javadoc del JDK, les classes System i Runtime i contesteu les preguntes següents: a) Enumereu els mètodes relacionats amb la gestió de memòria. b) És necessari realitzar alguna acció especial perquè el garbage collector entri en acció? c) Garanteix Java que quan un programa deixa d’usar un espai de memòria, aquest es recicli immediatament? I després d’un període de temps determinat? d) Quin creieu que és el motiu? 14. És possible implementar un sistema de gestió de memòria (garbage collection) alternatiu purament en Java? Per què?

Contenidors seqüencials


© FUOC • P06/05001/00577 • Mòdul 3

63

Solucionari Exercici 7 a) En absolut. El concepte de posició és aplicable a tots aquells TAD en què volem definir operacions en què ens interessi fer encabir, dins els paràmetres de l’operació, informació sobre la ubicació dels elements tractats. El concepte de posició està, doncs, relacionat amb el comportament del TAD i ha de ser tingut en compte en la definició de la seva signatura; però no té res a veure amb cap de les seves possibles implementacions. b) Els elements als quals accedim en una pila i una cua són exclusivament els dels dos extrems. No té cap utilitat, per tant, fer servir el concepte posició ja que estaríem limitats a usarlo únicament per a dues posicions concretes. En aquest cas, és molt més pràctic tenir operacions diferenciades per a accedir a cadascun dels dos extrems. c) Sí que pot tenir sentit. Tot depèn de les operacions que ens interessi definir i, en definitiva, de si ens interessa treballar posicionalment o no. El fet de treballar posicionalment és útil sobretot per a referenciar posicions d’una col·lecció des d’altres estructures. En el cas d’un vector, aquesta funció, però, també es pot aconseguir referenciant els índexs de les posicions del vector (una solució força més simple i, per tant, utilitzada). d) Per exemple: intercanviar(Posicio,Posicio), o modificarValor(Posicio).

Glossari apuntador m Tipus de dada utilitzat en força llenguatges de programació per a fer referència a un altre objecte o valor. El valor d’un apuntador és, en realitat, una adreça de memòria (en la qual hi ha guardat aquest altre objecte o valor). col·lecció posicional f Col·lecció en què els elements estan guardats en unes posicions que guarden una relació determinada entre elles. Els usuaris de la col·lecció tenen accés a aquestes posicions i poden gestionar la col·lecció fent-ne ús. estructura de dades encadenada f sin. estructura de dades recursiva estructura de dades recursiva f Estructura de dades que en la seva definició fa referència a ella mateixa. Anomenada estructura de dades recursiva perquè aquesta referència a ella mateixa representa, en realitat, un encadenament entre elements del mateix tipus (normalment anomenats nodes). sin. estructura de dades encadenada garbage collection f sin. recollida d’escombraries llista doblement encadenada f Estructura encadenada en què els nodes contenen dues referències a altres nodes: un al següent i un a l’anterior. llista simplement encadenada f Estructura encadenada en què els nodes contenen una única referència a un altre node (o bé el següent per a tots ells, o bé l’anterior). node f Element bàsic en representacions encadenades de col·leccions. recollida d’escombraries f Procés mitjançant el qual, mentre un programa s’executa, es recol·lecten aquells trossets de memòria que ha fet servir però ja no tornarà a fer servir. Alguns llenguatges de programació com el Java incorporen un sistema de recollida d’escombraries. Aquesta mena de sistemes permeten als programadors no haver de retornar explícitament al sistema operatiu la memòria descartada. sin. garbage collection referència f Tipus de dada usat en alguns llenguatges de programació (C++) per a fer referència a un altre objecte o valor. El concepte és equivalent al d’apuntador, i únicament presenta diferències de sintaxi en el llenguatge de programació. referència nul·la f Referència el valor de la qual no apunta a cap objecte. representació circular f Representació encadenada en què el darrer node està encadenat amb el primer. Utilitzada amb cura, aquest tipus de representació ens permet accedir al primer i al darrer d’una col·lecció en temps constant i guardar una única referència (al darrer node), en lloc de dues (una al primer i una al darrer). representació encadenada f Representació de col·lecció que fa ús d’una estructura de dades encadenada.

Contenidors seqüencials


© FUOC • P06/05001/00577 • Mòdul 3

64

vector extensible m Implementació que usa com a representació un vector, que és redimensionat segons les necessitats d’espai; és a dir, es redimensiona segons el nombre d’elements a guardar-hi.

Bibliografia Bibliografia bàsica Franch, X. (2001). Estructures de dades. Especificació, disseny i implementació (4a. ed.). Barcelona: Edicions UPC. Disponible en línia a: www.edicionsupc.es Goodrich, M.; Tamassia, R. (2001). Data structures and algorithms in Java (2a. ed.). John Wiley and Sons. Peña Marí, R. (2000). Diseño de programas. Formalismo y abstracción (2a. ed.). Madrid: Prentice Hall. Sahni, S. (2000). Data structures, algorithms, and applications in Java. Summit: McGraw-Hill. Weiss, M. A. (2003). Data structures & problem solving using Java (2a. ed.). Upper Saddle River: Addison Wesley. Disponible en línia a: www.cs.fiu.edu/~weiss

Contenidors seqüencials


© FUOC • P06/05001/00577 • Mòdul 3

65

Contenidors seqüencials

Annex

Per a saber-ne més Al llarg del mòdul hem descrit amb detall suficient els TAD seqüencials bàsics i les implementacions més emprades. Hi ha, però, algunes variacions tant pel que fa al TAD com a la implementació que no hem descrit. Comentarem breument els elements més representatius que han quedat per tractar i les fonts en què us podeu documentar. En l’examen de l’assignatura, no s’exigirà tenir coneixement previ dels temes estudiats aquí, si bé és possible treballar-los com a exercici. No és recomanable revisar aquests temes fins a haver assimilat el contingut del mòdul. Un TAD que no hem estudiat i que combina les operacions del TAD Cua amb el TAD Pila és la doble cua. Una doble cua permet afegir i esborrar elements per qualsevol dels dos extrems. En podeu trobar una bona descripció en l’obra de Goodrich i Tamassia (2001). Un tema que no veiem en aquest text és l’especificació algebraica dels TAD. En podeu trobar una bona descripció general i

l’especificació per als diferents TAD seqüencials vistos en aquest mòdul, tant en la obra de Franch (1999) com en la de Peña (1998). Podeu trobar la implementació de llistes mitjançant un vector en l’obra de Sahni (2000), en què es parla de la simulació d’apuntadors i s’explica com es pot gestionar la memòria des de la mateixa implementació del TAD. El TAD SkipList és una variació interessant del TAD Llista que mitjançant encadenaments addicionals permet fer operacions de cerca d’elements (i d’altres) d’una manera més eficient. Els TAD SkipList es fan servir habitualment com a implementació del TAD Diccionari, que es veu més endavant en l’assignatura. El trobareu descrit en diversos llocs, entre ells, en les obres de Goodrich i Tamassia (2001) i de Sahni (2000).



Issuu converts static files into: digital portfolios, online yearbooks, online catalogs, digital photo albums and more. Sign up and create your flipbook.