Você está na página 1de 14

Captulo 4 Representacao de listas em C

At aqui temos representado listas como vectores com um incio e um m. Esta representacao e foi util para a programacao imperativa e recursiva, mas agora pretendemos abordar a quest o a do desenvolvimento de funcoes cujo resultado seja uma lista. A prop sito deste objectivo ser o o a introduzidas outras representacoes de listas. Para j h que introduzir alguns conceitos prelimi a a nares, nomeadamente os de apontador e atribuicao din mica de mem ria. a o

4.1 Preliminares
4.1.1 Apontadores
Cada vari vel refere determinada posicao em mem ria onde se encontra armazenado o valor a o associado a essa vari vel. Uma atribuicao de um novo valor a uma vari vel corresponde a a a substituir o conte do dessa posicao de mem ria. A refer ncia a uma vari vel (numa express o) u o e a a implica a leitura do valor que lhe est associado e que se encontra armazenado nessa posicao de a mem ria. o Uma forma alternativa de aceder aos valores associados a uma vari vel e a utilizacao do endereco a da posicao de mem ria correspondente (que e um valor que identica a posicao de mem ria cor o o respondente). Seja x uma vari vel cujo valor se encontra na posicao de mem ria identicada a o pelo endereco px . Atribuir um valor a x corresponde a alterar o conte do de px . A leitura do u valor de x corresponde a leitura do conte do de px . Assim, por exemplo, a atribuicao x = x + 1; ` u implica a leitura do valor associado a x e, depois, associar a x o resultado de x + 1. Em termos da posicao de mem ria identicada por px , a mesma atribuicao signica leitura do conte do de o u px e actualizacao deste conte do com o novo valor. u Os enderecos de mem ria ser o na sequ ncia designados por apontadores pois referenciam o a e (apontam) para uma determinada posicao da mem ria. o Em C, existem apontadores de tipos diversos, consoante o tipo do valor para que apontam (existir o, portanto, apontadores para inteiros, para reais, para outros apontadores, etc.) sendo a possvel declarar vari veis do tipo apontador, que conter o este novo tipo de dados. a a Os operadores associados a apontadores s o os seguintes: a) o operador & que, quando aplicaa 49

do a uma vari vel, devolve o respectivo endereco (apontador) e b) o operador * que refere o a conte do de um determinado apontador. u Assim a declaracao int x,*px; declara duas vari veis: a vari vel x do tipo inteiro e a a a vari vel px cujo conte do e do tipo inteiro. Isto signica que px e um apontador para valores a u do tipo inteiro. A atribuicao px=&x; coloca em px o valor do apontador para (a posicao de mem ria associada a) x. Se agora se efectuar a atribuicao *px=*px+1; obt m-se exactamente o e o mesmo resultado que com a atribuicao x=x+1;, como se compreende facilmente do que foi dito atr s. a Nota-se ainda que existe um valor (NULL) que corresponde ao apontador indenido. A utilizacao de apontadores e muito importante na passagem de informacao para funcoes ou procedimentos por permitir alterar directamente os valores de vari veis do contexto, como se a ver na seccao seguinte. a

4.1.2 Apontadores e passagem de par metros a


A passagem de informacao para uma funcao (` excepcao do caso n o standard em C das va a a ri veis herdadas) e feita exclusivamento por valor. Isto signica que o valor dos argumentos e a determinado no estado anterior a invocacao da funcao e esses valores s o atribudos as vari veis ` a ` a locais argumentos, que desaparecem no m da invocacao da funcao 1 . O exemplo seguinte e cl ssico e ilustra (pela negativa) a passagem de par metros por valor, que implica n o se alteraa a a rem os valores das vari veis do contexto (n o locais). a a A intencao (gorada) e construir um procedimento que troque os valores de duas vari veis argu a mento. Eis a primeira vers o (incorrecta) do procedimento a

void troca(a,b) int a,b; int aux; aux=a; a=b; b=aux;


que pode ser invocada como se segue:

1 Se

