Professional Documents
Culture Documents
Asidai 225x c06 Ud1
Asidai 225x c06 Ud1
ndex
Introducci ............................................................................................... Objectius ................................................................................................... 1. Assignaci dinmica de memria. Mecanismes en C ..................... 1.1. Aprofundiment en el tipus punter del llenguatge C ............... 1.1.1. Punters genrics ............................................................... 1.1.2. Els punters i lespecificador const .................................. 1.1.3. Importncia dels punters en la definici de taules ...... 1.1.4. Taules de punters ............................................................. 1.1.5. Punters a punters ............................................................. 1.1.6. Taula de punters a cadenes ............................................. 1.2. Assignaci dinmica de memria .............................................. 1.2.1. Funcions per a lassignaci dinmica de memria en C .............................................................. 1.2.2. Taules dinmiques en C ................................................... 1.2.3. Reassignaci de blocs de memria en C ........................ 2. Llistes .................................................................................................. 2.1. Llistes simplement encadenades .............................................. 2.2. Llistes simplement encadenades ordenades sense repetits ............................................................ 2.3. Llistes circulars o en anell ......................................................... 2.4. Llistes doblement encadenades ................................................ 3. Piles i cues ........................................................................................... 3.1. Piles .............................................................................................. 3.1.1. Implementaci del tipus de dada pila ............................ 3.1.2. Aplicacions de les piles .................................................... 3.2. Cues .............................................................................................. 3.2.1. Implementaci del tipus de dada cua ............................. 3.2.2. Aplicacions de les cues .....................................................
5 7 9 9 10 10 12 13 16 17 18 19 20 24 26 27 35 37 37 39 39 40 42 45 46 49
Introducci
En laprenentatge de la programaci estructurada i modular hem aprs a gestionar dades de diversos tipus: nhi ha de facilitades pel llenguatge de programaci i nhi ha de dissenyades per nosaltres mateixos. A ms, hem efectuat aquesta gesti en memria i tamb hem aprs com enregistrar les dades en suport extern amb la utilitzaci dels sistemes gestors de fitxers. Si pensem una mica en com desenvolupem un programa, ens adonarem que en el moment de dissenyar els algorismes, hem de fixar les variables que cal utilitzar i la seva grandria. Aix provoca problemes, ja que de vegades seria convenient decidir la gesti de la memria en temps dexecuci perqu daquesta manera es podria adequar a les necessitats de cada moment. Aquesta possibilitat, desconeguda per nosaltres, ens s facilitada amb laparici de dades dinmiques. No tots els llenguatges de programaci faciliten la utilitzaci de dades dinmiques. En aquesta unitat didctica, establirem els fonaments per poder utilitzar dades dinmiques en llenguatges que les suportin. El llenguatge C suporta dades dinmiques i la seva gesti depn de la utilitzaci correcta dels punters. Aix doncs, en el nucli dactivitat Assignaci dinmica de memria. Mecanismes en C, introdum les operacions que els llenguatges acostumen a facilitar per a la gesti dinmica de la memria i, en especial, com cal actuar en utilitzar el llenguatge C, el qual basa tota la gesti dinmica de la memria en la utilitzaci correcta dels punters. En laprenentatge de la programaci en C, sintrodueixen els punters com a recurs imprescindible en el pas per referncia en la crida de funcions. Us cal refrescar els coneixements introductoris sobre els punters en C. Una vegada coneguts els mecanismes per assolir lassignaci dinmica de la memria, cal aplicar-los per a una gesti eficient de les dades en memria. Per poder-les gestionar eficientment, caldr conixer les principals estructures de dades dinmiques emprades en la programaci informtica: llistes, piles, cues, arbres... En el nucli dactivitat Llistes presentarem lestructura de dada dinmica llista emprada quan cal emmagatzemar informaci de manera lineal (element darrere element) i els principals algorismes per a fer-ne la gesti: recorregut, inserci, recerca i eliminaci.
En el nucli dactivitat Piles i cues presentarem aquestes estructures de dades dinmiques, de les quals ja podem avanar que no sn altra cosa que tipus especfics de llistes, per que per la seva important contribuci al mn de la informtica es mereixen un captol especfic. Per aconseguir els nostres objectius, heu de reproduir en el vostre ordinador i, si pot ser, en diverses plataformes tots els exemples incorporats en el text. Per a aix, trobareu tots els fitxers necessaris en la secci Recursos de contingut. Al web tamb trobareu activitats i exercicis dautoavaluaci. I un consell final: com que lassignaci dinmica de memria s una tcnica molt potent, ens cal conixer-la i aplicar-la, per tamb s complicada i, per tant, cal anar amb molt de compte. Penseu que sereu vosaltres els gestors de la memria de lordinador, passant per damunt del sistema operatiu. Per tant, si no ho fem de la manera adequada podem provocar el mal funcionament de lordinador. Evidentment, reiniciant la mquina tot queda solucionat. Per, i les dades que sestaven gestionant...?, i el programa que sestava executant...? Molt de compte!
Objectius
En acabar la unitat didctica, heu de ser capaos del segent: 1. Utilitzar de manera correcta els punters en el llenguatge C. 2. Utilitzar, quan correspongui, memria dinmica en el disseny dalgorismes. 3. Dissenyar algorismes per gestionar de manera eficient taules dinmiques, llistes, piles i cues. 4. Identificar en quines situacions s convenient la utilitzaci de taules dinmiques, llistes, piles i cues.
La majoria daplicacions informtiques gestionen dades que poden o no estar emmagatzemades en suport extern, per que mentre es gestionen, resideixen en la memria de lordinador. Els llenguatges de programaci faciliten mecanismes perqu el programador pugui declarar dades en memria. Aix, per exemple, estem acostumats a declaracions del tipus: En pseudocodi:
var edat: natural; sou: real; dni: cadena[10]; noms: taula[MAX_NOMS] de cadena[MAX_LONG_NOM]; fivar
En llenguatge C:
unsigned int edat; float sou; char dni[10]; char noms[MAX_NOMS] [MAX_LONG_NOM];
Les declaracions anteriors, tant si sn en pseudocodi com en llenguatge C, tenen en com que sn declaracions de dades esttiques. Recordem que: Les dades esttiques existeixen durant tota lexecuci del programa i/o acci/funci on estan declarades i tenen una capacitat fixa, mentre que les dades dinmiques es poden crear i destruir en temps dexecuci, segons evolucioni el programa, i poden acomodar la seva capacitat a les necessitats reals de lexecuci.
La majoria de llenguatges de programaci faciliten al programador mecanismes per a la gesti de dades dinmiques. El llenguatge C no ns una excepci i per gestionar amb aquest dades dinmiques ens cal la utilitzaci dels punters.
10
utilitzaci per passar arguments per referncia en la crida a funcions en llenguatge C. Enumerem els continguts que suposem ja coneguts: Declaraci de punters. Assignaci de valor a punter. Accs a una dada via punter. Importncia del tipus dobjecte a apuntar. Punters en el pas darguments per referncia. Error daccs al punter NULL. Punters i taules. Punters a estructures. Partint de la base que tenim assumits els coneixements corresponents als continguts anteriors, continuem amb lestudi del tipus punter en el llenguatge C.
Aquests punters sutilitzen per emmagatzemar adreces, per no es poden utilitzar per gestionar les dades emmagatzemades en aquestes adreces (amb loperaci dindirecci *, s clar) ja que no sn de cap tipus en concret, de manera que el llenguatge C no sap com interpretar les dades apuntades. Imaginem-nos que escrivim un codi semblant a:
void *p; int x = 1000; ... p = &x; /* p est apuntant ladrea de memria on hi ha x */ *p = 2000; /* voldrem canviar el valor emmagatzemat en x a travs de p */
El compilador es queixar en la darrera instrucci amb un missatge semblant a Not an allowed type, que vol indicar que no s perms loperador dindirecci amb un punter void *.
11
directiva de precompilaci #define), s una dada que ocupa un espai de memria, al qual saccedeix a travs del nom de la constant i que pren un valor en el moment de la seva declaraci que es mant constant mentre la dada resideix en memria. Lafirmaci anterior s certa a mitges. Hi manca una observaci important. El valor emmagatzemat en una dada constant no s modificable a travs de la dada (constant), per com que s en memria, shi pot accedir a travs dun punter i, daquesta manera, modificar-ne el valor emmagatzemat. s a dir:
const int n=10; int *p; ... n = 20; /* error en temps de compilaci */ ... p = &n; *p = 20; /* sha modificat el contingut de n */
Per tant, podrem dir que lespecificaci const per a una dada fa constant laccs a la posici de memria a travs del nom de la dada. Lespecificaci const tamb s utilitzable en la declaraci de punters. Tenim dues possibles utilitzacions: 1) Punter a constant En aquest cas el valor apuntat pel punter es comporta com a constant (no modificable) quan shi accedeix a travs del punter. La seva sintaxi s:
const <tipus_dada> *<nom_punter>;
2) Punter constant En aquest cas ladrea de memria emmagatzemada en el punter s constant. La seva sintaxi s:
<tipus_dada> *const <nom_punter>;
Evidentment, les dues utilitzacions es poden combinar per aconseguir un punter amb dues particularitats: que ladrea que emmagatzema sigui constant i que la dada apuntada per aquesta adrea es comporti com a constant quan shi accedeix a travs del punter. Estem davant un punter constant a constant:
const <tipus_dada> *const <nom_punter>;
12
int i = 30; const int j = 20; /* inicialitzaci obligatria en aquest moment */ const int *pi = &i; int *const pj = &j; /* inicialitzaci obligatria en aquest moment */ const int *const pc = &i; /* inicialitzaci obligatria en aquest moment */
Imaginem que utilitzem les dades anteriors en les instruccions segents, suposant que en cada instrucci partim de la situaci donada en les declaracions anteriors.
i = 300; /* correcte */ j = 200; /* error ja que j s constant */ pi = &j; /* correcte */ *pi = 300; /* error ja que la dada apuntada a travs de pi s constant */ pj = &i; /* error ja que ladrea que guarda pj s constant */ *pj = 200; /* correcte */ pc = &j; /* error */ *pc = 300; /* error */
Lemplenament de la taula anterior tamb shauria pogut fer amb lajut dun punter auxiliar:
int *pt = t; for (i=0; i<100; i++) { *pt = 2*i; pt++; }
Tamb cal tenir molt clar quin s lespai que ocupen en memria les declaracions dels exemples anteriors. Tal com podeu observar en la figura 1, en realitat la taula t genera un punter que cont ladrea inicial de lespai reservat per a les 100 posicions de la taula. El punter pt, que hem declarat posteriorment, en haver-lo inicialitzat amb el valor de t, tamb est apuntant la posici inicial de lespai reservat per a la taula. Sembla que t i pt sn equivalents, per en realitat no ho sn: el punter t s constant i no en pot modificar el contingut, ja que en cas de fer-ho es perdria ladrea de la primera posici de la taula; en canvi, pt pot anar modificant-ne el contingut, tal com sha vist en el darrer exemple.
13
Daltra banda, tenim clar que una cadena no s altra cosa que una taula de carcters amb una marca especial de final de cadena (el zero binari). Per tant, qualsevol tractament sobre les taules genriques es pot efectuar sobre cadenes. Fins aqu, les definicions de cadenes han estat del tipus segent:
char cad1[100]; /* cadena de 100 carcters */ char cad2[]="Hola"; /* cadena de 5 carcters (zero binari!) */
En les dues definicions, cad1 i cad2 han provocat la reserva de lespai corresponent al punter (de 100 i 5 octets, respectivament) en memria. s possible definir la cadena anterior cad2 amb la sintaxi segent:
char *cad2 = "Hola";
Aquesta definici provoca que internament shagin reservat lespai corresponent al punter i, a ms, 5 octets (quatre lletres i el carcter \0). Cal anar molt amb compte amb declaracions daquest tipus: !
char *s;
Fixeu-vos que sest produint la reserva de lespai corresponent al punter, per en canvi no hi ha cap reserva despai per als possibles carcters duna cadena apuntada per s. Aquest punter ens podr servir per a una assignaci dinmica de memria en temps dexecuci.
El fet de tenir una taula que emmagatzemi adreces de memria pot ser til en diferents situacions, sobretot quan es vol simular amb una taula unidimensional una taula multidimensional. El llenguatge C permet definir taules multidimensionals. Fixeu-vos en aquesta declaraci:
int t[10][20];
14
Correspon a una taula de dues dimensions, la qual hem dinterpretar com un tauler de 10 files (numerades de 0 a 9) i 20 columnes (numerades de 0 a 19), com sobserva en la figura 2. Cada casella est destinada a contenir un valor de tipus int.
Figura 2. Taula de 10 files i 20 columnes
En realitat, el llenguatge C crea una taula de 10 punters a taules de 20 celles cadascuna, com ens mostra la figura 3.
Figura 3. Distribuci en memria duna taula bidimensional t[10][20] en llenguatge C
Tanmateix, el llenguatge C tamb ens assegura que les 10 taules de 20 celles estan emmagatzemades en memria consecutivament. s a dir, en lexemple presentat, el llenguatge C provoca una reserva de 200 caselles consecutives en memria, corresponent a la seqncia de les caselles collocades en fila, luna al costat de laltra. s a dir: la casella que cont el valor A s accessible per t[0][0] o *t; la casella que cont el valor B s accessible per t[1][3] o *(t+1*20+3), ja que la casella de la fila 2 - columna 3 es troba en la posici 23 (comenant a numerar per zero) si situem les files en seqncia, luna darrere laltra;
15
la casella que cont el valor C s accessible per t[9][18] i tamb per *(t+198), ja que 198 s el resultat de 9*20+18. Ara b, interessa tenir una estructura semblant les files de la qual no estiguin consecutives en la memria? O, fins i tot, volem que no totes les files tinguin la mateixa longitud? En aquest cas, utilitzarem una taula de punters on cada punter contindr ladrea duna taula unidimensional corresponent a cada fila. Aix, per exemple:
int fila0[20], fila1[10], ..., fila9[15]; int *t[10]; t[0]=fila0; t[1]=fila1; ..., t[9]=fila9;
Observem que: la casella que cont el valor A s accessible per fila0[0] o t[0][0] o (*t)[0] o **t; (doble punter!) la casella que cont el valor B s accessible per fila1[8] o t[1][8] o(*(t + 1))[8]o*(*(t + 1) + 8); la casella que cont el valor C s accessible per fila9[18] o t[9][18] o(*(t + 9))[18]o *(*(t + 9) + 18). Totes les possibilitats anteriors sn el resultat de combinar dos fets: que els noms de les taules siguin punters a la primera posici de memria corresponent a la taula i que sutilitzi laritmtica de punters. Quina s la millor utilitzaci? Noms hi ha una resposta: la que a vosaltres us sigui ms planera. Aix no impedeix que hgiu de conixer totes
Doble punter Un doble punter a un tipus T no s res ms que un punter destinat a apuntar un punter destinat a apuntar una dada de tipus T.
16
les possibilitats i que les sapigueu utilitzar, ja que de vegades us podeu trobar un codi desenvolupat per altres programadors que han fet servir altres tcniques. Una darrera pregunta: t sentit utilitzar taules de punters? La resposta s afirmativa. En moltes ocasions necessitem una estructura bidimensional per emmagatzemar valors. Suposem que en temps de codificaci del programa coneixem el nombre de files, per que desconeixem el nombre de columnes, que fins i tot pot variar per a cada fila en temps dexecuci. En aquest cas, podrem definir una taula de punters amb una dimensi equivalent al nombre de files i deixar per al moment de lexecuci la definici dinmica de les columnes a apuntar per a cada fila, s a dir, acomodarem la seva dimensi a les necessitats de lexecuci. Evidentment, per poder fer aix necessitem utilitzar lassignaci dinmica de memria.
diem que t s un punter al tipus int, per coherncia davant duna declaraci com:
int *x[10];
haurem de convenir que x s un punter al tipus int *, s a dir, un doble punter al tipus int. De la mateixa manera que davant la declaraci anterior de la taula t, podem definir que:
int *pt = t; /* pt cont la mateixa adrea que cont t */
17
de manera que podrem utilitzar pmat en tots els llocs on apareix mat. Finalitzem aquesta breu introducci dels punters a punters amb una pregunta: t sentit utilitzar un doble punter? La resposta s afirmativa. Sovint necessitem una estructura bidimensional per emmagatzemar valors. Suposem que en temps de codificaci del programa ni tan sols coneixem el nombre de files, el qual pot variar segons lexecuci. En aquest cas, podrem definir un doble punter i deixar per al moment de lexecuci la definici dinmica de les files. Posteriorment, podrem definir, tamb dinmicament, les columnes corresponents a cada fila. Evidentment, necessitem utilitzar lassignaci dinmica de memria.
Hem hagut de definir una taula de 7 files per 10 columnes, quan hi ha noms que ocupen menys de 9 carcters. Per tant, desaprofitem espai. La soluci s que cada fila tingui tantes posicions com siguin necessries. El llenguatge C ens permet declarar una taula de punters a cadenes ocupant lespai necessari i imprescindible amb una declaraci com aquesta:
char *d[]={"Dilluns","Dimarts","Dimecres","Dijous","Divendres","Dissabte","Diumenge"};
En la compilaci, es crea una taula d de set punters a carcter, s a dir, char *, on cada punter cont ladrea a una posici de memria on trobem els continguts Dilluns, Dimarts, Dimecres, Dijous, Divendres, Dissabte i Diumenge. El valor del nombre de posicions de la taula es pot forar en la codificaci del programa escrivint:
char *d[7]={"Dilluns","Dimarts","Dimecres","Dijous","Divendres","Dissabte","Diumenge"};
18
Si el valor que defineix la grandria de la taula de punters s superior a la quantitat de valors inicials existents entre les claus, el llenguatge inicialitza convenientment les primeres posicions de la taula i deixa les altres amb el valor NULL. En canvi, si el valor que defineix la grandria de la taula de punters s inferior a la quantitat de valors inicials, el compilador detectar lerror i no generar el codi objecte. Observem que la variable d s un doble punter a char, per les declaracions char *d[7] i char **d no sn equivalents, ja que en el primer cas es genera una taula de set punters a char (i, evidentment, el doble punter a char corresponent al nom de la taula) i en el segon cas es genera nicament el doble punter a char sense cap altre tipus de reserva.
Lassignaci dinmica de memria consisteix a reservar, la quantitat de memria necessria per emmagatzemar dades en temps dexecuci.
Us hem de fer una observaci molt important: no tots els llenguatges de programaci disposen daquesta opci. El nostre bon amic C s que la t. ! Abans dentrar, per, en les particularitats de C, cal remarcar que els llenguatges que permeten lassignaci dinmica de memria ho fan principalment dacord amb dues accions i/o funcions: la primera intenta assignar la memria necessria i la segona allibera la memria assignada prviament, de manera que pugui ser utilitzada en peticions posteriors. Per poder treballar amb assignaci dinmica de memria, s indispensable la utilitzaci de punters. Fins ara els hem treballat detingudament en llenguatge C, per no en pseudocodi. A continuaci els presentarem en pseudocodi. Declaraci duna variable p punter a un tipus T; T pot ser un tipus predefinit del llenguatge o un tipus de dada definida pel programador:
var p : ^T;
Assignaci de valor a una variable p de tipus punter a partir de ladrea duna variable d del tipus T, ja existent en memria:
p = ^d;
19
Cal comentar lexistncia dun valor especial, el valor NULL, per indicar que un punter no cont cap adrea de memria. Tamb s important notar que continua essent responsabilitat del programador inicialitzar convenientment les variables de tipus punter a NULL perqu no continguin valors porqueria, que podrien ser interpretats com a adreces de memria incorrectes. Les dues accions i/o funcions bsiques que tenen tots els llenguatges que permeten lassignaci dinmica de memria en pseudocodi sn les segents:
acci assignar_memria (var p: ^T);
Intenta assignar lespai de memria necessari per a una dada de tipus T, ladrea de la qual quedar enregistrada en la variable punter p. Si el valor retornat a travs del parmetre p s NULL, vol dir que no sha pogut assignar lespai de memria necessari.
acci alliberar_memria (var p: ^T);
s el procs contrari i intenta alliberar la memria assignada per p perqu quedi disponible per a altres necessitats. Suposarem que en cas de tenir xit, el valor p passa a ser NULL (per a aix ha de ser un argument per referncia). Evidentment, s imprescindible que la memria assignada per p que volem alliberar hagi estat assignada per una instrucci dassignar_memria. La comesa del programador s alliberar la memria assignada dinmicament. Cal anar amb compte! Si sassigna memria dins una acci o funci, i en sortir de lacci o funci no es mant cap lligam amb la zona de memria assignada, aquesta ja no podr ser alliberada, ja que no t manera daccedir-hi. Haurem deixat una zona de memria inutilitzada fins que es reinici la mquina.
20
#include <stdlib.h> void free (void *p); /* Intenta alliberar el bloc de memria apuntat per largument p sempre que hagi estat assignat per les funcions malloc, calloc o realloc. */
s convenient acostumar-se a alliberar la memria assignada prviament quan ja no sigui necessria. Un bon sistema operatiu hauria de garantir que, en finalitzar lexecuci dun programa, la memria assignada dinmicament i no alliberada shauria dalliberar de manera automtica. Aix, malauradament, no s aix. Sistemes operatius mpliament estesos no fan aquesta comprovaci i la memria queda amb forats fins que es reinicia la mquina.
!!
Trobareu el fitxer u1n1p01.c i la resta darxius necessaris en la secci Recursos de contingut del web daquest crdit, concretament a Codi font en C.
void main(void) { int *p=NULL; unsigned int unsigned int unsigned int int k; char c;
/* punter a taula dinmica per guardar els enters introduts */ *q=NULL; /* punter a taula dinmica per comptadors */ qmax; /* quantitat mxima de valors diferents */ qn;/* quants enters diferents hi ha a la taula */
clrscr(); printf ("Quina quantitat mxima de valors diferents vol introduir?"); do { k = scanf ("%u",&qmax); neteja_stdin(); } while (k == 0); if (qmax == 0) { printf ("No t sentit l'execuci d'aquest programa.\n");
21
exit (0); } p = (int *) malloc ((qmax+1) * sizeof(int)); /* Tcnica sentinella */ if (p == NULL) { printf ("Memria insuficient per gestionar tal quantitat de valors.\n"); exit (1); } q = (unsigned int *) malloc (qmax * sizeof(unsigned int)); if (q == NULL) { printf ("Memria insuficient per gestionar tal quantitat de valors.\n"); free (p); exit (1); } qn = 0; do { printf ("Introdueixi un enter.\n"); do { k = scanf ("%d",&p[qn]); neteja_stdin(); } while (k == 0); for (k=0; p[k] != p[qn]; k++); if (k < qn) q[k]++; else if (qn == qmax) printf ("No emmagatzemat per haver sobrepassat la quantitat mxima de valors diferents.\n"); else { q[qn] = 1; qn++; } printf ("Vol continuar (S/N)?"); do { k = scanf ("%c",&c); neteja_stdin(); } while (k == 0 || (c!='S' && c!='s' && c!='N' && c!='n')); } while (c=='S' || c=='s'); if (qn == 0) printf ("No s'ha introdut cap valor\n"); else { clrscr(); for (k = 0; k < qn ; k++) printf ("Valor %d: %u\n",p[k],q[k]); } free (p); free (q); }
Observem que, tot i utilitzar punters, hem tractat les zones de memria assignades a p i q com si fossin taules, s a dir, amb loperador dindexaci. Observem, tamb, la utilitzaci de loperador sizeof dins la crida de la funci malloc per calcular el nombre de bytes necessaris. Aquesta utilitzaci s necessria per mantenir la compatibilitat del codi en diferents plataformes. Aix, en Turbo C, els int ocupen 2 bytes, mentre que en gcc nocupen 4. Cal utilitzar loperador sizeof perqu el llenguatge en calculi el nombre que correspongui. En lexemple anterior hem utilitzat les funcions malloc i free. El llenguatge C incorpora una instrucci que es pot utilitzar especialment per assignar memria a una taula dinmica. s la funci calloc.
#include <stdlib.h> void *calloc (size_t qelem, size_t n);
22
/* Intenta assignar un bloc de memria per a qelem elements de n bytes cadascun, per emmagatzemar dades de qualsevol tipus. Si ho aconsegueix, retorna ladrea de memria a partir del lloc on ha assignat lespai com un punter a void, que cal assignar a la variable punter que correspongui. En cas de no aconseguir lespai retorna un valor NULL. */
Amb la utilitzaci daquesta funci, les crides a la funci malloc que apareixien en lexemple anterior:
malloc ((qmax+1) * sizeof(int)); malloc (qmax * sizeof(unsigned int));
Vegem ara un nou exemple de gesti de taules dinmiques. s un cas similar a lexemple anterior, per en aquesta ocasi es tracta de comptabilitzar paraules. Per gestionar la memria eficientment interessa emmagatzemar les paraules en lespai adequat. Per tant, en aquest cas, hem de definir la taula que enregistra les paraules com a taula de punters a char, de manera que a mesura que lusuari vagi introduint les paraules anirem sollicitant memria al sistema operatiu per tal de crear dinmicament les cadenes on emmagatzemarem les paraules.
/* u1n1p02.c */ #include #include #include #include #include <stdio.h> <conio.h> <stdlib.h> <string.h> "verscomp.h"
!!
Trobareu el fitxer u1n1p02.c i la resta darxius necessaris en la secci Recursos de contingut del web daquest crdit , concretament a Codi font en C.
void main() { char **p=NULL; /* punter a taula dinmica per guardar les paraules introdudes */ unsigned int *q=NULL; /* punter a taula dinmica per comptadors */ unsigned int qmax; /* quantitat mxima de paraules diferents */ unsigned int qn;/* quantes paraules diferents hi ha a la taula */ int k; char s[255]; /* cadena reservada per a llegir les paraules */ char c; clrscr(); printf ("Quina quantitat mxima de paraules diferents vol introduir?"); do { k = scanf ("%u",&qmax); neteja_stdin(); } while (k == 0); if (qmax == 0) { printf ("No t sentit l'execuci d'aquest programa.\n"); exit (0); } p = (char **) calloc (qmax, sizeof(char *)); /* Tcnica sentinella */ if (p == NULL) { printf ("Memria insuficient per gestionar tal quantitat de paraules.\n"); exit (1);
23
} q = (unsigned int *) calloc (qmax, sizeof(unsigned int)); if (q == NULL) { printf ("Memria insuficient per gestionar tal quantitat de paraules.\n"); free (p); exit (1); } qn = 0; do { printf ("Introdueixi una paraula.\n"); do { k = scanf ("%[^,;.:?!\"\n]",s); neteja_stdin(); } while (k == 0); for (k=0; k<qn && strcmp (p[k],s)!=0; k++); if (k < qn) q[k]++; /* La paraula ja estava enregistrada */ else if (qn == qmax) printf ("No emmagatzemat per haver sobrepassat la quantitat mxima de paraules diferents.\n"); else { p[qn] = (char *) calloc (strlen(s)+1, sizeof (char)); if (p[qn] == NULL) printf ("Memria insuficient per introduir la paraula.\n"); else { strcpy(p[qn],s);/* s'ha emmagatzemat la paraula */ q[qn] = 1; qn++; } } printf ("Vol continuar (S/N)?"); do { k = scanf ("%c",&c); neteja_stdin(); } while (k == 0 || (c!='S' && c!='s' && c!='N' && c!='n')); } while (c=='S' || c=='s'); if (qn == 0) printf ("No s'ha introdut cap valor\n"); else { clrscr(); for (k = 0; k < qn ; k++) { printf ("Valor %s: %u\n",p[k],q[k]); free(p[k]); } } free (p); free (q); }
Hi ha algunes observacions importants a fer a partir de lexemple que acabem de veure: Per poder llegir la paraula per teclat, es necessita una variable de tipus cadena i aquesta ha dhaver estat declarada com a esttica. s la cadena s. A diferncia de lexemple anterior, no hem emprat la tcnica de sentinella, ja que per a aix necessitem definir una zona dinmica per guardar la paraula que acaba dintroduir lusuari i, daltra banda, aquesta sollicitud de memria al sistema operatiu pot resultar estril si la paraula ja estava incorporada a la taula, ja que llavors haurem de procedir a alliberar la memria sollicitada.
24
En lassignaci despai per emmagatzemar la paraula amb el punter p[qn], la quantitat de carcters necessaris s strlen(s)+1, ja que hem de pensar en el carcter nul. En finalitzar el programa, abans dalliberar lespai assignat a travs dels punters p i q, cal alliberar lespai assignat a travs de cada punter de la taula apuntada per p, s a dir, lespai assignat a travs de p[0], p[1], p[2]... Una atenci especial mereix lassignaci despai a cadenes que fa la taula de punters a travs del punter p:
p = (char **) calloc (qmax+1, sizeof (char *));
La importncia radica en el fet que, com que p s un doble punter a char, les posicions que es generen en aquesta assignaci estan destinades a apuntar cadenes, s a dir, a ser punters a char, amb la qual cosa la grandria en bytes necessria per a cada una daquestes posicions s la grandria que necessita un punter a char. Per aix sutilitza sizeof (char *) per saber lespai que ocupa un punter a char.
Suposem que el punter p a un tipus T apunta un espai dinmic assignat pel sistema operatiu. Disposem de dos mtodes per modificar la grandria daquest espai apuntat per p: mtode manual i mtode automtic. 1) Mtode manual Consisteix a sollicitar al sistema operatiu lassignaci dinmica dun nou espai via un punter auxiliar paux amb la grandria adequada, on copiem les dades apuntades per p, en cas que el sistema operatiu ens faciliti el nou espai. En aquest cas, desprs de la cpia cal alliberar lespai assignat a p i fer una simple assignaci p = paux, de manera que p apunti la nova zona de grandria adequada.
25
Aquest mtode t un problema: durant un cert interval de temps dexecuci, el sistema ha de tenir prou memria per mantenir lespai amb les dades originals i lespai on copiar aquestes dades. Podria donar-se el cas que el sistema no disposs de memria suficient per poder efectuar-ne el trasps. 2) Mtode automtic Consisteix a utilitzar la funci realloc que incorpora el llenguatge C.
#include <stdlib.h> void *realloc (void *p, size_t n); /* El parmetre p s un punter que apunta un bloc de memria pel qual volem canviar la quantitat de memria assignada. Si p s NULL, aquesta funci es comporta exactament igual que la funci malloc. Si p no s NULL i el bloc de memria apuntat actualment ha estat assignat amb les funcions malloc, calloc o la mateixa realloc, aquesta funci intenta modificar la seva grandria actual i adequar-la al valor n. Les dades emmagatzemades no canvien en el tros de bloc conservat. Aquesta funci retorna un punter al nou espai assignat, el qual pot residir en un altre lloc de la memria. En cas que no hi hagi prou espai en la memria o si n s zero, la funci retorna el valor NULL. En aquest cas, el bloc original s alliberat. */
Aquest mtode t un problema: si el sistema no pot efectuar-ne el trasps, hem perdut la zona de memria assignada! Molt perills, no us sembla?
26
2. Llistes
Iniciem lestudi dels tipus de dades dinmiques lineals que cal conixer, ja que es poden utilitzar en diversos mbits. El tipus de dada dinmica lineal per excellncia s la llista. Hi ha dos tipus de dades dinmiques lineals ms les piles i les cues que no deixen de ser tipus especials de llista, per acostumem a tenir captols especfics en tots els manuals de programaci per la seva importncia.
Llistes i taules En el crdit Estructures de dades dinmiques les llistes tenen un tractament encadenat i dinmic. Hi ha autors i llibres que presenten el concepte de llista com una taula en la qual es pot accedir al primer element i, a partir daquest, es pot accedir als segents. Aquest enfocament no deixa de ser un tractament seqencial duna taula, tractament que coneixem en profunditat. Per aquest motiu, tractarem el concepte de llista en lmbit ms ests, com un conjunt seqencial dinformaci encadenada amb tractament dinmic.
Una llista simplement encadenada (anomenada llista la majoria de les vegades) s una seqncia delements, anomenats generalment nodes, enllaats mitjanant punters continguts en els mateixos nodes, punters que apunten altres nodes. Una llista sidentifica per un punter al primer node de la seqncia. En particular, una llista pot ser buida (no hi ha seqncia delements) i es reconeix perqu el seu punter al primer node cont el valor NUL. En general, un node consta de dues parts: Una part que cont la informaci que es vol emmagatzemar a la llista. Una part que cont el lligam cap a un altre node de la llista. El darrer node de la llista cont el valor NUL com a lligam.
Per tant, si volem gestionar una llista simplement encadenada de dades dun cert tipus T, caldr tenir en compte la definici segent de node:
tipus node = tuple inf : T; /* cont la informaci */ seg : ^node; /* punter a segent node */ fituple
27
B, ja sabem com s una llista simplement encadenada. De la figura 5 es dedueix que per anar a un node no hi ha ms remei que efectuar un recorregut seqencial des del punter inici i que, una vegada estem en un node, noms es pot avanar cap als nodes posteriors per no es pot retrocedir als nodes anteriors. Hi ha altres tipus de llistes amb millors prestacions, per ms complicades de gestionar; per aix primer ens hem de centrar en els algorismes de gesti de llistes simplement encadenades i, quan ja hgim adquirit experincia, ja podrem intentar la incursi en la gesti de llistes ms complicades.
Evidentment, crear la llista noms consisteix a crear el punter inicial. Cal tenir, per, la precauci dinicialitzar-lo amb el valor NUL. 2) Afegir un element per linici La figura 6 ens ajudar a fer el seguiment del procs dafegir un element per linici de la llista. El procs dafegir per linici consisteix a crear un nou node N, emplenar-lo amb la informaci que es vol afegir a la llista i afegir-lo a la llista, de manera que el punter inicial, que en un principi est apuntant el node A (fletxa E), passi a apuntar el nou node N (fletxa E2), i aquest passi a apuntar el node A (fletxa E1).
Figura 6. Exemplificaci del procs dafegir un element per linici duna llista
28
Per poder crear el node N, ha calgut utilitzar una variable aux punter a tipus T. No podem fer aquesta tasca amb el punter inici ja que perdria la informaci que cont, i que s la que ens adrea al node A. Podeu observar tamb que largument inici que es passa a la funci afegirInici est declarat com a pas per referncia. Aix s degut al fet que la funci afegirInici ha de modificar el valor que contenia el punter inici. Noteu, a ms, que aquesta funci t un bon funcionament si la llista s buida, ja que en aquest cas afegirInici procedeix a inserir el primer element a la llista. Finalment, cal deixar constncia que lordre en qu sefectuen les assignacions s fonamental. Una vegada el nou node est creat i tenim la seva adrea en el punter auxiliar aux, emplenem el camp contenidor dinformaci amb lelement que sha passat per valor i. El segon pas s emplenar el seu punter al node segent, cosa que saconsegueix copiant a aux^.seg el contingut dinici (hem assolit la fletxa E1). El tercer pas s emplenar inici amb ladrea del nou node, la qual tenim a aux (hem assolit la fletxa E2). Si executssim el tercer pas abans que el segon, en aplicar el segon ja no disposarem del valor que ens porta el node A a inici. 3) Recrrer la llista Ja sabem que el recorregut en una llista s sempre seqencial i que en les llistes simplement enllaades noms es pot anar cap endavant. Observem que per poder recrrer la llista cal utilitzar un punter auxiliar, ja que si utilitzssim el punter inici perdrem ladrea on es troba el primer node. Fixeu-vos tamb que el recorregut finalitza quan es troba un node que t el valor NUL en el seu apartat seg. El recorregut ja no comena si el punter inici t el valor NUL (llista buida).
29
4) Afegir un element pel final La figura 7 ens ajudar a fer el seguiment del procs dafegir un element pel final de la llista.
Figura 7. Procs dafegir un valor pel final duna llista simplement encadenada
Afegir un element pel final implica necessriament un recorregut per la llista per arribar al darrer node i procedir a afegir el nou element. El procs dafegir pel final consisteix a crear un nou node N i emplenar-lo amb la informaci que es vol afegir a la llista, de manera que el punter del darrer node (F en el grfic) deixi de tenir el valor NUL com a punter al node segent i passi a apuntar el nou node N. Lalgorisme en pseudocodi seria:
funci afegirFinal (var inici:^node, element : T) retorna natural s /* Arguments: inici: Punter que apunta linici de la llista element: Dada a afegir a la llista Retorna: 0 : Operaci correcta 1 : Error per manca de memria */ var aux1, aux2:^node; fivar assignar_memria (aux1); si aux1 == NUL llavors retorna 1; fisi aux1^.inf = element; aux1^.seg = NUL; si inici == NUL llavors inici = aux1; sin aux2 = inici^.seg; mentre aux2^.seg != NUL fer aux2 = aux2^.seg fimentre aux2^.seg = aux1; fisi retorna 0; fifunci
30
Perqu aquest algorisme sigui correcte, cal utilitzar dues variables punters auxiliars. En efecte, una primera variable aux1 punter a tipus T s necessria per crear el nou node abans de fer cap recorregut i poder detectar daquesta manera que no hi ha problemes deguts a la manca de memria. Una vegada tenim el node N creat i emplenat adequadament amb lelement que es vol afegir a la llista i apuntant NUL, hem de fer que el darrer element de la llista deixi de ser-ho i passi a apuntar el nou node. Per, quin s el darrer element de la llista? Cal tenir present que la llista pot ser buida i, en aquest cas, el darrer element no existeix i noms ens hem de preocupar que el punter inici passi a apuntar el nou node, cosa que aconseguim emplenant-lo amb ladrea que cont aux1. Si el punter inici no t el valor NUL, t sentit preguntar pel contingut dinici^.seg, valor que situem en la segona variable aux2 punter a T. Utilitzem aquest punter per situar-nos en el darrer node F de la llista. Una vegada all, noms ens falta fer que el seu punter al segent passi a apuntar el node N, ladrea del qual tenim en la variable aux1. Finalment, cal fer notar que aqu tamb s imprescindible que largument inici es passi per referncia, ja que el seu contingut ser modificat en cas que la llista sigui buida. 5) Cercar un element Per poder realitzar una operaci daquest estil, cal tenir un criteri de recerca. Cal prendre algunes decisions. Com podem detectar lelement a cercar en un recorregut per la llista? Hi ha diverses possibilitats: coincidncia en el valor dun, alguns o tots els camps de lelement. En el darrer cas seria una recerca destinada noms a saber si lelement s a la llista, ja que no tindria sentit cercar-lo si ja coneixem totes les dades. Com que estem plantejant el problema en una situaci terica, considerarem que hi ha un conjunt de camps que formen un tipus T*, subtipus de T, pels quals sha defectuar la recerca. Per tant, caldr passar com a argument un element del tipus T* que contingui els camps de recerca. Com podem actuar si hi ha la possibilitat delements repetits en la llista? Prenem la decisi de considerar la primera ocurrncia trobada de lelement cercat. Sota aquests supsits, tenim el segent:
funci recerca (inici:^node; var element: T; referncia: T*) retorna natural s /* Arguments: inici: Punter que apunta linici de la llista element: Variable retornada plena amb el valor trobat
31
referncia: Dada de tipus T* a cercar Retorna: 0 : Recerca efectuada amb xit. 1 : Recerca efectuada sense xit. */ mentre inici != NUL i no coincideix (inici^.inf, referncia) fer inici = inici^.seg; fimentre si inici == NUL llavors retorna 1; sin element = inici^.inf; retorna 0; fisi fifunci funci coincideix (element: T, referncia: T*) retorna boole s /* Codi que comprovi si element correspon a referncia */ fifunci
Observeu que dins la funci no utilitzem cap variable punter auxiliar per recrrer la llista, ja que emprem el mateix punter inici. s lcit aix? No estarem perdent ladrea del primer node de la llista? Evidentment s que la perdem dins la funci, per en finalitzar-ne lexecuci el punter continua tenint el mateix valor inicial, ja que sha passat per valor i no per referncia. 6) Inserir un element Per poder realitzar una operaci daquest estil cal tenir com a referncia un element de la llista respecte al qual sinsereix el nou element (abans o desprs). Per poder realitzar aquest tipus de tractament considerem els mateixos supsits que hem tingut en compte en la recerca dun element.
funci inserirAbans (var inici:^node; element: T, referncia: T*) retorna natural s /* Arguments: inici: Punter que apunta linici de la llista element: Dada a afegir a la llista referncia: Dada de tipus T* a cercar per inserir, abans d'aquesta, lelement Retorna: 0 : Operaci correcta 1 : Error per manca de memria 2 : No es troba la referncia */ var pref, ant, aux:^node; fivar pref = inici; ant = NUL; mentre pref != NUL i no coincideix (pref^.inf, referncia) fer ant = pref; pref = pref^.seg; fimentre si pref == NUL llavors retorna 2; fisi assignar_memria (aux); si aux == NUL llavors retorna 1; fisi aux^.inf = element; aux^.seg = pref; si pref == inici llavors inici = aux; /* lelement referncia era el primer de la llista */ sin ant^.seg = pref; /* lelement referncia no era el primer de la llista */ fisi retorna 0; fifunci
32
Observem que cal passar largument inici per referncia, ja que pot quedar modificat si la dada referenciada s la que estava en primer lloc.
funci inserirDesprs (inici:^node; element: T, referncia: T*) retorna natural s /* Arguments: inici: Punter que apunta linici de la llista element: Dada a afegir a la llista referncia: Dada de tipus T* a cercar per inserir, desprs d'aquesta, lelement Retorna: 0 : Operaci correcta 1 : Error per manca de memria 2 : No es troba la referncia */ var pref, aux:^node; fivar pref = inici; mentre pref != NUL i no coincideix (pref^.inf, referncia) fer pref = pref^.seg; fimentre si pref == NUL llavors retorna 2; fisi assignar_memria (aux); si aux == NUL llavors retorna 1; fisi aux^.inf = element; aux.seg = pref^.seg; pref^.seg = aux; retorna 0; fifunci
Observem que no cal passar largument inici per referncia, ja que mai no pot resultar modificat per un inserirDesprs. 7) Esborrar el primer element Quan es parla desborrar un element duna llista (sigui el primer, el darrer o un dintermedi), podem considerar la funci que noms esborra o la funci que esborra i retorna lelement eliminat. De fet, aquesta segona opci s molt ms interessant i no suposa cap cost de programaci. En les operacions desborrament que presentem considerarem un argument per a recuperar lelement esborrat.
funci esborrarPrimer (var inici:^node, var element: T) retorna natural s /* Arguments: inici: Punter que apunta linici de la llista element: Variable on sha de recollir lelement esborrat Retorna: 0 : Operaci correcta 1 : Error ja que la llista s buida */ var aux:^node; fivar si inici == NUL llavors retorna 1; fisi aux = inici^.seg; element = inici^.inf; alliberar_memria (inici); inici = aux; retorna 0; fifunci
funci esborrar_darrer (var inici:^node, var element: T) retorna natural s /* Arguments: inici: Punter que apunta linici de la llista
33
element: Variable on sha de recollir lelement esborrat Retorna: 0 : Operaci correcta 1 : Error ja que la llista s buida */ var ult, ant: ^node; fivar si inici == NUL llavors retorna 1; fisi ult = inici; ant = NUL; mentre ult^.seg != NUL fer ant = ult; ult = ult^.seg; fimentre si ult == inici llavors inici = NUL; sin ant^.seg = NUL; fisi element = ult^.inf; alliberar_memria (ult); retorna 0; fifunci
9) Esborrar un element Ens trobem en la mateixa situaci que en lalgorisme corresponent a inserir un element. Cal tenir una referncia per saber quin element (un en concret, el dabans o el de desprs) sha desborrar. Tindrem en compte els mateixos supsits daquella situaci.
funci esborrarElement (var inici:^node; var element: T, referncia: T*) retorna natural s /* Arguments: inici: Punter que apunta linici de la llista element: Variable on sha de recollir lelement esborrat referncia: Dada de tipus T* corresponent a lelement a esborrar Retorna: 0 : Operaci correcta 1 : No es troba la referncia */ var pref, ant: ^node; fivar pref = inici; ant = NUL; mentre pref != NUL i no coincideix (pref^.inf, referncia) fer ant = pref; pref = pref^.seg; fimentre si pref == NUL llavors retorna 1; fisi si pref == inici llavors inici = pref^.seg; /* lelement referncia era el primer de la llista */ sin ant^.seg = pref^.seg; /* lelement referncia no era el primer de la llista */ fisi element = pref^.inf; alliberar_memria (pref); retorna 0; fifunci funci esborrarAnterior (var inici:^node; var element: T, referncia: T*) retorna natural s /* Arguments: inici: Punter que apunta linici de la llista element: Variable on sha de recollir lelement esborrat referncia: Dada de tipus T* a cercar per esborrar lelement anterior Retorna: 0 : Operaci correcta 1 : No es troba la referncia 2 : No existeix un element anterior a la referncia */ var pref, ant1, ant2: ^node; fivar pref = inici; ant1 = NUL; ant2 = NUL; mentre pref != NUL i no coincideix (pref^.inf, referncia) fer ant2 = ant1; ant1 = pref; pref = pref^.seg;
34
fimentre si pref == NUL llavors retorna 1; fisi si ant1 == NUL llavors retorna 2; fisi si ant1 == inici llavors inici = pref; /* lelement a esborrar era el primer de la llista */ sin ant2^.seg = pref; /* lelement a esborrar no era el primer de la llista */ fisi element = ant1^.inf; alliberar_memria (ant1); retorna 0; fifunci funci esborrarSegent (inici:^node; var element: T, referncia: T*) retorna natural s /* Arguments: inici: Punter que apunta linici de la llista element: Variable on sha de recollir lelement esborrat referncia: Dada de tipus T* a cercar per esborrar lelement posterior Retorna: 0 : Operaci correcta 1 : No es troba la referncia 2 : No existeix un element posterior a la referncia */ var pref, aux:^node; fivar pref = inici; mentre pref != NUL i no coincideix (pref^.inf, referncia) fer pref = pref^.seg; fimentre si pref == NUL llavors retorna 1; fisi si pref^.seg == NUL llavors retorna 2; fisi aux = pref^.seg; pref^.seg = aux^.seg; element = aux^.inf; alliberar_memria (aux); retorna 0; fifunci
Observem que no cal passar largument inici per referncia, ja que mai no pot resultar modificat per un esborrarSegent. 10)Eliminar la llista s molt important tenir un algorisme que ens elimini tota una llista. Aquest algorisme lexecutarem quan ja no ens interessi tenir la llista dins una execuci i, sobretot, en finalitzar lexecuci dun programa per eliminar totes les llistes creades dinmicament. Lalgorisme s, evidentment, molt senzill. Simplement cal fer un recorregut per tots els nodes i alliberar la memria assignada corresponent.
acci eliminar_llista (var inici:^node) s /* Argument inici: Punter que apunta linici de la llista */ var aux: ^node; fivar mentre inici != NUL fer aux = inici^.seg; alliberar_memria (inici); inici = aux; fimentre fiacci
Fixeu-vos que en sortir de lacci, el punter inici t el valor NUL, com s lgic.
35
funci recerca (inici:^node; var element: T; referncia: T*) retorna natural s /* Arguments: inici: Punter que apunta linici de la llista element: Variable retornada plena amb el valor trobat, si existeix referncia: Dada de tipus T* corresponent a lelement a cercar Retorna: 0 : Recerca efectuada amb xit. 1 : Recerca efectuada sense xit. */ mentre inici != NUL i s_menor (inici^.inf, referncia) fer inici = inici^.seg; fimentre si inici == NUL o s_major (inici^.inf, referncia) llavors retorna 1; fisi element = inici^.inf; retorna 0; fifunci funci s_menor (element: T, referncia: T*) retorna boole s /* Codi que comprovi si la part T* delement s ms petita que referncia */ fifunci funci s_major (element: T, referncia: T*) retorna boole s /* Codi que comprovi si la part T* delement s ms gran que referncia */ fifunci
36
2) Inserir un element
funci inserir (var inici:^node; element: T) retorna natural s /* Arguments: inici: Punter que apunta linici de la llista ordenada element: Dada a inserir a la llista Retorna: 0 : Operaci correcta 1 : Error per manca de memria 2 : Ja existeix un element amb el mateix identificador */ var seg, ant, aux:^node; fivar seg = inici; ant = NUL; mentre seg != NUL i no s_menor* (seg^.inf, element) fer ant = seg; seg = seg^.seg; fimentre si seg == NUL o s_major* (seg^.inf, element) llavors assignar_memria (aux); si aux == NUL llavors retorna 1; fisi aux^.inf = element; aux^.seg = seg; si ant == NUL llavors inici = aux; /* lelement sinsereix a linici de la llista */ sin ant^.seg = aux; fisi retorna 0; sin retorna 2; fisi fifunci funci s_menor* (ele1: T, ele2: T) retorna boole s /* Codi que comprovi si la part T* dele1 s ms petita que la dele2 */ fifunci funci s_major* (ele1: T, ele2: T) retorna boole s /* Codi que comprovi si la part T* dele1 s ms gran que la dele2 */ fifunci
3) Esborrar un element
funci esborrar (var inici:^node; var element: T; referncia: T*) retorna natural s /* Arguments: inici: Punter que apunta linici de la llista element: Variable retornada plena amb lelement a esborrar, si existeix referncia: Dada de tipus T* corresponent a lelement a esborrar Retorna: 0 : Esborrament efectuat amb xit. 1 : Esborrament no efectuat perqu no sha trobat lelement. */ var pref, ant: ^node; fivar pref = inici; ant = NUL; mentre pref != NUL i s_ms petit (pref^.inf, referncia) fer ant = pref; pref = pref^.seg; fimentre si pref == NUL o s_ms gran (pref^.inf, referncia) llavors retorna 1; fisi si pref == inici llavors inici = pref^.seg; /* lelement esborrat s linicial */ sin ant^.seg = pref^. seg fisi element = pref^.inf; alliberar_memria (pref); retorna 0; fifunci
37
38
Per accedir a una llista doblement encadenada tenim dos punters, inici i final. Com que el primer element de la llista no t un element anterior, el seu punter anterior cont el valor NUL. De la mateixa manera, com que el darrer element de la llista no t un element segent, el seu punter segent cont el valor NUL. Les operacions en llistes doblement encadenades sn similars a les operacions de les llistes simplement encadenades, per cal tenir en compte que hi ha els dos punters per accedir a la llista, els dos punters de cada node i que el recorregut es pot fer en tots dos sentits. Deixem per a vosaltres el disseny dels algorismes corresponents. Tamb es pot considerar el cas de llistes doblement encadenades circulars, de manera que el darrer node apunta el primer amb el punter segent i el primer node apunta el darrer amb el punter anterior. No necessitem, per, dos punters per accedir a la llista, amb un nhi ha prou. En aquest cas, ens preguntem quina s la millor elecci com a nic punter daccs en una llista doblement encadenada circular: ha dapuntar el primer o el darrer element de la llista? Qu en penseu?
39
3. Piles i cues
Passem ara a estudiar dos tipus de dades fonamentals en informtica: les piles i les cues. En la majoria dels nostres programes les hem estat utilitzant de manera implcita, sense ser-ne conscients. Les piles sn, com el seu nom indica, recipients on emmagatzemar dades apilades i, com en una pila de qualsevol tipus delements (plats, llibres...), les niques operacions que es poden fer sn: posar un element damunt la pila i treure lelement del damunt (darrer element posat a la pila). Les cues sn, com el seu nom indica, recipients on semmagatzemen dades en lordre en qu van arribant i, com en una cua qualsevol (de persones a lentrada dun cinema, de cotxes en una carretera...), les niques operacions que es poden fer sn: posar un element al final de la cua i treure lelement de linici de la cua (lelement que fa ms temps que s a la cua). Per a cadascun daquests tipus en veurem, en primer lloc, la definici i la implementaci amb la utilitzaci dels tipus de dades coneguts; posteriorment, en presentarem diferents camps daplicaci.
3.1. Piles
Una pila s un conjunt lineal (ja que els seus components ocupen llocs successius dins el conjunt) delements en el qual es poden inserir o eliminar elements noms per un dels dos extrems. En conseqncia, els elements duna pila seran eliminats en ordre invers al qual es van inserir.
A causa del funcionament dinserci i eliminaci, les piles es coneixen com a estructures LIFO (last in - first out), ja que el darrer element que va ser inserit (last in) s el primer a ser eliminat (first out). La noci de pila ens s familiar: pila de plats, de llibres... Per afegir un plat es colloca dalt de la pila; per agafar-lo es pren de la part superior de la pila. Hi ha un concepte angls molt utilitzat en la gesti de piles: top. Aquesta paraula indica al capdamunt de la pila i, evidentment, la gesti de piles treballa sempre amb lelement top de la pila.
40
Amb les piles noms es poden efectuar dues operacions: posar (molt ests el terme angls push) i treure (en angls pop). ! Hi ha autors que permeten una tercera operaci: capdamunt (en angls, top), pensada per consultar el valor que hi ha en el top de la pila. De fet, cada vegada que els autors volen consultar quin s lelement que hi ha al capdamunt de la pila, aquells que no tenen en compte aquesta operaci han defectuar un treure per veure lelement i un posar per tornar a posar lelement a la pila. Nosaltres permetrem utilitzar aquesta tercera operaci. Tornem-ho a repetir. Tot i que les implementacions de pila ho permetin, el concepte de tipus de dada pila porta implcit que no shi pugui fer altra cosa que les dues (o tres) operacions esmentades, s a dir, no es pot realitzar un recorregut per una pila, no es pot efectuar una recerca per una pila, no es pot treure lelement de sota (ja que no shi t accs)... !
41
el cas de llenguatges que no permeten taules dinmiques. Tot i aix, coneixem els problemes que t el llenguatge C per augmentar i/o disminuir la grandria duna taula dinmica i el problema de desaprofitament despai quan la pila no fos plena. A vegades es pot intentar compartir espai de memria per minorar tant com es pugui el problema presentat despai de memria fix i esttic. Aquesta gesti s possible quan cal tractar amb dues piles del mateix tipus delement de grandries M+1 i N+1. Podem decidir gestionar-les amb una nica taula de grandria M+N+2. La figura 9 ens en mostra el muntatge.
Figura 9. Implementaci de dues piles mitjanant una taula
Si en algun punt del procs la pila1 necessita ms de M+1 posicions i en aquell moment la pila2 no t ocupades N+1 posicions, aquesta pot cedir espai a laltra. b) Mitjanant llistes simplement encadenades ! La figura 10 ens en mostra una exemplificaci.
Figura 10. Implementaci de pila mitjanant llista simplement encadenada
En aquest cas, es tracta de crear una llista simplement encadenada on lelement top s el punter a linici de la pila. Llavors, les dues operacions sobre piles consisteixen en dues de les operacions sobre llistes simplement encadenades: Posar un element a la pila (push) equival a afegir un element per linici de la llista (per la zona OP de la figura 10). Recuperar un element de la pila (pop) equival a esborrar un element per linici de la llista (per la zona OP de la figura 10) i recollir lelement eliminat de la pila via parmetre per referncia.
42
A ms, s interessant tenir una operaci per eliminar la pila, la qual equival a loperaci corresponent per eliminar una llista. En aquesta implementaci lnica limitaci es troba en la quantitat de memria disponible. La pila pot crixer i disminuir segons les necessitats. Per tant, s la millor opci.
43
Evidentment, el resultat ha de ser 25. Quin raonament segueix lsser hum per obtenir aquest resultat? En primer lloc, fent una rpida ullada, la nostra capacitat visual ens permet captar lexpressi globalment (ja que s molt curta) i la nostra capacitat intellectual ens permet deduir amb rapidesa que primer cal efectuar el producte i posteriorment la suma, ja que matemticament un producte t prioritat davant una suma. Ara b, i si us demanem que em calculeu el resultat duna expressi com 3 ^ 2 * log 25 43 div 4 / 25 + 2 * 43 mod 21? Ens sembla que no sereu tan rpids com abans i efectuareu una avaluaci acurada de la prioritat dels operadors. Lordinador actua sempre de la mateixa manera; tant li fa si lexpressi s curta o si s llarga. Per resoldre lavaluaci es pot treballar amb dues piles: en la primera semmagatzemen els valors dels operands i de les avaluacions parcials i en la segona semmagatzemen els operadors pendents dexecuci. Fem-ne el seguiment amb lexpressi 10 + 2 3 * 5 + 4. Considerarem que Val (...) s la pila que cont els valors i que Ope (...) s la pila que cont els operadors. Partim de la situaci inicial en qu les dues piles sn buides: Val () i Ope (). Recepci del valor 10. No es pot fer altra cosa que apilar-lo a la pila de valors. Val (10) Ope()
Recepci de loperador +. No es pot fer altra cosa que apilar-lo a la pila doperadors i esperar el valor segent. Val (10) Ope (+)
Recepci del valor 2. No es pot fer altra cosa que apilar-lo a la pila de valors i esperar loperador segent, ja que si s ms prioritari que loperador que hi ha actualment en el top de la pila Ope no es pot operar amb el valor que hi ha actualment en el top de la pila Val. Val (10,2) Ope (+)
Recepci de loperador -. Com que el top de la pila Ope t un smbol +, que t la mateixa prioritat que el smbol que est arribant, estem en condicions dexecutar loperaci pendent + amb els valors existents en la pila Val. Per aquest motiu desapilem dos valors de la pila
44
Val (2 i 10) i un operador de la pila Ope (+), fem loperaci corresponent (10 + 2) i emmagatzemem el resultat (12) en la pila Val. Si analitzem el top de la pila Ope, observem que no hi ha cap execuci pendent possible. Cal apilar el nou operador en la pila de valors i esperar el valor segent. Val (12) Ope (-)
Recepci del valor 3. No es pot fer altra cosa que apilar-lo en la pila de valors i esperar loperador segent, ja que si s ms prioritari que loperador que hi ha actualment en el top de la pila Ope, no es pot operar amb el valor que hi ha actualment en el top de la pila Val. Val (12,3) Ope (-)
Recepci de loperador *. Com que el top de la pila Ope t un smbol , que t menys prioritat que el smbol que est arribant, sha demmagatzemar loperador entrant i executar-lo quan es pugui, per sempre abans que lactual top de la pila Ope. Val (12,3) Ope (-,*)
Recepci del valor 5. No es pot fer altra cosa que apilar-lo en la pila de valors i esperar loperador segent, ja que si s ms prioritari que loperador que hi ha actualment en el top de la pila Ope, no es pot operar amb el valor que hi ha actualment en el top de la pila Val. Val (12,3,5) Ope (-,*)
Recepci de loperador +. Com que el top de la pila Ope t un smbol *, que t ms prioritat que el smbol que est arribant, estem en condicions dexecutar loperaci pendent * amb els valors existents en la pila Val. Per aquest motiu, desapilem dos valors de la pila Val (5 i 3) i un operador de la pila Ope (*), fem loperaci corresponent (3 * 5) i emmagatzemem el resultat (15) en la pila Val. Ens queda aquesta situaci: Val (12,15) Ope (-)
Encara ara, el top de la pila Ope t un smbol que t la mateixa prioritat que el smbol + que est arribant i, per tant, estem en condicions dexecutar loperaci pendent amb els valors existents en la pila Val. Per aquest motiu, desapilem dos valors de la pila Val (15 i 12) i un operador de la pila Ope (), fem loperaci corresponent (12 15) i emmagatzemem el resultat (3) en la pila Val. Si analitzem el top de la pila Ope, observem que no hi ha cap execuci pendent possible. Cal apilar el nou operador + en la pila de valors i esperar el valor segent. Val (-3) Ope (+)
45
Recepci del valor 4. Com que detectem que s el darrer element de lexpressi, cal operar-lo a travs de loperador existent en el top de la pila Ope amb el valor existent en el top de la pila Val. Per aix desapilem loperador (+) de la pila Ope i loperand (3) de la pila Val, efectuem la darrera operaci (3 + 4) i obtenim el resultat (1) final de lexpressi inicial. Val () Ope ()
Evidentment, abans defectuar lavaluaci en temps dexecuci, el compilador haur efectuat una anlisi sintctica de lexpressi en temps de compilaci per garantir que en temps dexecuci no hi haur problemes sintctics. El procs davaluaci del compilador tamb utilitza piles. En el procs anterior comentvem, a cada pas, que mirvem el valor del top de les piles. Aix noms ho podrem fer si disposem de loperaci capdamunt. En cas que no es faciliti aquesta operaci, no t sentit executar les operacions dapilar i desapilar per consultar el valor del top. En aquest cas, podrem mantenir en altres variables els valors dels tops de les piles i desapilar noms quan fos estrictament necessari per efectuar els clculs corresponents. d) Algorisme dordenaci rpida El mtode dordenaci rpida, conegut com a quicksort, s un mtode fcilment programable utilitzant recursivitat. Per tant, es pot tenir una versi no recursiva amb la utilitzaci de piles.
3.2. Cues
Una cua s un conjunt lineal (ja que els seus components ocupen llocs successius dins el conjunt) delements en el qual aquests sintrodueixen per un extrem i seliminen per laltre. En conseqncia, els elements duna cua seran eliminats en el mateix ordre en qu es van inserir.
A causa del funcionament dinserci i eliminaci, les cues es coneixen com a estructures FIFO (first in - first out), ja que el primer element que va ser inserit (first in) s el primer a ser eliminat (first out). La noci de cua ens s familiar: cua en una taquilla dun cinema, en un taulell duna botiga... Per obtenir una entrada ens colloquem al final de la cua
46
i esperem arribar a linici. Quan ens donen lentrada, abandonem la cua. En aquest punt cal deixar clar que entenem que hi pugui haver persones que pensin que linici de la cua s lextrem on elles se situen quan sincorporen a la cua i que el final sigui lextrem pel qual abandonen la cua. No cal discutir. Aqu, considerarem que lextrem final de la cua s aquell pel qual un element sincorpora a la cua i lextrem inici s aquell pel qual un element abandona la cua. Dacord? Amb les cues solament es poden efectuar dues operacions: encuar i desencuar. ! Hi ha autors que permeten dues operacions ms: inici, pensada per consultar lelement que s a punt dabandonar la cua (lelement que es troba a linici de la cua) i final, pensada per consultar el darrer element que ha arribat a la cua (lelement que es troba al final de la cua). Nosaltres permetrem la utilitzaci daquestes dues operacions. Tornem-ho a repetir. Tot i que les implementacions de cua ho permetin, el concepte de tipus de dada cua porta implcit que no shi pugui fer altra cosa que les dues (o quatre) operacions esmentades, s a dir, no es pot realitzar un recorregut per una cua, no es pot efectuar una recerca per una cua, no es pot treure lelement del final... !
47
sici final. Notem-ho [0,...,final]. s el cas que sobserva en la figura 11. Si inici>1, llavors tenim dues possibilitats respecte al valor de final: Si final>=inici, llavors la cua est ocupant el tros de taula que va des de la posici inici fins a la posici final. s a dir [inici,...,final]. s el cas exemplificat en la situaci 1 de la figura 12. Si final<inici, llavors la cua est ocupant dos trossos de taula: el que va des de la posici inici fins a la posici M i el que va des de la posici 0 fins a la posici final. O sia, [inici,...,M] U [0,...,final]. s el cas exemplificat en la situaci 2 de la figura 12. a) Mitjanant llistes simplement encadenades ! En aquest cas, es tracta de crear una llista simplement encadenada, per amb un segon punter que apunti el darrer element de la llista. La figura 13 ens ho exemplifica. Les dues operacions sobre cues consisteixen en dues de les operacions sobre llistes simplement encadenades: Encuar un element s equivalent a afegir un element pel final de la llista (zona F de la figura 13). Aquesta operaci podr ser codificada de manera molt ms eficient que en les llistes simplement encadenades amb un nic punter a linici (abans calia fer el recorregut de tota la llista per arribar al final) grcies a laparici del punter al node final. Desencuar un element s equivalent a esborrar un element per linici de la llista (per la zona I de la figura 13) i recollir lelement eliminat de la cua via parmetre per referncia. A ms, s interessant tenir una operaci per eliminar la cua, la qual s equivalent a loperaci corresponent per eliminar una llista.
Figura 13. Implementaci duna cua mitjanant una llista simplement encadenada i 2 punters Figura 12. Estat duna cua, implementada mitjanant una taula, al llarg del temps
En aquesta implementaci lnica limitaci es troba en la quantitat de memria disponible. La cua pot crixer i disminuir segons les necessitats. Per tant, s la millor opci.
48
Com que les dues operacions no seran exactament iguals que en el tractament de llistes simplement encadenades a causa de laparici del segon punter, en presentarem el disseny.
funci encuar (var inici:^node, var final:^node, element:T) retorna natural s /* Arguments: inici: Punter que apunta linici de la cua /* per on es desencua */ final: Punter que apunta el final de la cua /* per on sencua */ element: Dada a afegir a la cua Retorna: 0 : Operaci correcta 1 : Error per manca de memria */ var aux:^node; fivar assignar_memria (aux); si aux == NUL llavors retorna 1; fisi aux^.inf = element; aux^.seg = NUL; si final == NUL llavors inici = aux; final = aux; /* la cua era buida */ sin final^.seg = aux; final = aux; /* la cua no era buida */ fisi retorna 0; fifunci
Observeu que, tot i que lencuament es produeix pel final de la cua, cal passar tots dos punters, inici i final, per referncia. El punter final sempre queda modificat desprs duna execuci de la funci encuar excepte si es produeix un error per manca de memria. En canvi, el punter inici quedar modificat desprs duna execuci de la funci encuar en cas que la cua sigui buida.
funci desencuar (var inici:^node, var final:^node, var element:T) retorna natural s /* Arguments: inici: Punter que apunta linici de la cua /* per on es desencua */ final: Punter que apunta el final de la cua /* per on sencua */ element: Variable on recollir la dada desencuada Retorna: 0 : Operaci correcta 1 : Error perqu la cua s buida */ var aux:^node; fivar si inici == NUL llavors retorna 1; fisi aux = inici; element = inici^.inf; inici = inici^.seg; si inici == NUL llavors final = NUL; fisi alliberar_memria (aux); retorna 0; fifunci
Observeu que, tot i que el desencuament es produeix per linici de la cua, cal passar tots dos punters, inici i final, per referncia. El punter inici sempre queda modificat desprs duna execuci de la funci desencuar excepte si la cua era buida. En canvi, el punter final quedar modificat desprs duna execuci de la funci desencuar en cas que la cua noms tingui un element. Llavors es convertir en una cua buida.
49
Podrem considerar el tipus cua que contingus els dos punters de la forma segent:
tipus cua: tuple inici, final: ^node; fituple
En aquest cas, les dues funcions per a la gesti de cues tindrien les declaracions segents:
funci encuar (var c: cua, element:T) retorna natural s funci desencuar (var c: cua, var element:T) retorna natural s
Evidentment, cal adequar la definici de les dues funcions als nous arguments. Aquesta manera de fer t un avantatge: largument c porta incorporats els dos punters a linici i al final de la cua. Daquesta manera, si en un programa es treballa amb diverses cues, no hi ha possibilitat dequivocar-se en el pas dels parmetres ni es poden estar barrejant punters a diferents cues.
50