Escolar Documentos
Profissional Documentos
Cultura Documentos
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
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
Neste caso h que invocar o procedimento troca com os enderecos das vari veis relevantes: a a
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
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.
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.
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);
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.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
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
struct [nome]
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.
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;
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:
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 :
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.
62