as vari aveis n forem do tipo est (ver a secc B.5 do ap ao atico ao endice B).

50

N o e difcil vericar que a invocacao deste procedimento em nada altera os valores das vaa ri veis argumento: embora as vari veis locais tenham trocado de valor, as vari veis x ey pera a a maneceram com os valores que tinham, como seria de esperar por a passagem de par metros a ser por valor. No entanto, a utilizacao de apontadores como argumento de procedimentos ou de funcoes per mite alterar directamente os conte dos desses enderecos. Isto signica que os valores das vau ri veis do contexto podem ser alterados por procedimentos que tenham acesso ao endereco a (apontador) correspondente. O procedimento seguinte troca, de facto, os valores das vari veis a do contexto porque recebe como argumentos os apontadores para essas vari veis. a

void troca(pa,pb) int *pa,*pb; int aux; aux=*pa; *pa=*pb; *pb=aux;


Neste caso h que invocar o procedimento troca com os enderecos das vari veis relevantes: a a

4.1.3 Vectores e apontadores


O nome associado a um vector e, simultaneamente, o apontador para a primeira posicao do vector: dada a declaracao int a[10]; tem-se que a e o mesmo que &a[0]. Ou seja, a e um apontador para valores do tipo inteiro e, em particular, aponta para a primeira posicao 51

main() int x,y; x=0; y=1; troca(&x,&y); printf("O valor de x %d e o valor de y %dn",x,y); e e

main() int x,y; x=0; y=1; troca(x,y); printf("O valor de x %d e o valor de y %dn",x,y); e e

do vector. A declaracao int b[]; pode ser usada com o mesmo signicado de int *b; armando que b e um apontador para um valor do tipo interio. Desta forma, a passagem de vectores pelo seu nome como argumento de funcoes corresponde a ` passagem do apontador para a primeira posicao o que permite alterar o vector argumento. Este facto j foi utilizado atr s a prop sito da ordenacao. a a o

4.1.4 Aritm tica de apontadores e


Pode ser util a utilizacao de aritm tica de apontadores, no contexto de um vector de compri e mento conhecido. Dado int a[10]; sabemos que a e o apontador para a primeira posicao do vector. Neste caso a+1 e o apontador para a segunda posicao (ou seja, a+1 coincide com &a[1];). Subtraccao e comparacao (<, >, <=, >=, ==) de apontadores s o tamb m possveis. a e

4.1.5 Atribuicao din mica de mem ria a o


As declaracoes de vari veis globais indicam ao compilador que e necess rio atribuir em mem ria a a o o espaco necess rio a essa vari vel. Esta atribuicao e denida na compilacao e efectuada logo a a que corre o programa. Quanto as vari veis locais, denidas no corpo de funcoes e procedimen` a tos, o processo e semelhante: na compilacao e denido, para cada funcao ou procedimento, o espaco necess rio as vari veis locais correspondentes. No entanto, a atribuicao de mem ria as a ` a o ` vari veis locais de uma funcao ou procedimento e efectuada em cada invocacao da funcao ou a procedimento em quest o. Em geral, o espaco correspondente as vari veis locais e libertado no a ` a 2 nal da invocacao correspondente . Desta forma, existe um mecanismo de atribuicao e libertacao din mica de mem ria, o qual a o pode ser tamb m utilizado directamente, quando conveniente. Veremos diversos exemplos de e utilizacao, em particular quando se pretende criar uma lista (ou vector) de comprimento desco nhecido a partida. ` A atribuicao din mica de mem ria, em C, e efectuada utilizando o procedimento malloc que, a o tendo por argumento o espaco solicitado, devolve um apontador para a primeira posicao desse espaco atribudo din micamente. a Para tal h que saber qual o espaco necess rio (a uma vari vel) do tipo em quest o. A funcao a a a a sizeof, dado um tipo, devolve o espaco necess rio a uma vari vel desse tipo. Assim, mal a a loc(10*sizeof(int)) devolve um apontador para o espaco3 correspondente a 10 va ri veis do tipo inteiro, armazenadas consecutivamente (ou seja, um vector de inteiros com 10 a posicoes). Falta referir que o tipo do resultado da funcao malloc dever ser correctamente adaptado a ao tipo para o qual se est a atribuir mem ria. Para tal usa-se o cast, como no exemplo a o que se segue: a=(int *)malloc(10*sizeof(int));. O valor de a e um aponta dor (do tipo int *) para a primeira posicao das 10 consecutivas que foram agora atribudas din micamente. Desta forma (` parte o comportamento din mico) esta atribuicao e equivalente a a a
2 Se 3O

as vari aveis n forem do tipo est ao atico. resultado de malloc(0) Null. e

52

a declaracao int a[10] que dene um vector de inteiros com 10 posicoes (recorde que o ` vector a e, tamb m, o apontador para a primeira posicao do vector). e Nota-se ainda que a funcao free liberta o espaco previamente atribudo ao apontador argu mento.

4.2 Listas representadas por vectores


A quest o que nos interessa resolver e a de denir funcoes com resultado em listas. Vamos a comecar por resolver esta quest o utilizando apontadores. At aqui temos representado listas a e por vectores com um incio e um m. Seguindo esta representacao deveremos desenvolver procedimentos que indiquem qual o vector resultante, qual o seu incio e qual o seu m. Dependendo do problema em quest o a informacao do incio e do m do vector correspondente a pode ser redundante. Por exemplo, no caso da ordenacao, o vector resultante tem incio e m nas mesmas posicoes que o vector argumento, pelo que esta informacao e redundante. Por esta raz o o procedimento ordena desenvolvido atr s n o informa dos valores do incio e m do a a a vector resultante (no captulo ??), como se depreende do cabecalho que se recorda: void ordena(int incio, flot a[], int fim) O resultado deste procedimento e ordenar o vector argumento, o que e conseguido alterando os valores nas posicoes do vector argumento. O facto de ser passado a este procedimento o nome desse vector (ou seja o endereco da sua primeira posicao) permite a modicacao destes valores. N o h necessidade de informar do novo incio e m. a a Se o procedimento em quest o construir um novo vector j a informacao do incio e m do novo a a vector e necess ria. No entanto, o vector assim construdo tem, geralmente, incio na posicao a 0, pelo que uma representacao alternativa e indicar qual o vector e qual o seu comprimento (assumindo-se que comeca em 0). Nos exemplos que se seguem (de programacao recursiva) utilizaremos a representacao que pareca mais razo vel em cada caso. a

4.2.1 Seleciona positivos


O primeiro exemplo e o da funcao selPos que, dada uma lista de reais, devolve a lista obtida daquela ao selecionar apenas os elementos positivos. Vamos considerar que o vector construdo comeca em 0 pelo que basta informar qual o novo vector e qual o seu comprimento. Quanto ao vector vamos identic -lo com o apontador para a primeira posicao e este ser o a a resultado desta funcao. O comprimento e armazenado numa vari vel cujo apontador e passado a como argumento para esta funcao. O cabecalho desta funcao e o seguinte:
float *selPos (int incio, float a[], int fim, int *apcomprimento)

53

A declaracao float *selPos signica que o resultado desta funcao ser um apontador pa a ra um real ou, o que e id ntico, um vector de reais. Quanto a apcomprimento e um e apontador para inteiro (onde estar o valor do comprimento do vector correspondente). a Pretende-se que execucao de b=selPos(0,v,99,&comp); construa um vector que ser a atribudo a b (com elementos b[0],b[1], ,b[comp-1] com comprimento armazenado em comp. Assim este procedimento dever devolver o apontador para a primeira posicao de a um vector que deve conter os elementos positivos de v, entre as posicoes 0 e 99. Vamos desenvolver este procedimento em duas fases: em primeiro lugar apresenta-se um procedimento que copia os positivos de um vector a para outro b, assumindo que b tem o comprimento correcto.
void copiaPos(int incio,float a[],int fim, int inicioR,float b[],int fimR) if(incio<=fim) if (a[incio]>=0) b[inicioR]=a[incio]; copiaPos(incio+1,a,fim,inicioR+1,b,fimR); else copiaPos(incio+1,a,fim,inicioR,b,fimR);

O procedimento nal encarrega-se de determinar o comprimento do vector resultado e de atribuir din micamente a mem ria necess ria. Note-se que o comprimento do vector a construir a o a e o n mero de elementos positivos em a, ou seja np(incio,a,fim) (recorde a funcao u np - n mero de positivos, apresentada no captulo anterior). Se este comprimento for zero o u resultado da atribuicao din mica de mem ria e Null. a o
float *selPos (int incio, float a[], int fim, int *apcomprimento) float resultado[]; *apcomprimento=np(incio,a,fim); resultado =(float *) malloc(*apcomprimento * sizeof(float)); copiaPos(incio,a,fim,0,resultado,*apcomprimento-1); return resultado;

Uma alternativa e informar do novo vector num argumento, em vez do resultado da funcao. A invocacao pretendida ser selPos(0,v,99,&comp,&b). Temos, neste caso: a
void selPos (int incio, float a[], int fim, int *apcomprimento,float **apresultado) *apcomprimento=np(incio,a,fim); apresultado =(float *) malloc(*apcomprimento * sizeof(float)); copiaPos(incio,a,fim,0,*apresultado,*apcomprimento-1);

Note-se que float **apresultado signica que apresultado e um apontador para um apontador para float, ou seja um apontador para um vector de reais.

4.2.2 Um parentesis: aritm tica de apontadores e


No caso dos vectores, e possvel utilizar quer o facto de o nome do vector corresponder ao apontador para o primeiro elemento quer as possibilidades oferecidas pela aritm tica de apone tadores, para uma terceira representacao de listas, que usa dois apontadores, o apontador para o primeiro elemento e o apontador para o ultimo elemento. 54

Com esta representacao a funcao np que, dada uma lista, devolve o n mero de elementos u positivos tem a seguinte express o em C: a
int np(float *apinicial, float *apfinal) return apinicial>apfinal?0: (*apinicial>=0,1+np(apinicial+1,apfinal),np(apinicial+1,apfinal));

A invocacao desta funcao, dado um vector (de reais) v1 com incio e m em inicio1 e fim1, e: n=np(v1+inicio1,v1+fim1);. O procedimento copiaPos passa a ser:
void copiaPos(float *apinicial,float *apfinal, float *apinicialR,float *apfinalR) if(apinicial<=apfinal) if (*apinicial>=0) *apinicialR=*apinicial; copiaPos(apinicial+1,apfinal,apinicialR+1,apfinalR); else copiaPos(apinicial+1,apfinal,apinicialR,apfinalR);

A invocacao deste procedimento com vectores (de reais) v1 e v2 com incio e m em ini cio1 e fim1 e inicio2 e fim2, e: copiaPos(v1+inicio1,v1+fim1,v2+inicio2,v2+fim2); Finalmente, a segunda vers o de selPos e, nesta representacao: a
void selPos (float *apinicial,float *apfinal, int *apcomprimento,float **apresultado) *apcomprimento=np(apinicial,apfinal); apresultado =(float *) malloc(*apcomprimento * sizeof(float)); copiaPos(apinicial,apfinal,*apresultado,*apresultado+*apcomprimento-1);

4.2.3 Merge Sort


Tradicionalmente os algoritmos que utilizam vectores procuram, por raz es de economia de o espaco em mem ria, utilizar o vector argumento tamb m como resultado. Desta forma, os o e algorimos de ordenacao recebem um vector a e o resultado da ordenacao e, tamb m, guardado e em a. Iremos utilizar esta t cnica no que se segue. Como veremos o quicksort utiliza o espaco e original do vector de forma mais inteligente que o merge sort. A estrutura gen rica do merge sort (que iremos alterar), em C, para um vector de reais cae racterizado por um incio e um m, e a seguinte, pressupondo que o resultado da ordenacao se armazena no pr prio vector: o
void ordena(int incio, float a[], int fim) int m; if (incio+1<=fim) m=(incio+fim)/2; ordena(incio,a,m); ordena(m+1,a,fim); combina(incio,a,m, m+1,a,fim, incio,a,fim);

55

Nota-se que, se o vector tiver comprimento inferior ou igual a 1, nada e alterado. Por hip tese o de inducao, depois de ordena(incio,a,m) e de ordena(m+1,a,fim); o vector a est dividido em duas partes (antes e depois de m), ambas ordenadas. O procedimento coma bina utiliza esta informacao para colocar o resultado em a (ou seja, usando a informacao em incio,a,m e m+1,a,fim, coloca-se o resultado da ordenacao em a.) N o existe solucao a (simples) para o procedimento combina, sem guardar temporariamente a informacao dos sub vectores de a noutras vari veis. De facto, ao armazenar em a os elementos por ordem estar-se-ia a simultaneamente a destruir a informacao dos que l estavam, informacao essa que e necess ria a a para a correcta execucao do algorimo. A solucao que se vai apresentar copia os subvectores relevantes para subvectores auxiliares antes de invocar o procedimento combina. Para tal h primeiro que atribuir dinamicamente a mem ria necess ria e, depois, copiar de facto aquea o a les subvectores. Utiliza-se o procedimento copiaVectores seguinte, que pressup e terem o ambos os vectores o mesmo comprimento.
void copiaVectores(int inicio,float v1[], int fim, int inicioR,float v2[],int fimR) if(inicio<=fim) v2[inicioR]=v1[inicio]; copiaVectores(inicio+1,v1,fim,inicioR+1,v2,fimR);

A nova vers o do procedimento ordena copia, como se referiu, os vectores argumento. No nal a esse espaco deixa de ser necess rio, pelo que e libertado. a
void ordena(int incio, float a[], int fim) int m; float esquerda[], direita[]; if (incio+1<=fim) m=(incio+fim)/2; ordena(incio,a,m); ordena(m+1,a,fim); esquerda= (float *) malloc((m-incio+1)*sizeof(float)); direita= (float *) malloc((fim-m)*sizeof(float)); copiaVectores(incio,a,m,0,esquerda,m-incio); copiaVectores(m+1,a,fim,0,direita,fim-m-1); combina(0,esquerda,m,0,direita,fim-m-1,incio,a,fim); free(esquerda);free(direita);

O procedimento combina usa os dois primeiros vectores, supostos ordenados, e coloca o re sultado da ordenacao no terceiro vector. E pressuposto que o terceiro vector tenha comprimento igual a soma dos outros dois. `
void combina (int inicio1, float v1[], int fim1, int inicio2, float v2[], int fim2, int inicioR, float r[], int fimR) if(inicio1<=fim1 && inicio2<=fim2) if(esquerda[inicio1]<direita[inicio2]) r[incio]=esquerda[inicio1]; combina(inicio1+1,v1,fim1,inicio2,v2,fim2,inicioR+1,r,fimR); else r[incio]=direita[inicio2]; combina(inicio1,v1,fim1,inicio2+1,v2,fim2,inicioR+1,r,fimR); else if (inicio1>fim1) copiaVectores(inicio2,v2,fim2,inicioR,a,fimR); else copiaVectores(inicio1,v1,fim1,inicioR,a,fimR);

56

Nos procedimentos apresentados utilizou-se sempre recurs o. Vers es imperativas de combia o na s o tamb m possveis. a e Quicksort O Quicksort com particao do vector original n o necessita da criacao de vectores auxiliares. A a sua traducao para C e, portanto, simples. void quickordena(int incio, float a[], int fim) float x, el; int i,j; if (incio<=fim) j=fim-incio+1; i=incio; x=a[incio]; while(j>=i) while(a[i]<x),i=i+1; while(a[j]>x),j=j-1; if(j>=i) el=a[i];a[i]=a[j];a[j]=el;i=i+1;j=j-1; quickordena(incio,a,j); quickordena(i,a,fim);

Nota-se que, como se est a utilizar o vector argumento, n o e necess rio actuar sobre os elea a a mentos entre j+1 e i-1 que j est o onde devem estar. a a

4.3 Listas representadas por listas ligadas


Uma outra forma de representacao de listas e a utilizacao de estruturas de dados din micas, a normalmente designadas por listas ligadas. Nesta representacao utilizam-se listas cujo primeiro elemento e um valor e o segundo elemento e a lista restante. Por exemplo, em Mathematica, para representar a,b,c utilizar-se-ia a, b, c, . Em C iremos considerar estruturas de dados com duas posicoes (ou melhor, com dois campos) em que o primeiro e o valor e o segundo um apontador para a lista restante. Para tal necessitamos de considerar estruturas de dados que armazenam elementos de tipos diferentes4 , o que em C e conseguido com o tipo struct.

4.3.1 Estruturas
Uma das formas de representar em C tuplos de elementos de tipo diferente e pela utilizacao de estruturas. Em cada aplicacao ser o diferentes quais as estruturas relevantes, pelo que e a providenciado um constructor de tipos. Uma declaracao da forma
tamb um construtor de tipos union que permite a denic de tipos compostos alternativos em ao signicando que uma vari do tipo composto armazena dados que podem ser de um dos tipos componentes (por avel exemplo, ou inteiro ou apontador).
4 Existe

57

cria um novo tipo de estrutura. O nome (opcional) designa o novo tipo. Por exemplo a declaracao struct contribuinte int numeroContribuinte; int idade; char nome[100]; char morada[150]; dene o novo tipo contribuinte que e uma estrutura com tr s campos. A declaracao e contribuinte umContribuinte; indica que a vari vel umContribuinte e do tia po contribuinte. O acesso aos campos desta vari vel e efectuado usando a notacao vaa rivel.campo, como se exemplica: a umContribuinte.idade = umContribuinte.idade +1; atribui ao campo idade da vari vel umContribuinte o valor anterior mais um. a

4.3.2 Abreviaturas de tipos


A declaracao typedef tipo nome1,...nomen; associa nomes alternativos ao tipo. Por exemplo, typedef contribuinte cont,*pcont; declara que os tipos cont e *pcont s o id nticos ao tipo contribuinte. Quanto a *pcont isto signica tamb m que pcont a e e e o tipo dos apontadores para valores do tipo contribuinte.

4.3.3 Listas ligadas


A representacao em C de listas ligadas de inteiros e conseguida utilizando estruturas com dois campos: o primeiro campo cont m o valor e o segundo aponta para a lista restante. A e representacao gr ca seguinte, onde representa NULL, sugere o que se pretende. a

Cada n e uma estrutura do tipo o struct noLista

Quanto a lista ligada ser representada pelo apontador para o primeiro n (com excepcao da ` a o lista vazia que e representada por NULL). Denimos, por conveni ncia, o tipo lLista como e se segue: typedef noLista *lLista;.

58

int valor; noLista *resto;

struct [nome]

tipo1 campo1; tipo2 campo2;

E importante notar que, usando esta representacao, as funcoes com resultado em listas ser o a funcoes cujo resultado e um apontador (para o primeiro n da lista). Como e possvel, em C, o denir funcoes cujo resultado e um apontador, iremos preferir utilizar funcoes em vez de usar procedimentos. Antes dos primeiros exemplos de programacao sobre estas estruturas conv m denir uma abre e viatura conveniente da lista vazia. A lista vazia e representada por (lLista) NULL onde houve o cuidado de atribuir o tipo correcto ao valor NULL. A denicao desta abreviatura e obti da com #define lvazia (lLista) NULL; que deve ser colocada no incio do cheiro contendo o programa. Note-se que a linha anterior n o e um comando ou declaracao de C 5 , mas a informa o compilador de que deve substituir lvazia por (lLista) NULL no c digo C que o ocorre no programa.

4.3.4 Exemplos de programacao


Como primeiro exemplo de programacao considera-se a determinacao do comprimento de uma lista ligada. A funcao seguinte determina o comprimento usando o paradigma imperativo. E de notar que se utiliza a vari vel argumento como vari vel do progresso. a a int comprimento (lLista l) int c; c=0; while (l != lvazia) c=c+1; l=(*l).resto; return c; A vers o recursiva da mesma funcao e a seguinte: a int comprimento (lLista l) return l==lvazia?0:1+comprimento((*l).resto); Acesso a elementos No caso das listas ligadas a operacao natural de acesso a elementos e obter o primeiro elemento da lista: int primeiro (lLista l) return (*l).valor; Na funcao anterior pressup e-se que a lista argumento e n o vazia. o a Insercao Neste exemplo pretende denir-se uma funcao que, dada uma lista (ligada), devolve a lista obtida por acrescentar um valor a lista argumento. E importante notar que a representacao em ` C de funcoes envolvendo valores representados por estruturas din micas exige algum cuidado. a Consideremos, por exemplo, que dispomos da funcao f que, dada uma lista e um elemento, tem
5 E uma

macro do compilador.

59

como resultado a lista correspondente a acrescentar o elemento no incio da lista argumento. Assim f ({1, 2}, 3) e {3, 1, 2}. Se quisermos realizar esta funcao em C, e considerando que a lista argumento se encontra em mem ria no endereco la, a solucao mais econ mica e acrescentar o o um novo n contendo o elemento no campo valor e tal que o campo resto aponta para o a lista argumento (ou seja, e-lhe atribudo la). A lista resultado lb e o apontador para este novo primeiro n . Pode, no entanto, acontecer, por interaccoes diferentes de outras partes do o programa, que a lista la seja alterada (por exemplo, passar a ser {0, 0}). Nesta circunst ncia, a sem que aparentemente o valor em lb tenha sido alterado, este passa a ser a lista {3, 0, 0} em vez de {3, 1, 2}. Uma forma de codicar a funcao em quest o evitando estes efeitos (mas menos econ mica em a o tempo e espaco) e criar uma nova lista, sicamente separada da lista argumento. Haver , natu a ralmente, que copiar a lista argumento, al m de lhe acrescentar um novo primeiro n . Ambas e o as solucoes s o uteis. a A pr xima funcao insere um novo elemento no incio da lista argumento (usando a primeira o solucao). Nota-se que e necess rio reservar o espaco necess rio ao novo n (usando malloc). a a o
lLista prepend(lLista la, int e) lLista laux; laux=(lLista)malloc(sizeof(noLista)); (*laux).valor=e; (*laux).resto=la; return laux;

A funcao seguinte, dada uma lista, devolve o apontador para uma c pia dessa lista: o lLista copiaLista(lLista la) lLista laux; laux=lvazia; while (la!=lvazia) laux=prepend(laux,(*la).valor)); la=(*la).resto; return laux; A pr xima vers o da funcao prepend copia o argumento e, assim, implementa a segunda o a solucao referida acima.
lLista prependCopia(lLista la, int e) lLista laux; laux=(lLista)malloc(sizeof(noLista)); (*laux).valor=e; (*laux).resto=copiaLista(la); return laux;

Remocao Nesta seccao ocupar-nos-emos da operacao resto que, dada uma lista ligada, devolve a lis ta sem o elemento inicial. A unica diculdade com esta operacao prende-se com o que j foi a

60

discutido acima sobre qual a forma de representar o resultado desta funcao. Como a lista resul tante e sublista da lista argumento a solucao mais econ mica e devolver o apontador para a lista o seguinte. Por outro lado pode alternativamente devolver-se uma c pia desta lista. o A primeira solucao leva a seguinte funcao: `
lLista resto (lLista la); return (*la).resto;

A segunda solucao leva a seguinte funcao: `


lLista restoCopia (lLista la); return copiaLista((*la).resto);

Nas funcoes anteriores pressup e-se que a lista argumento e n o vazia. o a H ainda que considerar mais uma quest o a prop sito desta funcao e que tem a ver com a a a o libertacao da mem ria j ocupada. Quando se pretende (ou e necess rio) limitar a mem ria o a a o ocupada h que libertar a mem ria atribuda din micamente e que j n o e necess ria. Uma a o a a a a boa oportunidade pode ser a prop sito do c lculo de resto desde que se saiba que o primeiro o a elemento n o ser mais necess rio. A funcao que determina a lista seguinte e, simultaneamente, a a a liberta a mem ria ocupada pelo primeiro n e a seguinte: o o
lLista restoLiberta (lLista la); lLista laux; laux = (*la).resto; free(la); return aux;

O procedimento seguinte (recursivo) liberta todo o espaco associado a uma determinada lista:

void libertaLista (lLista la); if (la!=lvazia) libertaLista(restoLiberta(la));

Junta A concatenacao de duas listas la e lb levanta o mesmo tipo de quest es: ser de devolver o a uma nova c pia das duas listas ou n o? A primeira solucao que apresentamos aqui e a mais o a econ mica em espaco. Em qualquer caso e util a denicao de uma funcao pultimo que, dada o uma lista, devolve o apontador para o seu ultimo n (ou lvazia se este n o existir). o a
lLista pultimo (lLista la) return la==lvazia ? lvazia : (resto(la)==lvazia ? la :

Usando esta funcao tem-se:


lLista junta (lLista la, lLista lb) lLista lr; if (la==lvazia) lr=lb; else (*(pultimo(la)))=lb; lr=la; return lr;

Quanto a segunda solucao obt m-se facilmente copiando primeiro as listas argumento e devol` e vendo o resultado da solucao agora encontrada com argumento nas c pias. o

61

pultimo(resto(la)));

Seleciona positivos A funcao que, dada uma lista, devolve uma nova lista com os elementos positivos da lista argumento dene-se facilmente nesta representacao, usando as funcoes agora denidas, como se segue:
lLista selecionaPos (lLista la) return la==lvazia ? lvazia : (primeiro(la)>0 ? prepend(selecionaPos(resto(la)),primeiro(la)) : selecionaPos(resto(la)));

Para a vers o imperativa seria conveniente denir uma funcao append. No entanto, usando a este tipo de estruturas de dados, essa solucao n o e eciente. Deixa-se esta quest o como a a exerccio.

4.3.5 Observacoes nais


Existem diversas variacoes desta estrutura de dados que referiremos apenas brevemente aqui. Em primeiro lugar pode armazenar-se mais informacao em cada n . Pode ser util um campo o com o comprimento da lista ou com o mnimo ou o m ximo se houver necessidade de obter a estes valores de forma eciente e com frequ ncia. Em segundo lugar pode imp r-se que a lista e o ligada esteja sempre ordenada. Podem considerar-se em cada n apontadores para o n anterior, o o bem como para o n seguinte. Neste caso, como tamb m se pode percorrer a lista da direita o e para a esquerda, podem caracterizar-se as listas por dois apontadores, um para o n inicial e o outro para o n nal. o

4.4 Exerc cios


4.4.1 Desenvolva recursivamente em C, usando vectores, uma funcao que inverta uma lista dada. 4.4.2 Desenvolva recursivamente e imperativamente em C, usando listas ligadas, uma funcao que teste igualdade de listas. 4.4.3 Desenvolva recursivamente em C, usando listas ligadas, a funcao m ximo (de uma lista a de reais). 4.4.4 Desenvolva recursivamente e imperativamente em C, usando vectores e listas ligadas, uma funcao que, dadas duas listas determina a lista resultante de as intercalar. 4.4.5 Desenvolva recursivamente em C, usando vectores e listas ligadas, as funcoes que deter minam o mnimo e o teste de prexo. 4.4.6 Desenvolva uma vers o do Merge Sort com o procedimento combina imperativo para a vectores. 4.4.7 Adapte os algoritmos Merge Sort e Quick Sort a listas ligadas.

62

Você também pode gostar