Escolar Documentos
Profissional Documentos
Cultura Documentos
Estruturas de Dados em C
Estruturas de Dados em C
Profs. Waldemar Celes e Jos Lucas Rangel PUC-RIO - Curso de Engenharia - 2002
Apresentao
A disciplina de Estruturas de Dados (ED) est sendo ministrada em sua nova verso desde o segundo semestre de 1998. Trata-se da segunda disciplina de informtica oferecida no curso de Engenharia da PUC-Rio. Na primeira disciplina, Introduo Cincia da Computao (ICC), so apresentados os conceitos fundamentais de programao. ICC, em sua verso mais nova, utiliza a linguagem Scheme, de fcil aprendizado, o que permite a discusso de diversos conceitos de programao num curso introdutrio. Isso acontece porque Scheme, como a linguagem LISP da qual descende, uma linguagem funcional, baseada em conceitos familiares aos alunos, como a definio de funes e sua aplicao em expresses que devem ser avaliadas. O enfoque do curso de Estruturas de Dados diferente. Discutem-se tcnicas de programao e estruturao de dados para o desenvolvimento de programas eficientes. Adota-se a linguagem de programao C. Apesar de reconhecermos as dificuldades na aprendizagem da linguagem C, optamos por sua utilizao neste curso simplesmente porque C a linguagem bsica da programao do UNIX, da Internet, do Windows, do Linux. Alm de C, usam-se nestes sistemas e em aplicaes desenvolvidas para eles linguagens derivadas de C, como C++ e Java. Um ponto adicional a favor da escolha de C que o estudo de vrias disciplinas posteriores a ED ser facilitado se os alunos j puderem programar com desenvoltura nessa linguagem. Este curso foi idealizado e montado pelo Prof. Jos Lucas Rangel. Neste semestre, estamos reformulando alguns tpicos, criando outros e alterando a ordem de apresentao. Esta apostila foi reescrita tendo como base a apostila do Prof. Rangel, utilizada nos semestres anteriores. O curso est dividido em trs partes. A Parte I apresenta os conceitos fundamentais da linguagem C e discute formas simples de estruturao de dados; a Parte II discute as estruturas de listas e rvores, e suas aplicaes; e a Parte III discute algoritmos e estruturas de dados para ordenao e busca. A apostila apresenta todos os tpicos que sero discutidos em sala de aula, mas recomendamos fortemente que outras fontes (livros, notas de aula, etc.) sejam consultadas. Rio de Janeiro, 19 de fevereiro de 2002 Waldemar Celes
ndice
1. Conceitos fundamentais........................................................................ 1-1
1.1. Introduo ........................................................................................................... 1-1 1.2. Modelo de um computador .................................................................................. 1-1 1.3. Interpretao versus Compilao ....................................................................... 1-3 1.4. Exemplo de cdigo em C .................................................................................... 1-4 1.5. Compilao de programas em C.......................................................................... 1-6 1.6. Ciclo de desenvolvimento .................................................................................... 1-8
9.1. Mdulos e Compilao em Separado .................................................................. 9-1 9.2. Tipo Abstrato de Dados........................................................................................ 9-3
1. Conceitos fundamentais
W. Celes e J. L. Rangel
1.1. Introduo
O curso de Estruturas de Dados discute diversas tcnicas de programao, apresentando as estruturas de dados bsicas utilizadas no desenvolvimento de software. O curso tambm introduz os conceitos bsicos da linguagem de programao C, que utilizada para a implementao das estruturas de dados apresentadas. A linguagem de programao C tem sido amplamente utilizada na elaborao de programas e sistemas nas diversas reas em que a informtica atua, e seu aprendizado tornou-se indispensvel tanto para programadores profissionais como para programadores que atuam na rea de pesquisa. O conhecimento de linguagens de programao por si s no capacita programadores necessrio saber us-las de maneira eficiente. O projeto de um programa engloba a fase de identificao das propriedades dos dados e caractersticas funcionais. Uma representao adequada dos dados, tendo em vista as funcionalidades que devem ser atendidas, constitui uma etapa fundamental para a obteno de programas eficientes e confiveis. A linguagem C, assim como as linguagens Fortran e Pascal, so ditas linguagens convencionais, projetadas a partir dos elementos fundamentais da arquitetura de von Neuman, que serve como base para praticamente todos os computadores em uso. Para programar em uma linguagem convencional, precisamos de alguma maneira especificar as reas de memria em que os dados com que queremos trabalhar esto armazenados e, freqentemente, considerar os endereos de memria em que os dados se situam, o que faz com que o processo de programao envolva detalhes adicionais, que podem ser ignorados quando se programa em uma linguagem como Scheme. Em compensao, temos um maior controle da mquina quando utilizamos uma linguagem convencional, e podemos fazer programas melhores, ou seja, menores e mais rpidos. A linguagem C prov as construes fundamentais de fluxo de controle necessrias para programas bem estruturados: agrupamentos de comandos; tomadas de deciso (if-else); laos com testes de encerramento no incio (while, for) ou no fim (do-while); e seleo de um dentre um conjunto de possveis casos (switch). C oferece ainda acesso a apontadores e a habilidade de fazer aritmtica com endereos. Por outro lado, a linguagem C no prov operaes para manipular diretamente objetos compostos, tais como cadeias de caracteres, nem facilidades de entrada e sada: no h comandos READ e WRITE. Todos esses mecanismos devem ser fornecidos por funes explicitamente chamadas. Embora a falta de algumas dessas facilidades possa parecer uma deficincia grave (deve-se, por exemplo, chamar uma funo para comparar duas cadeias de caracteres), a manuteno da linguagem em termos modestos tem trazido benefcios reais. C uma linguagem relativamente pequena e, no entanto, tornou-se altamente poderosa e eficiente.
conhecimento da existncia destes elementos nos ajudar a compreender como um programa de computador funciona.
Canal de comunicao (BUS)
CPU
Central de processamento Armazenamento secundrio Dispositivos de entrada/sada
Memria
A Figura 1.1 identifica os elementos bsicos de um computador tpico. O canal de comunicao (conhecido como BUS) representa o meio para a transferncia de dados entre os diversos componentes. Na memria principal so armazenados os programas e os dados no computador. Ela tem acesso randmico, o que significa que podemos enderear (isto , acessar) diretamente qualquer posio da memria. Esta memria no permanente e, para um programa, os dados so armazenados enquanto o programa est sendo executado. Em geral, aps o trmino do programa, a rea ocupada na memria fica disponvel para ser usada por outras aplicaes. A rea de armazenamento secundrio , em geral, representada por um disco (disco rgido, disquete, etc.). Esta memria secundria tem a vantagem de ser permanente. Os dados armazenados em disco permanecem vlidos aps o trmino dos programas. Esta memria tem um custo mais baixo do que a memria principal, porm o acesso aos dados bem mais lento. Por fim, encontram-se os dispositivos de entrada e sada. Os dispositivos de entrada (por exemplo, teclado, mouse) permitem passarmos dados para um programa, enquanto os dispositivos de sada permitem que um programa exporte seus resultados, por exemplo em forma textual ou grfica usando monitores ou impressoras. Armazenamento de dados e programas na memria A memria do computador dividida em unidades de armazenamento chamadas bytes. Cada byte composto por 8 bits, que podem armazenar os valores zero ou um. Nada alm de zeros e uns pode ser armazenado na memria do computador. Por esta razo, todas as informaes (programas, textos, imagens, etc.) so armazenadas usando uma codificao numrica na forma binria. Na representao binria, os nmeros so representados por uma seqncia de zeros e uns (no nosso dia a dia, usamos a representao decimal, uma vez que trabalhamos com 10 algarismos). Por exemplo, o nmero decimal 5 representado por 101, pois 1*22 + 0*21 + 1*20 igual a 5 (da mesma forma que, na base decimal, 456=4*102 + 5*101 + 6*100). Cada posio da memria (byte) tem um endereo nico. No possvel enderear diretamente um bit. Se s podemos armazenar nmeros na memria do computador, como fazemos para armazenar um texto (um documento ou uma mensagem)? Para ser possvel armazenar uma seqncia de caracteres, que representa o texto, atribui-se a cada caractere um cdigo
Estruturas de Dados PUC-Rio 1-2
numrico (por exemplo, pode-se associar ao caractere 'A' o cdigo 65, ao caractere 'B' o cdigo 66, e assim por diante). Se todos os caracteres tiverem cdigos associados (inclusive os caracteres de pontuao e de formatao), podemos armazenar um texto na memria do computador como uma seqncia de cdigos numricos. Um computador s pode executar programas em linguagens de mquina. Cada programa executvel uma seqncia de instrues que o processador central interpreta, executando as operaes correspondentes. Esta seqncia de instrues tambm representada como uma seqncia de cdigos numricos. Os programas ficam armazenados em disco e, para serem executados pelo computador, devem ser carregados (transferidos) para a memria principal. Uma vez na memria, o computador executa a seqncia de operaes correspondente.
IM Interpretador
Sada
Dados de Entrada
1-3
CM Compilador
Sada
Devemos notar que, na Figura 1.2, o programa fonte um dado de entrada a mais para o interpretador. No caso da compilao, Figura 1.3, identificamos duas fases: na primeira, o programa objeto a sada do programa compilador e, na segunda, o programa objeto executado, recebendo os dados de entrada e gerando a sada correspondente. Observamos que, embora seja comum termos linguagens funcionais implementadas por interpretao e linguagens convencionais por compilao, h excees, no existindo nenhum impedimento conceitual para implementar qualquer linguagem por qualquer dos dois mtodos, ou at por uma combinao de ambos. O termo mquina usado acima intencionalmente vago. Por exemplo, computadores idnticos com sistemas operacionais diferentes devem ser considerados mquinas, ou plataformas, diferentes. Assim, um programa em C, que foi compilado em um PC com Windows, no dever ser executado em um PC com Linux, e vice-versa.
1-4
int main (void) { float t1; float t2; /* mostra mensagem para usuario */ printf("Digite a temperatura em Celsius: "); /* captura valor entrado via teclado */ scanf("%f",&t1); /* faz a conversao */ t2 = converte(t1); /* exibe resultado */ printf("A temperatura em Fahrenheit : %f\n", t2); return 0; }
Um programa em C, em geral, constitudo de diversas pequenas funes, que so independentes entre si no podemos, por exemplo, definir uma funo dentro de outra. Dois tipos de ambientes so caracterizados em um cdigo C. O ambiente global, externo s funes, e os ambientes locais, definidos pelas diversas funes (lembrando que os ambientes locais so independentes entre si). Podem-se inserir comentrios no cdigo fonte, iniciados com /* e finalizados com */, conforme ilustrado acima. Devemos notar tambm que comandos e declaraes em C so terminados pelo caractere ponto-e-vrgula (;). Um programa em C tem que, obrigatoriamente, conter a funo principal (main). A execuo de um programa comea pela funo principal (a funo main automaticamente chamada quando o programa carregado para a memria). As funes auxiliares so chamadas, direta ou indiretamente, a partir da funo principal. Em C, como nas demais linguagens convencionais, devemos reservar rea de memria para armazenar cada dado. Isto feito atravs da declarao de variveis, na qual informamos o tipo do dado que iremos armazenar naquela posio de memria. Assim, a declarao float t1;, do cdigo mostrado, reserva um espao de memria para armazenarmos um valor real (ponto flutuante float). Este espao de memria referenciado atravs do smbolo t1. Uma caracterstica fundamental da linguagem C diz respeito ao tempo de vida e visibilidade das variveis. Uma varivel (local) declarada dentro de uma funo "vive" enquanto esta funo est sendo executada, e nenhuma outra funo tem acesso direto a esta varivel. Outra caracterstica das variveis locais que devem sempre ser explicitamente inicializadas antes de seu uso, caso contrrio contero lixo, isto , valores indefinidos. Como alternativa, possvel definir variveis que sejam externas s funes, isto , variveis globais, que podem ser acessadas pelo nome por qualquer funo subseqente
Estruturas de Dados PUC-Rio 1-5
(so visveis em todas as funes que se seguem sua definio). Alm do mais, devido s variveis externas (ou globais) existirem permanentemente (pelo menos enquanto o programa estiver sendo executado), elas retm seus valores mesmo quando as funes que as acessam deixam de existir. Embora seja possvel definir variveis globais em qualquer parte do ambiente global (entre quaisquer funes), prtica comum defini-las no incio do arquivo-fonte. Como regra geral, por razes de clareza e estruturao adequada do cdigo, devemos evitar o uso indisciplinado de variveis globais e resolver os problemas fazendo uso de variveis locais sempre que possvel. No prximo captulo, discutiremos variveis com mais detalhe.
Se no houver erro de compilao no nosso cdigo, este comando gera o executvel com o nome prog (prog.exe, no Windows). Podemos ento executar o programa:
> prog Digite a temperatura em Celsius: 10 A temperatura em Fahrenheit vale: 50.000000 >
Em itlico, representamos as mensagens do programa e, em negrito, exemplificamos um dado fornecido pelo usurio via teclado. Programas com vrios arquivos fontes Os programas reais so, naturalmente, maiores. Nestes casos, subdividimos o fonte do programa em vrios arquivos. Para exemplificar a criao de um programa com dois arquivos, vamos considerar que o programa para converso de unidades de temperatura
1
Podemos utilizar qualquer editor de texto para escrever os programas fontes, exceto editores que incluem caracteres de formatao (como o Word do Windows, por exemplo).
1-6
apresentado anteriormente seja dividido em dois fontes: o arquivo converte.c e o arquivo principal.c. Teramos que criar dois arquivos, como ilustrado abaixo: Arquivo converte.c:
/* Implementao do mdulo de converso */ float converte (float c) { float f; f = 1.8*c + 32; return f; }
Arquivo principal.c:
/* Programa para converso de temperatura */ #include <stdio.h> float converte (float c); int main (void) { float t1; float t2; /* mostra mensagem para usuario */ printf("Entre com temperatura em Celsius: "); /* captura valor entrado via teclado */ scanf("%f",&t1); /* faz a conversao */ t2 = converte(t1); /* exibe resultado */ printf("A temperatura em Fahrenheit vale: %f\n", t2); return 0; }
Embora o entendimento completo desta organizao de cdigo no fique claro agora, interessa-nos apenas mostrar como geramos um executvel de um programa com vrios arquivos fontes. Uma alternativa compilar tudo junto e gerar o executvel como anteriormente:
> gcc o prog converte.c principal.c
No entanto, esta no a melhor estratgia, pois se alterarmos a implementao de um determinado mdulo no precisaramos re-compilar os outros. Uma forma mais eficiente compilarmos os mdulos separadamente e depois ligar os diversos mdulos objetos gerados para criar um executvel.
> gcc c converte.c > gcc c principal.c > gcc o prog converte.o principal.o
1-7
A opo c do compilador gcc indica que no queremos criar um executvel, apenas gerar o arquivo objeto (com extenso .o ou .obj). Depois, invocamos gcc para fazer a ligao dos objetos, gerando o executvel.
Editar
Compilar
Ligar
Testar
1-8
Este ciclo pode ser realizado usando programas (editor, compilador, ligador) separados ou empregando um ambiente integrado de desenvolvimento (integrated development environment, ou IDE). IDE um programa que oferece janelas para a edio de programas e facilidades para abrir, fechar e salvar arquivos e para compilar, ligar e executar programas. Se um IDE estiver disponvel, possvel criar e testar um programa, tudo em um mesmo ambiente, e todo o ciclo mencionado acima acontece de maneira mais confortvel dentro de um mesmo ambiente, de preferncia com uma interface amigvel.
1-9
2. Expresses
W. Celes e J. L. Rangel Em C, uma expresso uma combinao de variveis, constantes e operadores que pode ser avaliada computacionalmente, resultando em um valor. O valor resultante chamado de valor da expresso.
2.1.
Variveis
Podemos dizer que uma varivel representa um espao na memria do computador para armazenar determinado tipo de dado. Na linguagem C, todas as variveis devem ser explicitamente declaradas. Na declarao de uma varivel, obrigatoriamente, devem ser especificados seu tipo e seu nome: o nome da varivel serve de referncia ao dado armazenado no espao de memria da varivel e o tipo da varivel determina a natureza do dado que ser armazenado. Tipos bsicos A linguagem C oferece alguns tipos bsicos. Para armazenar valores inteiros, existem trs tipos bsicos: char, short int, long int. Estes tipos diferem entre si pelo espao de memria que ocupam e conseqentemente pelo intervalo de valores que podem representar. O tipo char, por exemplo, ocupa 1 byte de memria (8 bits), podendo representar 28 (=256) valores distintos. Todos estes tipos podem ainda ser modificados para representarem apenas valores positivos, o que feito precedendo o tipo com o modificador sem sinal unsigned. A tabela abaixo compara os tipos para valores inteiros e suas representatividades.
Tipo char unsigned char short int unsigned short int long int unsigned long int Tamanho 1 byte 1 byte 2 bytes 2 bytes 4 bytes 4 bytes Representatividade -128 a 127 0 a 255 -32 768 a 32 767 0 a 65 535 -2 147 483 648 a 2 147 483 647 4 294 967295
Os tipos short int e long int podem ser referenciados simplesmente com short e long, respectivamente. O tipo int puro mapeado para o tipo inteiro natural da mquina, que pode ser short ou long. A maioria das mquinas que usamos hoje funcionam com processadores de 32 bits e o tipo int mapeado para o inteiro de 4 bytes (long).1 O tipo char geralmente usado apenas para representar cdigos de caracteres, como veremos nos captulos subseqentes.
Um contra-exemplo o compilador TurboC, que foi desenvolvido para o sistema operacional DOS mas ainda pode ser utilizado no Windows. No TurboC, o tipo int mapeado para 2 bytes.
2-1
A linguagem oferece ainda dois tipos bsicos para a representao de nmeros reais (ponto flutuante): float e double. A tabela abaixo compara estes dois tipos.
Tipo float double Tamanho 4 bytes 8 bytes Representatividade -38 38 10 a 10 -308 308 10 a 10
Declarao de variveis Para armazenarmos um dado (valor) na memria do computador, devemos reservar o espao correspondente ao tipo do dado a ser armazenado. A declarao de uma varivel reserva um espao na memria para armazenar um dado do tipo da varivel e associa o nome da varivel a este espao de memria.
int a; int b; float c; a = 5; b = 10; c = 5.3; /* declara uma varivel do tipo int */ /* declara outra varivel do tipo int */ /* declara uma varivel do tipo float */ /* armazena o valor 5 em a */ /* armazena o valor 10 em b */ /* armazena o valor 5.3 em c */
A linguagem permite que variveis de mesmo tipo sejam declaradas juntas. Assim, as duas primeiras declaraes acima poderiam ser substitudas por:
int a, b; /* declara duas variveis do tipo int */
Uma vez declarada a varivel, podemos armazenar valores nos respectivos espaos de memria. Estes valores devem ter o mesmo tipo da varivel, conforme ilustrado acima. No possvel, por exemplo, armazenar um nmero real numa varivel do tipo int. Se fizermos:
int a; a = 4.3; /* a varivel armazenar o valor 4 */
ser armazenada em a apenas a parte inteira do nmero real, isto , 4. Alguns compiladores exibem uma advertncia quando encontram este tipo de atribuio. Em C, as variveis podem ser inicializadas na declarao. Podemos, por exemplo, escrever:
int a = 5, b = 10; float c = 5.3; /* declara e inicializa as variveis */
Valores constantes Em nossos cdigos, usamos tambm valores constantes. Quando escrevemos a atribuio:
a = b + 123;
2-2
sendo a e b variveis supostamente j declaradas, reserva-se um espao para armazenar a constante 123. No caso, a constante do tipo inteiro, ento um espao de quatro bytes (em geral) reservado e o valor 123 armazenado nele. A diferena bsica em relao s variveis, como os nomes dizem (variveis e constantes), que o valor armazenado numa rea de constante no pode ser alterado. As constantes tambm podem ser do tipo real. Uma constante real deve ser escrita com um ponto decimal ou valor de expoente. Sem nenhum sufixo, uma constante real do tipo double. Se quisermos uma constante real do tipo float, devemos, a rigor, acrescentar o sufixo F ou f. Alguns exemplos de constantes reais so:
12.45 1245e-2 12.45F constante real do tipo double constante real do tipo double constante real do tipo float
pois o cdigo, a rigor, armazena um valor double (12.45) numa varivel do tipo float. Desde que a constante seja representvel dentro de um float, no precisamos nos preocupar com este tipo de advertncia. Variveis com valores indefinidos Um dos erros comuns em programas de computador o uso de variveis cujos valores ainda esto indefinidos. Por exemplo, o trecho de cdigo abaixo est errado, pois o valor armazenado na varivel b est indefinido e tentamos us-lo na atribuio a c. comum dizermos que b tem lixo.
int a, b, c; a = 2; c = a + b;
Alguns destes erros so bvios (como o ilustrado acima) e o compilador capaz de nos reportar uma advertncia; no entanto, muitas vezes o uso de uma varivel no definida fica difcil de ser identificado no cdigo. Repetimos que um erro comum em programas e uma razo para alguns programas funcionarem na parte da manh e no funcionarem na parte da tarde (ou funcionarem durante o desenvolvimento e no funcionarem quando entregamos para nosso cliente!). Todos os erros em computao tm lgica. A razo de o programa poder funcionar uma vez e no funcionar outra que, apesar de indefinido, o valor da varivel existe. No nosso caso acima, pode acontecer que o valor armazenado na memria ocupada por b seja 0, fazendo com que o programa funcione. Por outro lado, pode acontecer de o valor ser 293423 e o programa no funcionar.
2-3
2.2.
Operadores
A linguagem C oferece uma gama variada de operadores, entre binrios e unrios. Os operadores bsicos so apresentados a seguir. Operadores Aritmticos Os operadores aritmticos binrios so: +, -, *, / e o operador mdulo %. H ainda o operador unrio -. A operao feita na preciso dos operandos. Assim, a expresso 5/2 resulta no valor 2, pois a operao de diviso feita em preciso inteira, j que os dois operandos (5 e 2) so constantes inteiras. A diviso de inteiros trunca a parte fracionria, pois o valor resultante sempre do mesmo tipo da expresso. Conseqentemente, a expresso 5.0/2.0 resulta no valor real 2.5 pois a operao feita na preciso real (double, no caso). O operador mdulo, %, no se aplica a valores reais, seus operandos devem ser do tipo inteiro. Este operador produz o resto da diviso do primeiro pelo segundo operando. Como exemplo de aplicao deste operador, podemos citar o caso em que desejamos saber se o valor armazenado numa determinada varivel inteira x par ou mpar. Para tanto, basta analisar o resultado da aplicao do operador %, aplicado varivel e ao valor dois.
x % 2 x % 2 se resultado for zero se resultado for um nmero par nmero mpar
Os operadores *, / e % tm precedncia maior que os operadores + e -. O operador unrio tem precedncia maior que *, / e %. Operadores com mesma precedncia so avaliados da esquerda para a direita. Assim, na expresso:
a + b * c /d
executa-se primeiro a multiplicao, seguida da diviso, seguida da soma. Podemos utilizar parnteses para alterar a ordem de avaliao de uma expresso. Assim, se quisermos avaliar a soma primeiro, podemos escrever:
(a + b) * c /d
Uma tabela de precedncia dos operadores da linguagem C apresentada no final desta seo. Operadores de atribuio Na linguagem C, uma atribuio uma expresso cujo valor resultante corresponde ao valor atribudo. Assim, da mesma forma que a expresso:
5 + 3
2-4
resulta no valor 5 (alm, claro, de armazenar o valor 5 na varivel a). Este tratamento das atribuies nos permite escrever comandos do tipo:
y = x = 5;
Neste caso, a ordem de avaliao da direita para a esquerda. Assim, o computador avalia x = 5, armazenando 5 em x, e em seguida armazena em y o valor produzido por x = 5, que 5. Portanto, ambos, x e y, recebem o valor 5. A linguagem tambm permite utilizar os chamados operadores de atribuio compostos. Comandos do tipo:
i = i + 2;
em que a varivel esquerda do sinal de atribuio tambm aparece direita, podem ser escritas de forma mais compacta:
i += 2;
usando o operador de atribuio composto +=. Analogamente, existem, entre outros, os operadores de atribuio: -=, *=, /=, %=. De forma geral, comandos do tipo:
var op= expr;
so equivalentes a:
var = var op (expr);
equivale a
x = x * (y + 1)
e no a
x = x * y + 1;
Operadores de incremento e decremento A linguagem C apresenta ainda dois operadores no convencionais. So os operadores de incremento e decremento, que possuem precedncia comparada ao - unrio e servem para incrementar e decrementar uma unidade nos valores armazenados nas variveis. Assim, se n uma varivel que armazena um valor, o comando:
n++;
2-5
incrementa de uma unidade o valor de n (anlogo para o decremento em n--). O aspecto no usual que ++ e -- podem ser usados tanto como operadores pr-fixados (antes da varivel, como em ++n) ou ps-fixados (aps a varivel, como em n++). Em ambos os casos, a varivel n incrementada. Porm, a expresso ++n incrementa n antes de usar seu valor, enquanto n++ incrementa n aps seu valor ser usado. Isto significa que, num contexto onde o valor de n usado, ++n e n++ so diferentes. Se n armazena o valor 5, ento:
x = n++;
atribui 5 a x, mas:
x = ++n;
atribuiria 6 a x. Em ambos os casos, n passa a valer 6, pois seu valor foi incrementado em uma unidade. Os operadores de incremento e decremento podem ser aplicados somente em variveis; uma expresso do tipo x = (i + 1)++ ilegal. A linguagem C oferece diversas formas compactas para escrevermos um determinado comando. Neste curso, procuraremos evitar as formas compactas pois elas tendem a dificultar a compreenso do cdigo. Mesmo para programadores experientes, o uso das formas compactas deve ser feito com critrio. Por exemplo, os comandos:
a = a + 1; a += 1; a++; ++a;
so todos equivalentes e o programador deve escolher o que achar mais adequado e simples. Em termos de desempenho, qualquer compilador razovel capaz de otimizar todos estes comandos da mesma forma. Operadores relacionais e lgicos Os operadores relacionais em C so:
< > <= >= == != menor que maior que menor ou igual que maior ou igual que igual a diferente de
Estes operadores comparam dois valores. O resultado produzido por um operador relacional zero ou um. Em C, no existe o tipo booleano (true ou false). O valor zero interpretado como falso e qualquer valor diferente de zero considerado verdadeiro. Assim, se o
Estruturas de Dados PUC-Rio 2-6
resultado de uma comparao for falso, produz-se o valor 0, caso contrrio, produz-se o valor 1. Os operadores lgicos combinam expresses booleanas. A linguagem oferece os seguintes operadores lgicos:
&& || ! operador binrio E (AND) operador binrio OU (OR) operador unrio de NEGAO (NOT)
Expresses conectadas por && ou || so avaliadas da esquerda para a direita, e a avaliao pra assim que a veracidade ou falsidade do resultado for conhecida. Recomendamos o uso de parnteses em expresses que combinam operadores lgicos e relacionais. Os operadores relacionais e lgicos so normalmente utilizados para tomada de decises. No entanto, podemos utiliz-los para atribuir valores a variveis. Por exemplo, o trecho de cdigo abaixo vlido e armazena o valor 1 em a e 0 em b.
int a, b; int c = 23; int d = c + 4; a = (c < 20) || (d > c); b = (c < 20) && (d > c); /* verdadeiro */ /* falso */
Devemos salientar que, na avaliao da expresso atribuda varivel b, a operao (d>c) no chega a ser avaliada, pois independente do seu resultado a expresso como um todo ter como resultado 0 (falso), uma vez que a operao (c<20) tem valor falso. Operador sizeof Outro operador fornecido por C, sizeof, resulta no nmero de bytes de um determinado tipo. Por exemplo:
int a = sizeof(float);
armazena o valor 4 na varivel a, pois um float ocupa 4 bytes de memria. Este operador pode tambm ser aplicado a uma varivel, retornando o nmero de bytes do tipo associado varivel. Converso de tipo Em C, como na maioria das linguagens, existem converses automticas de valores na avaliao de uma expresso. Assim, na expresso 3/1.5, o valor da constante 3 (tipo int) promovido (convertido) para double antes de a expresso ser avaliada, pois o segundo operando do tipo double (1.5) e a operao feita na preciso do tipo mais representativo.
2-7
Quando, numa atribuio, o tipo do valor atribudo diferente do tipo da varivel, tambm h uma converso automtica de tipo. Por exemplo, se escrevermos:
int a = 3.5;
o valor 3.5 convertido para inteiro (isto , passa a valer 3) antes de a atribuio ser efetuada. Como resultado, como era de se esperar, o valor atribudo varivel 3 (inteiro). Alguns compiladores exibem advertncias quando a converso de tipo pode significar uma perda de preciso ( o caso da converso real para inteiro, por exemplo). O programador pode explicitamente requisitar uma converso de tipo atravs do uso do operador de molde de tipo (operador cast). Por exemplo, so vlidos (e isentos de qualquer advertncia por parte dos compiladores) os comandos abaixo.
int a, b; a = (int) 3.5; b = (int) 3.5 % 2;
Precedncia e ordem de avaliao dos operadores A tabela abaixo mostra a precedncia, em ordem decrescente, dos principais operadores da linguagem C.
Operador ( ) [ ] -> . ! ~ ++ -- - (tipo) * & sizeof(tipo) * / % + << >> < <= > >= == != & ^ | && || ?: = += -= etc. , Associatividade esquerda para direita direita para esquerda esquerda para direita esquerda para direita esquerda para direita esquerda para direita esquerda para direita esquerda para direita esquerda para direita esquerda para direita esquerda para direita esquerda para direita direita para esquerda direita para esquerda esquerda para direita
2.3.
A linguagem C no possui comandos de entrada e sada do tipo READ e WRITE encontrados na linguagem FORTRAN. Tudo em C feito atravs de funes, inclusive as operaes de entrada e sada. Por isso, j existe em C uma biblioteca padro que possui as funes bsicas normalmente necessrias. Na biblioteca padro de C, podemos, por exemplo, encontrar funes matemticas do tipo raiz quadrada, seno, cosseno, etc., funes para a manipulao de cadeias de caracteres e funes de entrada e sada. Nesta seo, sero apresentadas as duas funes bsicas de entrada e sada disponibilizadas pela biblioteca padro. Para utiliz-las, necessrio incluir o prottipo destas funes no
2-8
cdigo. Este assunto ser tratado em detalhes na seo sobre funes. Por ora, basta saber que preciso escrever:
#include <stdio.h>
no incio do programa que utiliza as funes da biblioteca de entrada e sada. Funo printf A funo printf possibilita a sada de valores (sejam eles constantes, variveis ou resultado de expresses) segundo um determinado formato. Informalmente, podemos dizer que a forma da funo :
printf (formato, lista de constantes/variveis/expresses...);
O primeiro parmetro uma cadeia de caracteres, em geral delimitada com aspas, que especifica o formato de sada das constantes, variveis e expresses listadas em seguida. Para cada valor que se deseja imprimir, deve existir um especificador de formato correspondente na cadeia de caracteres formato. Os especificadores de formato variam com o tipo do valor e a preciso em que queremos que eles sejam impressos. Estes especificadores so precedidos pelo caractere % e podem ser, entre outros:
%c %d %u %f %e %g %s especifica um char especifica um int especifica um unsigned int especifica um double (ou float) especifica um double (ou float) no formato cientfico especifica um double (ou float) no formato mais apropriado (%f ou %e) especifica uma cadeia de caracteres
Alguns exemplos:
printf ("%d %g\n", 33, 5.3);
Ou:
printf ("Inteiro = %d Real = %g\n", 33, 5.3);
com sada:
Inteiro = 33 Real = 5.3
2-9
Isto , alm dos especificadores de formato, podemos incluir textos no formato, que so mapeados diretamente para a sada. Assim, a sada formada pela cadeia de caracteres do formato onde os especificadores so substitudos pelos valores correspondentes. Existem alguns caracteres de escape que so freqentemente utilizados nos formatos de sada. So eles:
\n \t \r \" \\ caractere de nova linha caractere de tabulao caractere de retrocesso o caractere " o caractere \
Ainda, se desejarmos ter como sada um caractere %, devemos, dentro do formato, escrever %%. possvel tambm especificarmos o tamanho dos campos:
%4d 4 %7.2f 5 . 3 0 2 7 3 3
A funo printf retorna o nmero de campos impressos. Salientamos que para cada constante, varivel ou expresso listada devemos ter um especificador de formato apropriado. Funo scanf A funo scanf permite capturarmos valores fornecidos via teclado pelo usurio do programa. Informalmente, podemos dizer que sua forma geral :
scanf (formato, lista de endereos das variveis...);
O formato deve possuir especificadores de tipos similares aos mostrados para a funo printf. Para a funo scanf, no entanto, existem especificadores diferentes para o tipo float e o tipo double:
2-10
especifica um char especifica um int especifica um unsigned int especificam um float especificam um double especifica uma cadeia de caracteres
A principal diferena que o formato deve ser seguido por uma lista de endereos de variveis (na funo printf passamos os valores de constantes, variveis e expresses). Na seo sobre ponteiros, este assunto ser tratado em detalhes. Por ora, basta saber que, para ler um valor e atribu-lo a uma varivel, devemos passar o endereo da varivel para a funo scanf. O operador & retorna o endereo de uma varivel. Assim, para ler um inteiro, devemos ter:
int n; scanf ("%d", &n);
Para a funo scanf, os especificadores %f, %e e %g so equivalentes. Aqui, caracteres diferentes dos especificadores no formato servem para cercar a entrada. Por exemplo:
scanf ("%d:%d", &h, &m);
obriga que os valores (inteiros) fornecidos sejam separados pelo caractere dois pontos (:). Um espao em branco dentro do formato faz com que sejam "pulados" eventuais brancos da entrada. Os especificadores %d, %f, %e e %g automaticamente pulam os brancos que precederem os valores numricos a serem capturados. A funo scanf retorna o nmero de campos lidos com sucesso.
2-11
3.
Controle de fluxo
W. Celes e J. L. Rangel
A linguagem C prov as construes fundamentais de controle de fluxo necessrias para programas bem estruturados: agrupamentos de comandos, tomadas de deciso (if-else), laos com teste de encerramento no incio (while, for) ou no fim (do-while), e seleo de um dentre um conjunto de possveis casos (switch).
ou
if ( expr ) { bloco de comandos 1 ... } else { bloco de comandos 2 ... }
Se expr produzir um valor diferente de 0 (verdadeiro), o bloco de comandos 1 ser executado. A incluso do else requisita a execuo do bloco de comandos 2 se a expresso produzir o valor 0 (falso). Cada bloco de comandos deve ser delimitado por uma chave aberta e uma chave fechada. Se dentro de um bloco tivermos apenas um comando a ser executado, as chaves podem ser omitidas (na verdade, deixamos de ter um bloco):
if ( expr ) comando1; else comando2;
A indentao (recuo de linha) dos comandos fundamental para uma maior clareza do cdigo. O estilo de indentao varia a gosto do programador. Alm da forma ilustrada acima, outro estilo bastante utilizado por programadores C :
if ( expr ) { bloco de comandos 1 ... } else { bloco de comandos 2 ... }
Estruturas de Dados PUC-Rio 3-1
Primeiramente, notamos que no foi necessrio criar blocos ( {...} ) porque a cada if est associado apenas um comando. Ao primeiro, associamos o segundo comando if, e ao segundo if associamos o comando que chama a funo printf. Assim, o segundo if s ser avaliado se o primeiro valor fornecido for par, e a mensagem s ser impressa se o segundo valor fornecido tambm for par. Outra construo para este mesmo exemplo simples pode ser:
int main (void) { int a, b; printf("Digite dois numeros inteiros:"); scanf("%d%d",&a,&b); if ((a%2 == 0) && (b%2 == 0)) printf ( "Voce digitou dois numeros pares!\n"); return 0; }
produzindo resultados idnticos. Devemos, todavia, ter cuidado com o aninhamento de comandos if-else. Para ilustrar, consideremos o exemplo abaixo.
/* temperatura (versao 1 - incorreta) */ #include <stdio.h> int main (void) { int temp; printf("Digite a temperatura: "); scanf("%d", &temp); if (temp < 30) if (temp > 20) printf(" Temperatura agradavel \n"); else printf(" Temperatura muito quente \n"); return 0; }
A idia deste programa era imprimir a mensagem Temperatura agradvel se fosse fornecido um valor entre 20 e 30, e imprimir a mensagem Temperatura muito quente se fosse fornecido um valor maior que 30. No entanto, vamos analisar o caso de
Estruturas de Dados PUC-Rio 3-2
ser fornecido o valor 5 para temp. Observando o cdigo do programa, podemos pensar que nenhuma mensagem seria fornecida, pois o primeiro if daria resultado verdadeiro e ento seria avaliado o segundo if. Neste caso, teramos um resultado falso e como, aparentemente, no h um comando else associado, nada seria impresso. Puro engano. A indentao utilizada pode nos levar a erros de interpretao. O resultado para o valor 5 seria a mensagem Temperatura muito quente. Isto , o programa est INCORRETO. Em C, um else est associado ao ltimo if que no tiver seu prprio else. Para os casos em que a associao entre if e else no est clara, recomendamos a criao explcita de blocos, mesmo contendo um nico comando. Reescrevendo o programa, podemos obter o efeito desejado.
/* temperatura (versao 2) */ #include <stdio.h> int main (void) { int temp; printf ( "Digite a temperatura: " ); scanf ( "%d", &temp ); if ( temp < 30 ) { if ( temp > 20 ) printf ( " Temperatura agradavel \n" ); } else printf ( " Temperatura muito quente \n" ); return 0; }
Esta regra de associao do else propicia a construo do tipo else-if, sem que se tenha o comando elseif explicitamente na gramtica da linguagem. Na verdade, em C, construmos estruturas else-if com ifs aninhados. O exemplo abaixo vlido e funciona como esperado.
/* temperatura (versao 3) */ #include <stdio.h> int main (void) { int temp; printf("Digite a temperatura: "); scanf("%d", &temp); if (temp < 10) printf("Temperatura muito fria \n"); else if (temp < 20) printf(" Temperatura fria \n"); else if (temp < 30) printf("Temperatura agradavel \n"); else printf("Temperatura muito quente \n"); return 0;
3-3
Estruturas de bloco Observamos que uma funo C composta por estruturas de blocos. Cada chave aberta e fechada em C representa um bloco. As declaraes de variveis s podem ocorrer no incio do corpo da funo ou no incio de um bloco, isto , devem seguir uma chave aberta. Uma varivel declarada dentro de um bloco vlida apenas dentro do bloco. Aps o trmino do bloco, a varivel deixa de existir. Por exemplo:
... if ( n > 0 ) { int i; ... } ...
A varivel i, definida dentro do bloco do if, s existe dentro deste bloco. uma boa prtica de programao declarar as varveis o mais prximo possvel dos seus usos. Operador condicional C possui tambm um chamado operador condicional. Trata-se de um operador que substitui construes do tipo:
... if ( a > b ) maximo = a; else maximo = b; ...
se a condio for verdadeira, a expresso1 avaliada; caso contrrio, avalia-se a expresso2. O comando:
maximo = a > b ? a : b ;
n != n (n 1) (n 2)...3 2 1, onde 0 != 1
3-4
Para calcular o fatorial de um nmero atravs de um programa de computador, utilizamos tipicamente um processo iterativo, em que o valor da varivel varia de 1 a n. A linguagem C oferece diversas construes possveis para a realizao de laos iterativos. O primeiro a ser apresentado o comando while. Sua forma geral :
while (expr) { bloco de comandos ... }
Enquanto expr for avaliada em verdadeiro, o bloco de comandos executado repetidamente. Se expr for avaliada em falso, o bloco de comando no executado e a execuo do programa prossegue. Uma possvel implementao do clculo do fatorial usando while mostrada a seguir.
/* Fatorial */ #include <stdio.h> int main (void) { int i; int n; int f = 1; printf("Digite um nmero inteiro nao negativo:"); scanf("%d", &n); /* calcula fatorial */ i = 1; while (i <= n) { f *= i; i++; } printf(" Fatorial = %d \n", f); return 0;
Uma segunda forma de construo de laos em C, mais compacta e amplamente utilizada, atravs de laos for. A forma geral do for :
for (expr_inicial; expr_booleana; expr_de_incremento) { bloco de comandos ... }
3-5
Observamos que as chaves que seguem o comando for, neste caso, so desnecessrias, j que o corpo do bloco composto por um nico comando. Tanto a construo com while como a construo com for avaliam a expresso booleana que caracteriza o teste de encerramento no incio do lao. Assim, se esta expresso tiver valor igual a zero (falso), quando for avaliada pela primeira vez, os comandos do corpo do bloco no sero executados nem uma vez. C prov outro comando para construo de laos cujo teste de encerramento avaliado no final. Esta construo o do-while, cuja forma geral :
do {
Um exemplo do uso desta construo mostrado abaixo, onde validamos a insero do usurio, isto , o programa repetidamente requisita a insero de um nmero enquanto o usurio inserir um inteiro negativo (cujo fatorial no est definido).
3-6
/* Fatorial (versao 3) */ #include <stdio.h> int main (void) { int i; int n; int f = 1; /* requisita valor do usurio */ do { printf("Digite um valor inteiro nao negativo:"); scanf ("%d", &n); } while (n<0); /* calcula fatorial */ for (i = 1; i <= n; i++) f *= i; printf(" Fatorial = %d\n", f); return 0;
Interrupes com break e continue A linguagem C oferece ainda duas formas para a interrupo antecipada de um determinado lao. O comando break, quando utilizado dentro de um lao, interrompe e termina a execuo do mesmo. A execuo prossegue com os comandos subseqentes ao bloco. O cdigo abaixo ilustra o efeito de sua utilizao.
#include <stdio.h> int main (void) { int i; for (i = 0; i < 10; i++) { if (i == 5) break; printf("%d ", i); } printf("fim\n"); return 0; }
pois, quando i tiver o valor 5, o lao ser interrompido e finalizado pelo comando break, passando o controle para o prximo comando aps o lao, no caso uma chamada final de printf.
3-7
O comando continue tambm interrompe a execuo dos comandos de um lao. A diferena bsica em relao ao comando break que o lao no automaticamente finalizado. O comando continue interrompe a execuo de um lao passando para a prxima iterao. Assim, o cdigo:
#include <stdio.h> int main (void) { int i; for (i = 0; i < 10; i++ ) { if (i == 5) continue; printf("%d ", i); } printf("fim\n"); return 0; }
gera a sada:
0 1 2 3 4 6 7 8 9 fim
Devemos ter cuidado com a utilizao do comando continue nos laos while. O programa:
/* INCORRETO */ #include <stdio.h> int main (void) { int i = 0; while (i < 10) { if (i == 5) continue; printf("%d ", i); i++; } printf("fim\n"); return 0; }
um programa INCORRETO, pois o lao criado no tem fim a execuo do programa no termina. Isto porque a varivel i nunca ter valor superior a 5, e o teste ser sempre verdadeiro. O que ocorre que o comando continue "pula" os demais comandos do lao quando i vale 5, inclusive o comando que incrementa a varivel i.
3.3. Seleo
Alm da construo else-if, C prov um comando (switch) para selecionar um dentre um conjunto de possveis casos. Sua forma geral :
3-8
switch ( expr ) { case op1: ... /* comandos executados se expr == op1 break; case op2: ... /* comandos executados se expr == op2 break; case op3: ... /* comandos executados se expr == op3 break; default: ... /* executados se expr for diferente de break; }
*/ */ */ todos */
opi deve ser um nmero inteiro ou uma constante caractere. Se expr resultar no valor opi, os comandos que se seguem ao caso opi so executados, at que se encontre um break. Se o comando break for omitido, a execuo do caso continua com os comandos do caso seguinte. O caso default (nenhum dos outros) pode aparecer em qualquer posio, mas
normalmente colocado por ltimo. Para exemplificar, mostramos a seguir um programa que implementa uma calculadora convencional que efetua as quatro operaes bsicas. Este programa usa constantes caracteres, que sero discutidas em detalhe quando apresentarmos cadeias de caracteres em C. O importante aqui entender conceitualmente a construo switch.
/* calculadora de quatro operaes */ #include <stdio.h> int main (void) { float num1, num2; char op; printf("Digite: numero op numero\n"); scanf ("%f %c %f", &num1, &op, &num2); switch (op) { case '+': printf(" = %f\n", num1+num2); break; case '-': printf(" = %f\n", num1-num2); break; case '*': printf(" = %f\n", num1*num2); break; case '/': printf(" = %f\n", num1/num2); break; default: printf("Operador invalido!\n"); break; } return 0; }
3-9
4. Funes
W. Celes e J. L. Rangel
Para ilustrar a criao de funes, consideraremos o clculo do fatorial de um nmero. Podemos escrever uma funo que, dado um determinado nmero inteiro no negativo n, imprime o valor de seu fatorial. Um programa que utiliza esta funo seria:
/* programa que le um numero e imprime seu fatorial */ #include <stdio.h> void fat (int n); /* Funo principal */ int main (void) { int n; scanf("%d", &n); fat(n); return 0; } /* Funo para imprimir o valor do fatorial */ void fat ( int n ) { int i; int f = 1; for (i = 1; i <= n; i++) f *= i; printf("Fatorial = %d\n", f); }
4-1
Notamos, neste exemplo, que a funo fat recebe como parmetro o nmero cujo fatorial deve ser impresso. Os parmetros de uma funo devem ser listados, com seus respectivos tipos, entre os parnteses que seguem o nome da funo. Quando uma funo no tem parmetros, colocamos a palavra reservada void entre os parnteses. Devemos notar que main tambm uma funo; sua nica particularidade consiste em ser a funo automaticamente executada aps o programa ser carregado. Como as funes main que temos apresentado no recebem parmetros, temos usado a palavra void na lista de parmetros. Alm de receber parmetros, uma funo pode ter um valor de retorno associado. No exemplo do clculo do fatorial, a funo fat no tem nenhum valor de retorno, portanto colocamos a palavra void antes do nome da funo, indicando a ausncia de um valor de retorno.
void fat (int n) { . . . }
A funo main obrigatoriamente deve ter um valor inteiro como retorno. Esse valor pode ser usado pelo sistema operacional para testar a execuo do programa. A conveno geralmente utilizada faz com que a funo main retorne zero no caso da execuo ser bem sucedida ou diferente de zero no caso de problemas durante a execuo. Por fim, salientamos que C exige que se coloque o prottipo da funo antes desta ser chamada. O prottipo de uma funo consiste na repetio da linha de sua definio seguida do caractere (;). Temos ento:
void fat (int n); int main (void) { . . . } void fat (int n) { . . . } /* obs: nao existe ; na definio */ /* obs: existe ; no prottipo */
A rigor, no prottipo no h necessidade de indicarmos os nomes dos parmetros, apenas os seus tipos, portanto seria vlido escrever: void fat (int);. Porm, geralmente mantemos os nomes dos parmetros, pois servem como documentao do significado de cada parmetro, desde que utilizemos nomes coerentes. O prottipo da funo necessrio para que o compilador verifique os tipos dos parmetros na chamada da funo. Por exemplo, se tentssemos chamar a funo com fat(4.5); o compilador provavelmente indicaria o erro, pois estaramos passando um valor real enquanto a funo espera um valor inteiro. devido a esta necessidade que se exige a incluso do arquivo stdio.h para a utilizao das funes de entrada e sada da biblioteca padro. Neste arquivo, encontram-se, entre outras coisas, os prottipos das funes printf e scanf.
Estruturas de Dados PUC-Rio 4-2
Uma funo pode ter um valor de retorno associado. Para ilustrar a discusso, vamos reescrever o cdigo acima, fazendo com que a funo fat retorne o valor do fatorial. A funo main fica ento responsvel pela impresso do valor.
/* programa que le um numero e imprime seu fatorial (verso 2) */ #include <stdio.h> int fat (int n); int main (void) { int n, r; scanf("%d", &n); r = fat(n); printf("Fatorial = %d\n", r); return 0; } /* funcao para calcular o valor do fatorial */ int fat (int n) { int i; int f = 1; for (i = 1; i <= n; i++) f *= i; return f; }
4-3
/* programa que le um numero e imprime seu fatorial (verso 3) */ #include <stdio.h> int fat (int n); int main (void) { int n = 5; int r; r = fat ( n ); printf("Fatorial de %d = %d \n", n, r); return 0; } int fat (int n) { int f = 1.0; while (n != 0) { f *= n; n--; } return f; }
Neste exemplo, podemos verificar que, no final da funo fat, o parmetro n tem valor igual a zero (esta a condio de encerramento do lao while). No entanto, a sada do programa ser:
Fatorial de 5 = 120
pois o valor da varivel n no mudou no programa principal. Isto porque a linguagem C trabalha com o conceito de passagem por valor. Na chamada de uma funo, o valor passado atribudo ao parmetro da funo chamada. Cada parmetro funciona como uma varivel local inicializada com o valor passado na chamada. Assim, a varivel n (parmetro da funo fat) local e no representa a varivel n da funo main (o fato de as duas variveis terem o mesmo nome indiferente; poderamos chamar o parmetro de v, por exemplo). Alterar o valor de n dentro de fat no afeta o valor da varivel n de main. A execuo do programa funciona com o modelo de pilha. De forma simplificada, o modelo de pilha funciona da seguinte maneira: cada varivel local de uma funo colocada na pilha de execuo. Quando se faz uma chamada a uma funo, os parmetros so copiados para a pilha e so tratados como se fossem variveis locais da funo chamada. Quando a funo termina, a parte da pilha correspondente quela funo liberada, e por isso no podemos acessar as variveis locais de fora da funo em que elas foram definidas. Para exemplificar, vamos considerar um esquema representativo da memria do computador. Salientamos que este esquema apenas uma maneira didtica de explicar o que ocorre na memria do computador. Suponhamos que as variveis so armazenadas na memria como ilustrado abaixo. Os nmeros direita representam endereos (posies)
Estruturas de Dados PUC-Rio 4-4
fictcios de memria e os nomes esquerda indicam os nomes das variveis. A figura abaixo ilustra este esquema representativo da memria que adotaremos.
c b a
'x' 43.5 7
112 - varivel c no endereo 112 com valor igual a 'x' 108 - varivel b no endereo 108 com valor igual a 43.5 104 - varivel a no endereo 104 com valor igual a 7
Podemos, ento, analisar passo a passo a evoluo do programa mostrado acima, ilustrando o funcionamento da pilha de execuo.
1 - Incio do programa: pilha vazia 2 - Declarao das variveis: n, r 3 - Chamada da funo: cpia do parmetro
n r n
5 5
5 - Final do lao
1.0 5 5
120.0 0 5
main >
r n
120.0 5
Isto ilustra por que o valor da varivel passada nunca ser alterado dentro da funo. A seguir, discutiremos uma forma para podermos alterar valores por passagem de parmetros, o que ser realizado passando o endereo de memria onde a varivel est armazenada. Vale salientar que existe outra forma de fazermos comunicao entre funes, que consiste no uso de variveis globais. Se uma determinada varivel global visvel em duas funes, ambas as funes podem acessar e/ou alterar o valor desta varivel diretamente. No
Estruturas de Dados PUC-Rio 4-5
entanto, conforme j mencionamos, o uso de variveis globais em um programa deve ser feito com critrio, pois podemos criar cdigos com uma alto grau de interdependncia entre as funes, o que dificulta a manuteno e o reuso do cdigo.
declaramos uma varivel com nome a que pode armazenar valores inteiros. Automaticamente, reserva-se um espao na memria suficiente para armazenar valores inteiros (geralmente 4 bytes). Da mesma forma que declaramos variveis para armazenar inteiros, podemos declarar variveis que, em vez de servirem para armazenar valores inteiros, servem para armazenar valores de endereos de memria onde h variveis inteiras armazenadas. C no reserva uma palavra especial para a declarao de ponteiros; usamos a mesma palavra do tipo com os nomes das variveis precedidas pelo caractere *. Assim, podemos escrever:
int *p;
Neste caso, declaramos uma varivel com nome p que pode armazenar endereos de memria onde existe um inteiro armazenado. Para atribuir e acessar endereos de memria, a linguagem oferece dois operadores unrios ainda no discutidos. O operador unrio & (endereo de), aplicado a variveis, resulta no endereo da posio da memria reservada para a varivel. O operador unrio * (contedo de), aplicado a variveis do tipo ponteiro, acessa o contedo do endereo de memria armazenado pela varivel ponteiro. Para exemplificar, vamos ilustrar esquematicamente, atravs de um exemplo simples, o que ocorre na pilha de execuo. Consideremos o trecho de cdigo mostrado na figura abaixo.
/*varivel inteiro */
int a;
int *p;
p a
4-6
Aps as declaraes, ambas as variveis, a e p, armazenam valores "lixo", pois no foram inicializadas. Podemos fazer atribuies como exemplificado nos fragmentos de cdigo da figura a seguir:
/* a recebe o valor 5 */
a = 5;
p a
p = &a;
/* contedo de p recebe o valor 6 */
p a
104 5
*p = 6;
p a
104 6
Com as atribuies ilustradas na figura, a varivel a recebe, indiretamente, o valor 6. Acessar a equivalente a acessar *p, pois p armazena o endereo de a. Dizemos que p aponta para a, da o nome ponteiro. Em vez de criarmos valores fictcios para os endereos de memria no nosso esquema ilustrativo da memria, podemos desenhar setas graficamente, sinalizando que um ponteiro aponta para uma determinada varivel.
p a 6
A possibilidade de manipular ponteiros de variveis uma das maiores potencialidades de C. Por outro lado, o uso indevido desta manipulao o maior causador de programas que "voam", isto , no s no funcionam como, pior ainda, podem gerar efeitos colaterais no previstos.
4-7
cometemos um ERRO tpico de manipulao de ponteiros. O pior que esse programa, embora incorreto, s vezes pode funcionar. O erro est em usar a memria apontada por p para armazenar o valor 3. Ora, a varivel p no tinha sido inicializada e, portanto, tinha armazenado um valor (no caso, endereo) "lixo". Assim, a atribuio *p = 3; armazena 3 num espao de memria desconhecido, que tanto pode ser um espao de memria no utilizado, e a o programa aparentemente funciona bem, quanto um espao que armazena outras informaes fundamentais por exemplo, o espao de memria utilizado por outras variveis ou outros aplicativos. Neste caso, o erro pode ter efeitos colaterais indesejados. Portanto, s podemos preencher o contedo de um ponteiro se este tiver sido devidamente inicializado, isto , ele deve apontar para um espao de memria onde j se prev o armazenamento de valores do tipo em questo. De maneira anloga, podemos declarar ponteiros de outros tipos:
float *m; char *s;
Passando ponteiros para funes Os ponteiros oferecem meios de alterarmos valores de variveis acessando-as indiretamente. J discutimos que as funes no podem alterar diretamente valores de variveis da funo que fez a chamada. No entanto, se passarmos para uma funo os valores dos endereos de memria onde suas variveis esto armazenadas, a funo pode alterar, indiretamente, os valores das variveis da funo que a chamou.
4-8
Vamos analisar o uso desta estratgia atravs de um exemplo. Consideremos uma funo projetada para trocar os valores entre duas variveis. O cdigo abaixo:
/* funcao troca (versao ERRADA) */ #include <stdio.h> void troca (int x, int y ) { int temp; temp = x; x = y; y = temp; } int main ( void ) { int a = 5, b = 7; troca(a, b); printf("%d %d \n", a, b); return 0; }
no funciona como esperado (sero impressos 5 e 7), pois os valores de a e b da funo main no so alterados. Alterados so os valores de x e y dentro da funo troca, mas eles no representam as variveis da funo main, apenas so inicializados com os valores de a e b. A alternativa fazer com que a funo receba os endereos das variveis e, assim, alterar seus valores indiretamente. Reescrevendo:
/* funcao troca (versao CORRETA) */ #include <stdio.h> void troca (int *px, int *py ) { int temp; temp = *px; *px = *py; *py = temp; } int main ( void ) { int a = 5, b = 7; troca(&a, &b); /* passamos os endereos das variveis */ printf("%d %d \n", a, b); return 0; }
A Figura 4.6 ilustra a execuo deste programa mostrando o uso da memria. Assim, conseguimos o efeito desejado. Agora fica explicado por que passamos o endereo das variveis para a funo scanf, pois, caso contrrio, a funo no conseguiria devolver os valores lidos.
4-9
main
b a >
7 5
troca main
py px >b a >
108 104 7 5
4.4. Recursividade
As funes podem ser chamadas recursivamente, isto , dentro do corpo de uma funo podemos chamar novamente a prpria funo. Se uma funo A chama a prpria funo A, dizemos que ocorre uma recurso direta. Se uma funo A chama uma funo B que, por sua vez, chama A, temos uma recurso indireta. Diversas implementaes ficam muito mais fceis usando recursividade. Por outro lado, implementaes no recursivas tendem a ser mais eficientes. Para cada chamada de uma funo, recursiva ou no, os parmetros e as variveis locais so empilhados na pilha de execuo. Assim, mesmo quando uma funo chamada recursivamente, cria-se um ambiente local para cada chamada. As variveis locais de chamadas recursivas so independentes entre si, como se estivssemos chamando funes diferentes.
Estruturas de Dados PUC-Rio 4-10
As implementaes recursivas devem ser pensadas considerando-se a definio recursiva do problema que desejamos resolver. Por exemplo, o valor do fatorial de um nmero pode ser definido de forma recursiva:
1, se n = 0 n!= n (n 1)!, se n > 0 Considerando a definio acima, fica muito simples pensar na implementao recursiva de uma funo que calcula e retorna o fatorial de um nmero.
/* Funo recursiva para calculo do fatorial */ int fat (int n) { if (n==0) return 1; else return n*fat(n-1); }
Se uma varivel esttica no for explicitamente inicializada na declarao, ela automaticamente inicializada com zero. (As variveis globais tambm so, por default, inicializadas com zero.)
Estruturas de Dados PUC-Rio 4-11
Neste caso, antes da compilao, toda ocorrncia da palavra PI (desde que no envolvida por aspas) ser trocada pelo nmero 3.14159. O uso de diretivas de definio para representarmos constantes simblicas fortemente recomendvel, pois facilita a manuteno e acrescenta clareza ao cdigo. C permite ainda a utilizao da diretiva de definio com parmetros. vlido escrever, por exemplo:
#define MAX(a,b) ((a) > (b) ? (a) : (b))
assim, se aps esta definio existir uma linha de cdigo com o trecho:
v = 4.5; c = MAX ( v, 3.0 );
o compilador ver:
v = 4.5; c = ((v) > (4.5) ? (v) : (4.5));
Estas definies com parmetros recebem o nome de macros. Devemos ter muito cuidado na definio de macros. Mesmo um erro de sintaxe pode ser difcil de ser detectado, pois o
Estruturas de Dados PUC-Rio 4-12
compilador indicar um erro na linha em que se utiliza a macro e no na linha de definio da macro (onde efetivamente encontra-se o erro). Outros efeitos colaterais de macros mal definidas podem ser ainda piores. Por exemplo, no cdigo abaixo:
#include <stdio.h> #define DIF(a,b) a - b
o resultado impresso 17 e no 8, como poderia ser esperado. A razo simples, pois para o compilador (fazendo a substituio da macro) est escrito:
printf(" %d ", 4 * 5 - 3);
e a multiplicao tem precedncia sobre a subtrao. Neste caso, parnteses envolvendo a macro resolveriam o problema. Porm, neste outro exemplo que envolve a macro com parnteses:
#include <stdio.h> #define PROD(a,b) (a * b)
Conclumos, portanto, que, como regra bsica para a definio de macros, devemos envolver cada parmetro, e a macro como um todo, com parnteses.
4-13
5.1. Vetores
A forma mais simples de estruturarmos um conjunto de dados por meio de vetores. Como a maioria das linguagens de programao, C permite a definio de vetores. Definimos um vetor em C da seguinte forma:
int v[10];
A declarao acima diz que v um vetor de inteiros dimensionado com 10 elementos, isto , reservamos um espao de memria contnuo para armazenar 10 valores inteiros. Assim, se cada int ocupa 4 bytes, a declarao acima reserva um espao de memria de 40 bytes, como ilustra a figura abaixo.
144
104
O acesso a cada elemento do vetor feito atravs de uma indexao da varivel v. Observamos que, em C, a indexao de um vetor varia de zero a n-1, onde n representa a dimenso do vetor. Assim:
v[0] v[1] ... v[9] acessa o primeiro elemento de v acessa o segundo elemento de v acessa o ltimo elemento de v
Mas:
v[10] est ERRADO (invaso de memria)
5-1
Para exemplificar o uso de vetores, vamos considerar um programa que l 10 nmeros reais, fornecidos via teclado, e calcula a mdia e a varincia destes nmeros. A mdia e a varincia so dadas por:
x m= , N
(x m ) v=
N
/* declara vetor com 10 elementos */ /* variveis para armazenar a mdia e a varincia */ /* varivel usada como ndice do vetor */ /* faz ndice variar de 0 a 9 */ /* l cada elemento do vetor */ /* inicializa mdia com zero */ /* acumula soma dos elementos */ /* calcula a mdia */
/* leitura dos valores */ for (i = 0; i < 10; i++) scanf("%f", &v[i]); /* clculo da mdia */ med = 0.0; for (i = 0; i < 10; i++) med = med + v[i]; med = med / 10;
/* clculo da varincia */ var = 0.0; /* inicializa varincia com zero */ for ( i = 0; i < 10; i++ ) var = var+(v[i]-med)*(v[i]-med); /* acumula quadrado da diferena */ var = var / 10; /* calcula a varincia */ printf ( "Media = %f return 0; Variancia = %f \n", med, var );
Devemos observar que passamos para a funo scanf o endereo de cada elemento do vetor (&v[i]), pois desejamos que os valores capturados sejam armazenados nos elementos do vetor. Se v[i] representa o (i+1)-simo elemento do vetor, &v[i] representa o endereo de memria onde esse elemento est armazenado. Na verdade, existe uma associao forte entre vetores e ponteiros, pois se existe a declarao:
int v[10];
a varivel v, que representa o vetor, uma constante que armazena o endereo inicial do vetor, isto , v, sem indexao, aponta para o primeiro elemento do vetor.
5-2
A linguagem C tambm suporta aritmtica de ponteiros. Podemos somar e subtrair ponteiros, desde que o valor do ponteiro resultante aponte para dentro da rea reservada para o vetor. Se p representa um ponteiro para um inteiro, p+1 representa um ponteiro para o prximo inteiro armazenado na memria, isto , o valor de p incrementado de 4 (mais uma vez assumindo que um inteiro tem 4 bytes). Com isto, num vetor temos as seguintes equivalncias:
v+0 v+1 v+2 ... v+9 aponta para o primeiro elemento do vetor aponta para o segundo elemento do vetor aponta para o terceiro elemento do vetor aponta para o ltimo elemento do vetor
Portanto, escrever &v[i] equivalente a escrever (v+i). De maneira anloga, escrever v[i] equivalente a escrever *(v+i) ( lgico que a forma indexada mais clara e adequada). Devemos notar que o uso da aritmtica de ponteiros aqui perfeitamente vlido, pois os elementos dos vetores so armazenados de forma contnua na memria. Os vetores tambm podem ser inicializados na declarao:
int v[5] = { 5, 10, 15, 20, 25 };
ou simplesmente:
int v[] = { 5, 10, 15, 20, 25 };
Neste ltimo caso, a linguagem dimensiona o vetor pelo nmero de elementos inicializados.
Passagem de vetores para funes Passar um vetor para uma funo consiste em passar o endereo da primeira posio do vetor. Se passarmos um valor de endereo, a funo chamada deve ter um parmetro do tipo ponteiro para armazenar este valor. Assim, se passarmos para uma funo um vetor de int, devemos ter um parmetro do tipo int*, capaz de armazenar endereos de inteiros. Salientamos que a expresso passar um vetor para uma funo deve ser interpretada como passar o endereo inicial do vetor. Os elementos do vetor no so copiados para a funo, o argumento copiado apenas o endereo do primeiro elemento.
Para exemplificar, vamos modificar o cdigo do exemplo acima, usando funes separadas para o clculo da mdia e da varincia. (Aqui, usamos ainda os operadores de atribuio += para acumular as somas.)
/* Clculo da media e da varincia de 10 reais (segunda verso) */ #include <stdio.h> /* Funo para clculo da mdia */ float media (int n, float* v) { int i;
Estruturas de Dados PUC-Rio 5-3
float s = 0.0; for (i = 0; i < n; i++) s += v[i]; return s/n; } /* Funo para clculo da varincia */ float variancia (int n, float* v, float m) { int i; float s = 0.0; for (i = 0; i < n; i++) s += (v[i] - m) * (v[i] - m); return s/n; } int main ( void ) { float v[10]; float med, var; int i; /* leitura dos valores */ for ( i = 0; i < 10; i++ ) scanf("%f", &v[i]); med = media(10,v); var = variancia(10,v,med); printf ( "Media = %f return 0; Variancia = %f \n", med, var);
Observamos ainda que, como passado para a funo o endereo do primeiro elemento do vetor (e no os elementos propriamente ditos), podemos alterar os valores dos elementos do vetor dentro da funo. O exemplo abaixo ilustra:
/* Incrementa elementos de um vetor */ #include <stdio.h> void incr_vetor ( int n, int *v ) { int i; for (i = 0; i < n; i++) v[i]++; } int main ( void ) { int a[ ] = {1, 3, 5}; incr_vetor(3, a); printf("%d %d %d \n", a[0], a[1], a[2]); return 0; }
5-4
Uso da memria Informalmente, podemos dizer que existem trs maneiras de reservarmos espao de memria para o armazenamento de informaes. A primeira delas atravs do uso de variveis globais (e estticas). O espao reservado para uma varivel global existe enquanto o programa estiver sendo executado. A segunda maneira atravs do uso de variveis locais. Neste caso, como j discutimos, o espao existe apenas enquanto a funo que declarou a varivel est sendo executada, sendo liberado para outros usos quando a execuo da funo termina. Por este motivo, a funo que chama no pode fazer referncia ao espao local da funo chamada. As variveis globais ou locais podem ser simples ou vetores. Para os vetores, precisamos informar o nmero mximo de elementos, caso contrrio o compilador no saberia o tamanho do espao a ser reservado.
A terceira maneira de reservarmos memria requisitando ao sistema, em tempo de execuo, um espao de um determinado tamanho. Este espao alocado dinamicamente permanece reservado at que explicitamente seja liberado pelo programa. Por isso, podemos alocar dinamicamente um espao de memria numa funo e acess-lo em outra. A partir do momento que liberarmos o espao, ele estar disponibilizado para outros usos e no podemos mais acess-lo. Se o programa no liberar um espao alocado, este ser automaticamente liberado quando a execuo do programa terminar. Apresentamos abaixo um esquema didtico que ilustra de maneira fictcia a distribuio do uso da memria pelo sistema operacional1.
A rigor, a alocao dos recursos bem mais complexa e varia para cada sistema operacional.
5-5
Cdigo do Programa
Memria Livre
Pilha
Quando requisitamos ao sistema operacional para executar um determinado programa, o cdigo em linguagem de mquina do programa deve ser carregado na memria, conforme discutido no primeiro captulo. O sistema operacional reserva tambm os espaos necessrios para armazenarmos as variveis globais (e estticas) existentes no programa. O restante da memria livre utilizado pelas variveis locais e pelas variveis alocadas dinamicamente. Cada vez que uma determinada funo chamada, o sistema reserva o espao necessrio para as variveis locais da funo. Este espao pertence pilha de execuo e, quando a funo termina, desempilhado. A parte da memria no ocupada pela pilha de execuo pode ser requisitada dinamicamente. Se a pilha tentar crescer mais do que o espao disponvel existente, dizemos que ela estourou e o programa abortado com erro. Similarmente, se o espao de memria livre for menor que o espao requisitado dinamicamente, a alocao no feita e o programa pode prever um tratamento de erro adequado (por exemplo, podemos imprimir a mensagem Memria insuficiente e interromper a execuo do programa).
Funes da biblioteca padro Existem funes, presentes na biblioteca padro stdlib, que permitem alocar e liberar memria dinamicamente. A funo bsica para alocar memria malloc. Ela recebe como parmetro o nmero de bytes que se deseja alocar e retorna o endereo inicial da rea de memria alocada.
Para exemplificar, vamos considerar a alocao dinmica de um vetor de inteiros com 10 elementos. Como a funo malloc retorna o endereo da rea alocada e, neste exemplo, desejamos armazenar valores inteiros nessa rea, devemos declarar um ponteiro de inteiro para receber o endereo inicial do espao alocado. O trecho de cdigo ento seria:
int *v; v = malloc(10*4);
Aps este comando, se a alocao for bem sucedida, v armazenar o endereo inicial de uma rea contnua de memria suficiente para armazenar 10 valores inteiros. Podemos,
Estruturas de Dados PUC-Rio 5-6
ento, tratar v como tratamos um vetor declarado estaticamente, pois, se v aponta para o inicio da rea alocada, podemos dizer que v[0] acessa o espao para o primeiro elemento que armazenaremos, v[1] acessa o segundo, e assim por diante (at v[9]). No exemplo acima, consideramos que um inteiro ocupa 4 bytes. Para ficarmos independentes de compiladores e mquinas, usamos o operador sizeof( ).
v = malloc(10*sizeof(int));
Alm disso, devemos lembrar que a funo malloc usada para alocar espao para armazenarmos valores de qualquer tipo. Por este motivo, malloc retorna um ponteiro genrico, para um tipo qualquer, representado por void*, que pode ser convertido automaticamente pela linguagem para o tipo apropriado na atribuio. No entanto, comum fazermos a converso explicitamente, utilizando o operador de molde de tipo (cast). O comando para a alocao do vetor de inteiros fica ento:
v = (int *) malloc(10*sizeof(int));
504
Livre
Livre v 504
Se, porventura, no houver espao livre suficiente para realizar a alocao, a funo retorna um endereo nulo (representado pelo smbolo NULL, definido em stdlib.h). Podemos cercar o erro na alocao do programa verificando o valor de retorno da funo malloc. Por exemplo, podemos imprimir uma mensagem e abortar o programa com a funo exit, tambm definida na stdlib.
5-7
v = (int*) malloc(10*sizeof(int)); if (v==NULL) { printf("Memoria insuficiente.\n"); exit(1); /* aborta o programa e retorna 1 para o sist. operacional */ }
Para liberar um espao de memria alocado dinamicamente, usamos a funo free. Esta funo recebe como parmetro o ponteiro da memria a ser liberada. Assim, para liberar o vetor v, fazemos:
free (v);
S podemos passar para a funo free um endereo de memria que tenha sido alocado dinamicamente. Devemos lembrar ainda que no podemos acessar o espao na memria depois que o liberamos. Para exemplificar o uso da alocao dinmica, alteraremos o programa para o clculo da mdia e da varincia mostrado anteriormente. Agora, o programa l o nmero de valores que sero fornecidos, aloca um vetor dinamicamente e faz os clculos. Somente a funo principal precisa ser alterada, pois as funes para calcular a mdia e a varincia anteriormente apresentadas independem do fato de o vetor ter sido alocado esttica ou dinamicamente.
/* Clculo da mdia e da varincia de n reais */ #include <stdio.h> #include <stdlib.h> ... int main ( void ) { int i, n; float *v; float med, var; /* leitura do nmero de valores */ scanf("%d", &n); /* alocao dinmica */ v = (float*) malloc(n*sizeof(float)); if (v==NULL) { printf("Memoria insuficiente.\n); return 1; } /* leitura dos valores */ for (i = 0; i < n; i++) scanf("%f", &v[i]); med = media(n,v); var = variancia(n,v,med); printf("Media = %f Variancia = %f \n", med, var); /* libera memria */ free(v); return 0;
5-8
6. Cadeia de caracteres
W. Celes e J. L. Rangel
6.1. Caracteres
Efetivamente, a linguagem C no oferece um tipo caractere. Os caracteres so representados por cdigos numricos. A linguagem oferece o tipo char, que pode armazenar valores inteiros pequenos: um char tem tamanho de 1 byte, 8 bits, e sua verso com sinal pode representar valores que variam de 128 a 127. Como os cdigos associados aos caracteres esto dentro desse intervalo, usamos o tipo char para representar caracteres1. A correspondncia entre os caracteres e seus cdigos numricos feita por uma tabela de cdigos. Em geral, usa-se a tabela ASCII, mas diferentes mquinas podem usar diferentes cdigos. Contudo, se desejamos escrever cdigos portteis, isto , que possam ser compilados e executados em mquinas diferentes, devemos evitar o uso explcito dos cdigos referentes a uma determinada tabela, como ser discutido nos exemplos subseqentes. Como ilustrao, mostramos a seguir os cdigos associados a alguns caracteres segundo a tabela ASCII. Alguns caracteres que podem ser impressos (sp representa o branco, ou espao): 0 30 40 50 60 70 80 90 100 110 120
( 2 < F P Z d n x
1
) 3 = G Q [ e o y
2
sp * 4 > H R \ f p z
3
! + 5 ? I S ] g q {
4
" , 6 @ J T ^ h r |
5
# 7 A K U _ i S }
6
$ . 8 B L V ` j t ~
7
% / 9 C M W a k u
8
& 0 : D N X b l v
9
' 1 ; E O Y c m w
null: nulo bell: campainha backspace: voltar e apagar um caractere tab ou tabulao horizontal newline ou line feed: mudana de linha carriage return: volta ao incio da linha delete: apagar um caractere
Alguns alfabetos precisam de maior representatividade. O alfabeto chins, por exemplo, tem mais de 256 caracteres, no sendo suficiente o tipo char (alguns compiladores oferecem o tipo wchar, para estes casos).
6-1
Em C, a diferena entre caracteres e inteiros feita apenas atravs da maneira pela qual so tratados. Por exemplo, podemos imprimir o mesmo valor de duas formas diferentes usando formatos diferentes. Vamos analisar o fragmento de cdigo abaixo:
char c = 97; printf("%d %c\n",c,c);
Considerando a codificao de caracteres via tabela ASCII, a varivel c, que foi inicializada com o valor 97, representa o caractere a. A funo printf imprime o contedo da varivel c usando dois formatos distintos: com o especificador de formato para inteiro, %d, ser impresso o valor do cdigo numrico, 97; com o formato de caractere, %c, ser impresso o caractere associado ao cdigo, a letra a. Conforme mencionamos, devemos evitar o uso explcito de cdigos de caracteres. Para tanto, a linguagem C permite a escrita de constantes caracteres. Uma constante caractere escrita envolvendo o caractere com aspas simples. Assim, a expresso 'a' representa uma constante caractere e resulta no valor numrico associado ao caractere a. Podemos, ento, reescrever o fragmento de cdigo acima sem particularizar a tabela ASCII.
char c = 'a'; printf("%d %c\n", c, c);
Alm de agregar portabilidade e clareza ao cdigo, o uso de constantes caracteres nos livra de conhecermos os cdigos associados a cada caractere. Independente da tabela de cdigos numricos utilizada, garante-se que os dgitos so codificados em seqncia. Deste modo, se o dgito zero tem cdigo 48, o dgito um tem obrigatoriamente cdigo 49, e assim por diante. As letras minsculas e as letras maisculas tambm formam dois grupos de cdigos seqenciais. O exemplo a seguir tira proveito desta seqncia dos cdigos de caracteres. Exemplo. Suponhamos que queremos escrever uma funo para testar se um caractere c um dgito (um dos caracteres entre '0' e '9'). Esta funo pode ter o prottipo:
int digito(char c);
e ter como resultado 1 (verdadeiro) se c for um dgito, e 0 (falso) se no for. A implementao desta funo pode ser dada por:
int digito(char c) { if ((c>='0')&&(c<='9')) return 1; else return 0; }
6-2
Exerccio. Escreva uma funo para determinar se um caractere uma letra, com prottipo:
int letra(char c);
Exerccio. Escreva uma funo para converter um caractere para maiscula. Se o caractere dado representar uma letra minscula, devemos ter como valor de retorno a letra maiscula correspondente. Se o caractere dado no for uma letra minscula, devemos ter como valor de retorno o mesmo caractere, sem alterao. O prottipo desta funo pode ser dado por:
char maiuscula(char c);
Se o caractere '\0' no fosse colocado, a funo printf executaria de forma errada, pois no conseguiria identificar o final da cadeia. Como as cadeias de caracteres so vetores, podemos reescrever o cdigo acima inicializando os valores dos elementos do vetor na declarao:
int main ( void )
Estruturas de Dados PUC-Rio 6-3
{ char cidade[ ] = {'R', 'i', 'o', '\0'}; printf("%s \n", cidade); return 0; }
A inicializao de cadeias de caracteres to comum em cdigos C que a linguagem permite que elas sejam inicializadas escrevendo-se os caracteres entre aspas duplas. Neste caso, o caractere nulo representado implicitamente. O cdigo acima pode ser reescrito da seguinte forma:
int main ( void ) { char cidade[ ] = "Rio"; printf("%s \n", cidade); return 0; }
A varivel cidade automaticamente dimensionada e inicializada com 4 elementos. Para ilustrar a declarao e inicializao de cadeias de caracteres, consideremos as declaraes abaixo:
char char char char s1[] = ""; s2[] = "Rio de Janeiro"; s3[81]; s4[81] = "Rio";
Nestas declaraes, a varivel s1 armazena uma cadeia de caracteres vazia, representada por um vetor com um nico elemento, o caractere '\0'. A varivel s2 representa um vetor com 15 elementos. A varivel s3 representa uma cadeia de caracteres capaz de representar cadeias com at 80 caracteres, j que foi dimensionada com 81 elementos. Esta varivel, no entanto, no foi inicializada e seu contedo desconhecido. A varivel s4 tambm foi dimensionada para armazenar cadeias at 80 caracteres, mas seus primeiros quatro elementos foram atribudos na declarao. Leitura de caracteres e cadeias de caracteres Para capturarmos o valor de um caractere simples fornecido pelo usurio via teclado, usamos a funo scanf, com o especificador de formato %c.
char a; ... scanf("%c", &a); ...
Desta forma, se o usurio digitar a letra r, por exemplo, o cdigo associado letra r ser armazenado na varivel a. Vale ressaltar que, diferente dos especificadores %d e %f, o especificador %c no pula os caracteres brancos2. Portanto, se o usurio teclar um espao
Um caractere branco pode ser um espao (' '), um caractere de tabulao ('\t') ou um caractere de nova linha ('\n').
Estruturas de Dados PUC-Rio 6-4
2
antes da letra r, o cdigo do espao ser capturado e a letra r ser capturada apenas numa prxima chamada da funo scanf. Se desejarmos pular todas as ocorrncias de caracteres brancos que porventura antecedam o caractere que queremos capturar, basta incluir um espao em branco no formato, antes do especificador.
char a; ... scanf(" %c", %a); ...
J mencionamos que o especificador %s pode ser usado na funo printf para imprimir uma cadeia de caracteres. O mesmo especificador pode ser utilizado para capturar cadeias de caracteres na funo scanf. No entanto, seu uso muito limitado. O especificador %s na funo scanf pula os eventuais caracteres brancos e captura a seqncia de caracteres no brancos. Consideremos o fragmento de cdigo abaixo:
char cidade[81]; ... scanf("%s", cidade); ...
Devemos notar que no usamos o caractere & na passagem da cadeia para a funo, pois a cadeia um vetor (o nome da varivel representa o endereo do primeiro elemento do vetor e a funo atribui os valores dos elementos a partir desse endereo). O uso do especificador de formato %s na leitura limitado, pois o fragmento de cdigo acima funciona apenas para capturar nomes simples. Se o usurio digitar Rio de Janeiro, apenas a palavra Rio ser capturada, pois o %s l somente uma seqncia de caracteres no brancos. Em geral, queremos ler nomes compostos (nome de pessoas, cidades, endereos para correspondncia, etc.). Para capturarmos estes nomes, podemos usar o especificador de formato %[...], no qual listamos entre os colchetes todos os caracteres que aceitaremos na leitura. Assim, o formato "%[aeiou]" l seqncias de vogais, isto , a leitura prossegue at que se encontre um caractere que no seja uma vogal. Se o primeiro caractere entre colchetes for o acento circunflexo (^), teremos o efeito inverso (negao). Assim, com o formato "%[^aeiou]" a leitura prossegue enquanto uma vogal no for encontrada. Esta construo permite capturarmos nomes compostos. Consideremos o cdigo abaixo:
char cidade[81]; ... scanf(" %[^\n]", cidade); ...
A funo scanf agora l uma seqncia de caracteres at que seja encontrado o caractere de mudana de linha ('\n'). Em termos prticos, captura-se a linha fornecida pelo usurio at que ele tecle Enter. A incluso do espao no formato (antes do sinal %) garante que eventuais caracteres brancos que precedam o nome sero pulados. Para finalizar, devemos salientar que o trecho de cdigo acima perigoso, pois, se o usurio fornecer uma linha que tenha mais de 80 caracteres, estaremos invadindo um espao de memria que no est reservado (o vetor foi dimensionado com 81 elementos).
Estruturas de Dados PUC-Rio 6-5
Para evitar esta possvel invaso, podemos limitar o nmero mximo de caracteres que sero capturados.
char cidade[81]; ... scanf(" %80[^\n]", cidade); ...
/* l no mximo 80 caracteres */
Exemplos de funes que manipulam cadeias de caracteres Nesta seo, discutiremos a implementao de algumas funes que manipulam cadeias de caracteres. Exemplo. Impresso caractere por caractere. Vamos inicialmente considerar a implementao de uma funo que imprime uma cadeia de caracteres, caractere por caractere. A implementao pode ser dada por:
void imprime (char* s) { int i; for (i=0; s[i] != '\0'; i++) printf("%c",s[i]); printf("\n"); }
Exemplo. Comprimento da cadeia de caracteres. Consideremos a implementao de uma funo que recebe como parmetro de entrada uma cadeia de caracteres e fornece como retorno o nmero de caracteres existentes na cadeia. O prottipo da funo pode ser dado por:
int comprimento (char* s);
Para contar o nmero de caracteres da cadeia, basta contarmos o nmero de caracteres at que o caractere nulo (que indica o fim da cadeia) seja encontrado. O caractere nulo em si no deve ser contado. Uma possvel implementao desta funo :
int comprimento (char* s) { int i; int n = 0; /* contador */ for (i=0; s[i] != '\0'; i++) n++; return n;
6-6
A sada deste programa ser: A string "Rio de Janeiro" tem 14 caracteres. Salientamos o uso do caractere de escape \" para incluir as aspas na sada. Exemplo. Cpia de cadeia de caracteres. Vamos agora considerar a implementao de uma funo para copiar os elementos de uma cadeia de caracteres para outra. Assumimos que a cadeia que receber a cpia tem espao suficiente para que a operao seja realizada. O prottipo desta funo pode ser dado por:
void copia (char* dest, char* orig);
A funo copia os elementos da cadeia original (orig) para a cadeia de destino (dest). Uma possvel implementao desta funo mostrada abaixo:
void copia (char* dest, char* orig) { int i; for (i=0; orig[i] != '\0'; i++) dest[i] = orig[i]; /* fecha a cadeia copiada */ dest[i] = '\0'; }
Salientamos a necessidade de fechar a cadeia copiada aps a cpia dos caracteres no nulos. Quando o lao do for terminar, a varivel i ter o ndice de onde est armazenado o caractere nulo na cadeia original. A cpia tambm deve conter o '\0' nesta posio. Exemplo. Concatenao de cadeias de caracteres. Vamos considerar uma extenso do exemplo anterior e discutir a implementao de uma funo para concatenar uma cadeia de caracteres com outra j existente. Isto , os caracteres de uma cadeia so copiados no final da outra cadeia. Assim, se uma cadeia representa inicialmente a cadeia PUC e concatenarmos a ela a cadeia Rio, teremos como resultado a cadeia PUCRio. Vamos mais uma vez considerar que existe espao reservado que permite fazer a cpia dos caracteres. O prottipo da funo pode ser dado por:
void concatena (char*dest, char* orig);
6-7
Funes anlogas s funes comprimento, copia e concatena so disponibilizadas pela biblioteca padro de C. As funes da biblioteca padro so, respectivamente, strlen, strcpy e strcat, que fazem parte da biblioteca de cadeias de caracteres (strings), string.h. Existem diversas outras funes que manipulam cadeias de caracteres nessa biblioteca. A razo de mostrarmos possveis implementaes destas funes como exerccio ilustrar a codificao da manipulao de cadeias de caracteres. Exemplo 5: Duplicao de cadeias de caracteres. Consideremos agora um exemplo com alocao dinmica. O objetivo implementar uma funo que receba como parmetro uma cadeia de caracteres e fornea uma cpia da cadeia, alocada dinamicamente. O prottipo desta funo pode ser dado por:
char* duplica (char* s);
A funo que chama duplica fica responsvel por liberar o espao alocado. Funes recursivas Uma cadeia de caracteres pode ser definida de forma recursiva. Podemos dizer que uma cadeia de caracteres representada por: uma cadeia de caracteres vazia; ou um caractere seguido de uma (sub) cadeia de caracteres.
Estruturas de Dados PUC-Rio 6-8
Isto , podemos dizer que uma cadeia s no vazia pode ser representada pelo seu primeiro caractere s[0] seguido da cadeia que comea no endereo do ento segundo caractere, &s[1]. Vamos reescrever algumas das funes mostradas acima, agora com a verso recursiva. Exemplo. Impresso caractere por caractere. Uma verso recursiva da funo para imprimir a cadeia caractere por caractere mostrada a seguir. Como j foi discutido, uma implementao recursiva deve ser projetada considerando-se a definio recursiva do objeto, no caso uma cadeia de caracteres. Assim, a funo deve primeiro testar se a condio da cadeia vazia. Se a cadeia for vazia, nada precisa ser impresso; se no for vazia, devemos imprimir o primeiro caractere e ento chamar uma funo para imprimir a sub-cadeia que se segue. Para imprimir a sub-cadeia podemos usar a prpria funo, recursivamente.
void imprime_rec (char* s) { if (s[0] != '\0') { printf("%c",s[0]); imprime_rec(&s[1]); } }
Algumas implementaes ficam bem mais simples se feitas recursivamente. Por exemplo, simples alterar a funo acima e fazer com que os caracteres da cadeia sejam impressos em ordem inversa, de trs para a frente: basta imprimir a sub-cadeia antes de imprimir o primeiro caractere.
void imprime_inv (char* s) { if (s[0] != '\0') { imprime_inv(&s[1]); printf("%c",s[0]); } }
Como exerccio, sugerimos implementar a impresso inversa sem usar recursividade. Exemplo. Comprimento da cadeia de caracteres. Uma implementao recursiva da funo que retorna o nmero de caracteres existentes na cadeia mostrada a seguir:
int comprimento_rec (char* s) { if (s[0] == '\0') return 0; else return 1 + comprimento_rec(&s[1]); }
6-9
Exemplo. Cpia de cadeia de caracteres. Vamos mostrar agora uma possvel implementao recursiva da funo copia mostrada anteriormente.
void copia_rec (char* dest, char* orig) { if (orig[0] == '\0') dest[0] = '\0'; else { dest[0] = orig[0]; copia_rec(&dest[1],&orig[1]); } }
fcil verificar que o cdigo acima pode ser escrito de forma mais compacta:
void copia_rec_2 (char* dest, char* orig) { dest[0] = orig[0]; if (orig[0] != '\0') copia_rec_2(&dest[1],&orig[1]); }
Constante cadeia de caracteres** Em cdigos C, uma seqncia de caracteres delimitada por aspas representa uma constante cadeia de caracteres, ou seja, uma expresso constante, cuja avaliao resulta no ponteiro onde a cadeia de caracteres est armazenada. Para exemplificar, vamos considerar o trecho de cdigo abaixo:
#include <string.h> int main ( void ) { char cidade[4]; strcpy (cidade, "Rio" ); printf ( "%s \n", cidade ); return 0; }
De forma ilustrativa, o que acontece que, quando o compilador encontra a cadeia "Rio", automaticamente alocada na rea de constantes a seguinte seqncia de caracteres:
'R', 'i', 'o', '\0'
e fornecido o ponteiro para o primeiro elemento desta seqncia. Assim, a funo strcpy recebe dois ponteiros de cadeias: o primeiro aponta para o espao associado varivel cidade e o segundo aponta para a rea de constantes onde est armazenada a cadeia Rio.
6-10
"Rio" */
Na primeira, declaramos um vetor de char local que inicializado com a cadeia de caracteres Rio de Janeiro, seguido do caractere nulo. A varivel s1 ocupa, portanto, 15 bytes de memria. Na segunda, declaramos um ponteiro para char que inicializado com o endereo de uma rea de memria onde a constante cadeia de caracteres Rio de Janeiro est armazenada. A varivel s2 ocupa 4 bytes (espao de um ponteiro). Podemos verificar esta diferena imprimindo os valores sizeof(s1) e sizeof(s2). Como s1 um vetor local, podemos alterar o valor de seus elementos. Por exemplo, vlido escrever s1[0]='X'; alterando o contedo da cadeia para Xio de Janeiro. No entanto, no vlido escrever s2[0]='X'; pois estaramos tentando alterar o contedo de uma rea de constante.
Com esta varivel declarada, alunos[i] acessa a cadeia de caracteres com o nome do (i+1)-simo aluno da turma e, conseqentemente, alunos[i][j] acessa a (j+1)-sima letra do nome do (i+1)-simo aluno. Considerando que alunos uma varivel global, uma funo para imprimir os nomes dos n alunos de uma turma poderia ser dada por:
void imprime (int n) { int i; for (i=0; i<n; i++) printf("%s\n", alunos[i]); }
Estruturas de Dados PUC-Rio 6-11
No prximo captulo, que trata de matrizes, discutiremos conjuntos bidimensionais com mais detalhes. Para a representao de vetores de cadeias de caracteres, optamos, em geral, por declarar um vetor de ponteiros e alocar dinamicamente cada elemento (no caso, uma cadeia de caracteres). Desta forma, otimizamos o uso do espao de memria, pois no precisamos achar uma dimenso mxima para todas as cadeias do vetor nem desperdiamos espao excessivo quando temos poucos nomes de alunos a serem armazenados. Cada elemento do vetor um ponteiro. Se precisarmos armazenar um nome na posio, alocamos o espao de memria necessrio para armazenar a cadeia de caracteres correspondente. Assim, nosso vetor com os nomes dos alunos pode ser declarado da seguinte forma:
#define MAX 50 char* alunos[MAX];
Exemplo. Leitura e impresso dos nomes dos alunos. Vamos escrever uma funo que captura os nomes dos alunos de uma turma. A funo inicialmente l o nmero de alunos da turma (que deve ser menor ou igual a MAX) e captura os nomes fornecidos por linha, fazendo a alocao correspondente. Para escrever esta funo, podemos pensar numa funo auxiliar que captura uma linha e fornece como retorno uma cadeia alocada dinamicamente com a linha inserida. Fazendo uso das funes que escrevemos acima, podemos ter:
char* lelinha (void) { char linha[121]; /* variavel auxiliar para ler linha */ scanf(" %120[^\n]",linha); return duplica(linha); }
A funo para capturar os nomes dos alunos preenche o vetor de nomes e pode ter como valor de retorno o nmero de nomes lidos:
int lenomes (char** alunos) { int i; int n; do { scanf("%d",&n); } while (n>MAX); for (i=0; i<n; i++) alunos[i] = lelinha(); return n;
A funo para liberar os nomes alocados na tabela pode ser implementada por:
void liberanomes (int n, char** alunos) { int i; for (i=0; i<n; i++) free(alunos[i]); }
6-12
Uma funo para imprimir os nomes dos alunos pode ser dada por:
void imprimenomes (int n, char** alunos) { int i; for (i=0; i<n; i++) printf("%s\n", alunos[i]); }
Parmetros da funo main** Em todos os exemplos mostrados, temos considerado que a funo principal, main, no recebe parmetros. Na verdade, a funo main pode ser definida para receber zero ou dois parmetros, geralmente chamados argc e argv. O parmetro argc recebe o nmero de argumentos passados para o programa quando este executado; por exemplo, de um comando de linha do sistema operacional. O parmetro argv um vetor de cadeias de caracteres, que armazena os nomes passados como argumentos. Por exemplo, se temos um programa executvel com o nome mensagem e se ele for invocado atravs da linha de comando:
> mensagem estruturas de dados
a varivel argc receber o valor 4 e o vetor argv ser inicializado com os seguintes elementos: argv[0]="mensagem", argv[1]="estruturas", argv[2]="de", e argv[3]="dados". Isto , o primeiro elemento armazena o prprio nome do executvel e os demais so preenchidos com os nomes passados na linha de comando. Esses parmetros podem ser teis para, por exemplo, passar o nome de um arquivo de onde sero capturados os dados de um programa. A manipulao de arquivos ser discutida mais adiante no curso. Por ora, mostramos um exemplo simples que trata os dois parmetros da funo main.
#include <stdio.h> int main (int argc, char** argv) { int i; for (i=0; i<argc; i++) printf("%s\n", argv[i]); return 0; }
Se este programa tiver seu executvel chamado de mensagem e for invocado com a linha de comando mostrada acima, a sada ser:
mensagem estruturas de dados
Estruturas de Dados PUC-Rio 6-13
7. Tipos estruturados
W. Celes e J. L. Rangel Na linguagem C, existem os tipos bsicos (char, int, float, etc.) e seus respectivos ponteiros que podem ser usados na declarao de variveis. Para estruturar dados complexos, nos quais as informaes so compostas por diversos campos, necessitamos de mecanismos que nos permitam agrupar tipos distintos. Neste captulo, apresentaremos os mecanismos fundamentais da linguagem C para a estruturao de tipos.
No entanto, deste modo, os dois valores ficam dissociados e, no caso do programa manipular vrios pontos, cabe ao programador no misturar a coordenada x de um ponto com a coordenada y de outro. Para facilitar este trabalho, a linguagem C oferece recursos para agruparmos dados. Uma estrutura, em C, serve basicamente para agrupar diversas variveis dentro de um nico contexto. No nosso exemplo, podemos definir uma estrutura ponto que contenha as duas variveis. A sintaxe para a definio de uma estrutura mostrada abaixo:
struct ponto { float x; float y; };
Desta forma, a estrutura ponto passa a ser um tipo e podemos ento declarar variveis deste tipo.
struct ponto p;
Esta linha de cdigo declara p como sendo uma varivel do tipo struct ponto. Os elementos de uma estrutura podem ser acessados atravs do operador de acesso ponto (.). Assim, vlido escrever:
ponto.x = 10.0; ponto.y = 5.0;
Manipulamos os elementos de uma estrutura da mesma forma que variveis simples. Podemos acessar seus valores, atribuir-lhes novos valores, acessar seus endereos, etc.
Estruturas de Dados PUC-Rio 7-1
Exemplo: Capturar e imprimir as coordenadas de um ponto. Para exemplificar o uso de estruturas em programas, vamos considerar um exemplo simples em que capturamos e imprimimos as coordenadas de um ponto qualquer.
/* Captura e imprime as coordenadas de um ponto qualquer */ #include <stdio.h> struct ponto { float x; float y; }; int main (void) { struct ponto p; printf("Digite as coordenadas do ponto(x y): "); scanf("%f %f", &p.x, &p.y); printf("O ponto fornecido foi: (%.2f,%.2f)\n", p.x, p.y); return 0;
A varivel p, definida dentro de main, uma varivel local como outra qualquer. Quando a declarao encontrada, aloca-se, na pilha de execuo, um espao para seu armazenamento, isto , um espao suficiente para armazenar todos os campos da estrutura (no caso, dois nmeros reais). Notamos que o acesso ao endereo de um campo da estrutura feito da mesma forma que com variveis simples: basta escrever &(p.x), ou simplesmente &p.x, pois o operador de acesso ao campo da estrutura tem precedncia sobre o operador endereo de. Ponteiro para estruturas Da mesma forma que podemos declarar variveis do tipo estrutura:
struct ponto p;
Se a varivel pp armazenar o endereo de uma estrutura, podemos acessar os campos dessa estrutura indiretamente, atravs de seu ponteiro:
(*pp).x = 12.0;
Neste caso, os parnteses so indispensveis, pois o operador contedo de tem precedncia menor que o operador de acesso. O acesso de campos de estruturas to comum em programas C que a linguagem oferece outro operador de acesso, que permite acessar campos a partir do ponteiro da estrutura. Este operador composto por um trao seguido de um sinal de maior, formando uma seta (->). Portanto, podemos reescrever a atribuio anterior fazendo:
7-2
pp->x = 12.0;
Em resumo, se temos uma varivel estrutura e queremos acessar seus campos, usamos o operador de acesso ponto (p.x); se temos uma varivel ponteiro para estrutura, usamos o operador de acesso seta (pp->x). Seguindo o raciocnio, se temos o ponteiro e queremos acessar o endereo de um campo, fazemos &pp->x! Passagem de estruturas para funes Para exemplificar a passagem de variveis do tipo estrutura para funes, podemos reescrever o programa simples, mostrado anteriormente, que captura e imprime as coordenadas de um ponto qualquer. Inicialmente, podemos pensar em escrever uma funo que imprima as coordenadas do ponto. Esta funo poderia ser dada por:
void imprime (struct ponto p) { printf("O ponto fornecido foi: (%.2f,%.2f)\n", p.x, p.y); }
A passagem de estruturas para funes se processa de forma anloga passagem de variveis simples, porm exige uma anlise mais detalhada. Da forma como est escrita no cdigo acima, a funo recebe uma estrutura inteira como parmetro. Portanto, faz-se uma cpia de toda a estrutura para a pilha e a funo acessa os dados desta cpia. Existem dois pontos a serem ressaltados. Primeiro, como em toda passagem por valor, a funo no tem como alterar os valores dos elementos da estrutura original (na funo imprime isso realmente no necessrio, mas seria numa funo de leitura). O segundo ponto diz respeito eficincia, visto que copiar uma estrutura inteira para a pilha pode ser uma operao custosa (principalmente se a estrutura for muito grande). mais conveniente passar apenas o ponteiro da estrutura, mesmo que no seja necessrio alterar os valores dos elementos dentro da funo, pois copiar um ponteiro para a pilha muito mais eficiente do que copiar uma estrutura inteira. Um ponteiro ocupa em geral 4 bytes, enquanto uma estrutura pode ser definida com um tamanho muito grande. Desta forma, uma segunda (e mais adequada) alternativa para escrevermos a funo imprime :
void imprime (struct ponto* pp) { printf("O ponto fornecido foi: (%.2f,%.2f)\n", pp->x, pp->y); }
Podemos ainda pensar numa funo para ler a hora do evento. Observamos que, neste caso, obrigatoriamente devemos passar o ponteiro da estrutura, caso contrrio no seria possvel passar ao programa principal os dados lidos:
void captura (struct ponto* pp) { printf("Digite as coordenadas do ponto(x y): "); scanf("%f %f", &p->x, &p->y); }
7-3
Com estas funes, nossa funo main ficaria como mostrado abaixo.
int main (void) { struct ponto p; captura(&p); imprime(&p); return 0; }
Exerccio: Funo para determinar a distncia entre dois pontos. Considere a implementao de uma funo que tenha como valor de retorno a distncia entre dois pontos. O prottipo da funo pode ser dado por:
float distancia (struct ponto *p, struct ponto *q);
Nota: A distncia entre dois pontos dada por: d = ( x2 x1 ) 2 + ( y2 y1 ) 2 Alocao dinmica de estruturas Da mesma forma que os vetores, as estruturas podem ser alocadas dinamicamente. Por exemplo, vlido escrever:
struct ponto* p; p = (struct ponto*) malloc (sizeof(struct ponto));
Neste fragmento de cdigo, o tamanho do espao de memria alocado dinamicamente dado pelo operador sizeof aplicado sobre o tipo estrutura (sizeof(struct ponto)). A funo malloc retorna o endereo do espao alocado, que ento convertido para o tipo ponteiro da estrutura ponto. Aps uma alocao dinmica, podemos acessar normalmente os campos da estrutura, atravs da varivel ponteiro que armazena seu endereo:
... p->x = 12.0; ...
podemos usar o nome Real como um mnemnico para o tipo float. O uso de typedef muito til para abreviarmos nomes de tipos e para tratarmos tipos complexos. Alguns exemplos vlidos de typedef:
typedef unsigned char UChar; typedef int* PInt; typedef float Vetor[4];
7-4
Neste fragmento de cdigo, definimos UChar como sendo o tipo char sem sinal, PInt como um tipo ponteiro para int, e Vetor como um tipo que representa um vetor de quatro elementos. A partir dessas definies, podemos declarar variveis usando estes mnemnicos:
Vetor v; ... v[0] = 3; ...
Em geral, definimos nomes de tipos para as estruturas com as quais nossos programas trabalham. Por exemplo, podemos escrever:
struct ponto { float x; float y; }; typedef struct ponto Ponto;
Neste caso, Ponto passa a representar nossa estrutura de ponto. Tambm podemos definir um nome para o tipo ponteiro para a estrutura.
typedef struct ponto *PPonto;
Podemos ainda definir mais de um nome num mesmo typedef. Os dois typedef anteriores poderiam ser escritos por:
typedef struct ponto Ponto, *PPonto;
A sintaxe de um typedef pode parecer confusa, mas equivalente da declarao de variveis. Por exemplo, na definio abaixo:
typedef float Vector[4];
se omitssemos a palavra typedef, estaramos declarando a varivel Vector como sendo um vetor de 4 elementos do tipo float. Com typedef, estamos definindo um nome que representa o tipo vetor de 4 elementos float. De maneira anloga, na definio:
typedef struct ponto Ponto, *PPonto;
se omitssemos a palavra typedef, estaramos declarando a varivel Ponto como sendo do tipo struct ponto e a varivel PPonto como sendo do tipo ponteiro para struct ponto. Por fim, vale salientar que podemos definir a estrutura e associar mnemnicos para elas em um mesmo comando:
typedef struct ponto { float x; float y; } Ponto, *PPonto;
7-5
comum os programadores de C usarem nomes com as primeiras letras maisculas na definio de tipos. Isso no uma obrigatoriedade, apenas um estilo de codificao.
yi+1
xi
xi+1
Na figura, ressaltamos a rea do trapzio definido pela aresta que vai do ponto pi ao ponto pi+1. A rea desse trapzio dada por: a = ( xi +1 xi )( yi +1 + yi ) / 2 . Somando-se as reas (algumas delas negativas) dos trapzios definidos por todas as arestas chega-se a rea do polgono (as reas externas ao polgono so anuladas). Se a seqncia de pontos que define o polgono for dada em sentido anti-horrio, chega-se a uma rea de valor negativo. Neste caso, a rea do polgono o valor absoluto do resultado da soma.
Um vetor de estruturas pode ser usado para definir um polgono. O polgono passa a ser representado por um seqncia de pontos. Podemos, ento, escrever uma funo para calcular a rea de um polgono, dados o nmero de pontos e o vetor de pontos que o representa. Uma implementao dessa funo mostrada abaixo.
float area (int n, Ponto* p) { int i, j; float a = 0; for (i=0; i<n; i++) { j = (i+1) % n; /* prximo ndice (incremento circular) */ a += (p[j].x-p[i].x)*(p[i].y + p[j].y)/2; } if (a < 0) return -a; else return a; }
Estruturas de Dados PUC-Rio 7-6
Exerccio: Altere o programa acima para capturar do teclado o nmero de pontos que delimitam o polgono. O programa deve, ento, alocar dinamicamente o vetor de pontos, capturar as coordenadas dos pontos e, chamando a funo area, exibir o valor da rea.
Vamos montar a tabela de alunos usando um vetor global com um nmero mximo de alunos. Uma primeira opo declarar um vetor de estruturas:
#define MAX 100 Aluno tab[MAX];
Desta forma, podemos armazenar nos elementos do vetor os dados dos alunos que queremos organizar. Seria vlido, por exemplo, uma atribuio do tipo:
... tab[i].mat = 9912222; ...
7-7
No entanto, o uso de vetores de estruturas tem, neste caso, uma grande desvantagem. O tipo 1 Aluno definido acima ocupa pelo menos 227 (=81+4+121+21) bytes . A declarao de um vetor desta estrutura representa um desperdcio significativo de memria, pois provavelmente estaremos armazenando de fato um nmero de alunos bem inferior ao mximo estimado. Para contornar este problema, podemos trabalhar com um vetor de ponteiros.
typedef struct aluno *PAluno; #define MAX 100 PAluno tab[MAX];
Assim, cada elemento do vetor ocupa apenas o espao necessrio para armazenar um ponteiro. Quando precisarmos alocar os dados de um aluno numa determinada posio do vetor, alocamos dinamicamente a estrutura Aluno e guardamos seu endereo no vetor de ponteiros. Considerando o vetor de ponteiros declarado acima como uma varivel global, podemos ilustrar a implementao de algumas funcionalidades para manipular nossa tabela de alunos. Inicialmente, vamos considerar uma funo de inicializao. Uma posio do vetor estar vazia, isto , disponvel para armazenar informaes de um novo aluno, se o valor do seu elemento for o ponteiro nulo. Portanto, numa funo de inicializao, podemos atribuir NULL a todos os elementos da tabela, significando que temos, a princpio, uma tabela vazia.
void inicializa (void) { int i; for (i=0; i<MAX; i++) tab[i] = NULL; }
Uma segunda funcionalidade que podemos prever armazena os dados de um novo aluno numa posio do vetor. Vamos considerar que os dados sero fornecidos via teclado e que uma posio onde os dados sero armazenados ser passada para a funo. Se a posio da tabela estiver vazia, devemos alocar uma nova estrutura; caso contrrio, atualizamos a estrutura j apontada pelo ponteiro.
void preenche (int i) { if (tab[i]==NULL) tab[i] = (PAluno)malloc(sizeof(Aluno)); printf("Entre com o nome:"); scanf(" %80[^\n]", tab[i]->nome); printf("Entre com a matricula:"); scanf("%d", &tab[i]->mat); printf("Entre com o endereco:"); scanf(" %120[^\n]", tab[i]->end); printf("Entre com o telefone:"); scanf(" %20[^\n]", tab[i]->tel);
Provavelmente o tipo ocupar mais espao, pois os dados tm que estar alinhados para serem armazenados na memria.
7-8
Podemos tambm prever uma funo para remover os dados de um aluno da tabela. Vamos considerar que a posio da tabela a ser liberada ser passada para a funo:
void remove (int i) { if (tab[i] != NULL) { free(tab[i]); tab[i] = NULL; } }
Para consultarmos os dados, vamos considerar uma funo que imprime os dados armazenados numa determinada posio do vetor:
void imprime (int i) { if (tab[i] != NULL) { printf("Nome: %s\n, tab[i]->nome); printf("Matrcula: %d\n, tab[i]->mat); printf("Endereo: %s\n, tab[i]->end); printf("Telefone: %s\n, tab[i]->tel); } }
Por fim, podemos implementar uma funo que imprima os dados de todos os alunos da tabela:
void imprime_tudo (void) { int i; for (i=0; i<MAX; i++) imprime(i); }
Exerccio. Faa um programa que utilize as funes da tabela de alunos escritas acima. Exerccio. Re-escreva as funes acima sem usar uma varivel global. Sugesto: Crie um tipo Tabela e faa as funes receberem este tipo como primeiro parmetro.
7-9
Anlogo estrutura, este fragmento de cdigo no declara nenhuma varivel, apenas define o tipo unio. Aps uma definio, podemos declarar variveis do tipo unio:
union exemplo v;
Na varivel v, os campos i e c compartilham o mesmo espao de memria. A varivel ocupa pelo menos o espao necessrio para armazenar o maior de seus campos (um inteiro, no caso). O acesso aos campos de uma unio anlogo ao acesso a campos de uma estrutura. Usamos o operador ponto (.) para acess-los diretamente e o operador seta (->) para acess-los atravs de um ponteiro da unio. Assim, dada a declarao acima, podemos escrever:
v.i = 10;
ou
v.c = 'x';
Salientamos, no entanto, que apenas um nico elemento de uma unio pode estar armazenado num determinado instante, pois a atribuio a um campo da unio sobrescreve o valor anteriormente atribudo a qualquer outro campo.
Desta forma, as definies de FALSE e TRUE permitem a utilizao destes smbolos no cdigo, dando maior clareza, mas o tipo booleano criado, como equivalente a um inteiro qualquer, pode armazenar qualquer valor inteiro, no apenas FALSE e TRUE, que seria mais adequado. Para validarmos os valores atribudos, podemos enumerar os valores constantes que um determinado tipo pode assumir, usando enum:
enum bool { FALSE, TRUE }; typedef enum bool Bool;
7-10
Com isto, definimos as constantes FALSE e TRUE. Por default, o primeiro smbolo representa o valor 0, o seguinte o valor 1, e assim por diante. Poderamos explicitar os valores dos smbolos numa enumerao, como por exemplo:
enum bool { TRUE = 1, FALSE = 0, };
No exemplo do tipo booleano, a numerao default coincide com a desejada (desde que o smbolo FALSE preceda o smbolo TRUE dentro da lista da enumerao). A declarao de uma varivel do tipo criado pode ser dada por:
Bool resultado;
onde resultado representa uma varivel que pode receber apenas os valores FALSE (0) ou TRUE (1).
7-11
8. Matrizes
W. Celes e J. L. Rangel J discutimos em captulos anteriores a construo de conjuntos unidimensionais atravs do uso de vetores. A linguagem C tambm permite a construo de conjuntos bi ou multidimensionais. Neste captulo, discutiremos em detalhe a manipulao de matrizes, representadas por conjuntos bidimensionais de valores numricos. As construes apresentadas aqui podem ser estendidas para conjuntos de dimenses maiores.
Podemos dizer que, nestes casos, os vetores so declarados estaticamente 1. A varivel que representa o vetor uma constante que armazena o endereo ocupado pelo primeiro elemento do vetor. Esses vetores podem ser declarados como variveis globais ou dentro do corpo de uma funo. Se declarado dentro do corpo de uma funo, o vetor existir apenas enquanto a funo estiver sendo executada, pois o espao de memria para o vetor reservado na pilha de execuo. Portanto, no podemos fazer referncia ao espao de memria de um vetor local de uma funo que j retornou. O problema de declararmos um vetor estaticamente, seja como varivel global ou local, que precisamos saber de antemo a dimenso mxima do vetor. Usando alocao dinmica, podemos determinar a dimenso do vetor em tempo de execuo:
int* v; v = (int*) malloc(n * sizeof(int));
Neste fragmento de cdigo, n representa uma varivel com a dimenso do vetor, determinada em tempo de execuo (podemos, por exemplo, capturar o valor de n fornecido pelo usurio). Aps a alocao dinmica, acessamos os elementos do vetor da mesma forma que os elementos de vetores criados estaticamente. Outra diferena importante: com alocao dinmica, declaramos uma varivel do tipo ponteiro que posteriormente recebe o valor do endereo do primeiro elemento do vetor, alocado dinamicamente. A rea de memria ocupada pelo vetor permanece vlida at que seja explicitamente liberada (atravs da funo free). Portanto, mesmo que um vetor seja
1
criado dinamicamente dentro da funo, podemos acess-lo depois da funo ser finalizada, pois a rea de memria ocupada por ele permanece vlida, isto , o vetor no est alocado na pilha de execuo. Usamos esta propriedade quando escrevemos a funo que duplica uma cadeia de caracteres (string): a funo duplica aloca um vetor de char dinamicamente, preenche seus valores e retorna o ponteiro, para que a funo que chama possa acessar a nova cadeia de caracteres. A linguagem C oferece ainda um mecanismo para re-alocarmos um vetor dinamicamente. Em tempo de execuo, podemos verificar que a dimenso inicialmente escolhida para um vetor tornou-se insuficiente (ou excessivamente grande), necessitando um redimensionamento. A funo realloc da biblioteca padro nos permite re-alocar um vetor, preservando o contedo dos elementos, que permanecem vlidos aps a re-alocao (no fragmento de cdigo abaixo, m representa a nova dimenso do vetor).
v = (int*) realloc(v, m*sizeof(int));
Vale salientar que, sempre que possvel, optamos por trabalhar com vetores criados estaticamente. Eles tendem a ser mais eficientes, j que os vetores alocados dinamicamente tm uma indireo a mais (primeiro acessa-se o valor do endereo armazenado na varivel ponteiro para ento acessar o elemento do vetor).
Esta declarao reserva um espao de memria necessrio para armazenar os 12 elementos da matriz, que so armazenados de maneira contnua, organizados linha a linha.
float m[4][3] = {{ 5.0,10.0,15.0}, {20.0,25.0,30.0}, {35.0,40.0,45.0}, {50.0,55.0,60.0}}; j i 5.0 20.0 35.0 50.0 10.0 25.0 40.0 55.0 15.0 30.0 45.0 60.0
152 60.0 55.0 50.0 45.0 40.0 35.0 30.0 25.0 20.0 15.0 10.0 5.0
104
8-2
Os elementos da matriz so acessados com indexao dupla: mat[i][j]. O primeiro ndice, i, acessa a linha e o segundo, j, acessa a coluna. Como em C a indexao comea em zero, o elemento da primeira linha e primeira coluna acessado por mat[0][0]. Aps a declarao esttica de uma matriz, a varivel que representa a matriz, mat no exemplo acima, representa um ponteiro para o primeiro vetor-linha, composto por 3 elementos. Com isto, mat[1] aponta para o primeiro elemento do segundo vetor-linha, e assim por diante. As matrizes tambm podem ser inicializadas na declarao:
float mat[4][3] = {{1,2,3},{4,5,6},{7,8,9},{10,11,12}};
O nmero de elementos por linha pode ser omitido numa inicializao, mas o nmero de colunas deve, obrigatoriamente, ser fornecido:
float mat[][3] = {1,2,3,4,5,6,7,8,9,10,11,12};
Passagem de matrizes para funes Conforme dissemos acima, uma matriz criada estaticamente representada por um ponteiro para um vetor-linha com o nmero de elementos da linha. Quando passamos uma matriz para uma funo, o parmetro da funo deve ser deste tipo. Infelizmente, a sintaxe para representar este tipo obscura. O prottipo de uma funo que recebe a matriz declarada acima seria:
void f (..., float (*mat)[3], ...);
Uma segunda opo declarar o parmetro como matriz, podendo omitir o nmero de linhas2:
void f (..., float mat[][3], ...);
De qualquer forma, o acesso aos elementos da matriz dentro da funo feito da forma usual, com indexao dupla. Na prxima seo, examinaremos formas de trabalhar com matrizes alocadas dinamicamente. No entanto, vale salientar que recomendamos, sempre que possvel, o uso de matrizes alocadas estaticamente. Em diversas aplicaes, as matrizes tm dimenses fixas e no justificam a criao de estratgias para trabalhar com alocao dinmica. Em aplicaes da rea de Computao Grfica, por exemplo, comum trabalharmos com matrizes de 4 por 4 para representar transformaes geomtricas e projees. Nestes casos, muito mais simples definirmos as matrizes estaticamente (float mat[4][4];), uma
2
Isto tambm vale para vetores. Um prottipo de uma funo que recebe um vetor como parmetro pode ser dado por: void f (..., float v[], ...);.
8-3
vez que sabemos de antemo as dimenses a serem usadas. Nestes casos, vale a pena definirmos um tipo prprio, pois nos livramos das construes sintticas confusas explicitadas acima. Por exemplo, podemos definir o tipo Matrix4.
typedef float Matrix4[4][4];
a b c d e f
g h I j
k = i*n+j = 1*4+2 = 6
Figura 8.2: Matriz representada por vetor simples.
Esta conta de endereamento intuitiva: se quisermos acessar elementos da terceira (i=2) linha da matriz, temos que pular duas linhas de elementos (i*n) e depois indexar o elemento da linha com j.
8-4
Com esta estratgia, a alocao da matriz recai numa alocao de vetor que tem m*n elementos, onde m e n representam as dimenses da matriz.
float *mat; /* matriz representada por um vetor */ ... mat = (float*) malloc(m*n*sizeof(float)); ...
No entanto, somos obrigados a usar uma notao desconfortvel, v[i*n+j], para acessar os elementos, o que pode deixar o cdigo pouco legvel. Matriz representada por um vetor de ponteiros Nesta segunda estratgia, faremos algo parecido com o que fizemos para tratar vetores de cadeias de caracteres, que em C so representados por conjuntos bidimensionais de caracteres. De acordo com esta estratgia, cada linha da matriz representada por um vetor independente. A matriz ento representada por um vetor de vetores, ou vetor de ponteiros, no qual cada elemento armazena o endereo do primeiro elemento de cada linha. A figura abaixo ilustra o arranjo da memria utilizada nesta estratgia.
j=2 i=1 a e I b f j c g k d h l j=2 i=1 a b c d e f I j
Figura 8.3: Matriz com vetor de ponteiros.
g h k l
A alocao da matriz agora mais elaborada. Primeiro, temos que alocar o vetor de ponteiros. Em seguida, alocamos cada uma das linhas da matriz, atribuindo seus endereos aos elementos do vetor de ponteiros criado. O fragmento de cdigo abaixo ilustra esta codificao:
int i; float **mat; /* matriz representada por um vetor de ponteiros */ ... mat = (float**) malloc(m*sizeof(float*)); for (i=0; i<m; i++) m[i] = (float*) malloc(n*sizeof(float));
A grande vantagem desta estratgia que o acesso aos elementos feito da mesma forma que quando temos uma matriz criada estaticamente, pois, se mat representa uma matriz
Estruturas de Dados PUC-Rio 8-5
alocada segundo esta estratgia, mat[i] representa o ponteiro para o primeiro elemento da linha i, e, conseqentemente, mat[i][j] acessa o elemento da coluna j da linha i. A liberao do espao de memria ocupado pela matriz tambm exige a construo de um lao, pois temos que liberar cada linha antes de liberar o vetor de ponteiros:
... for (i=0; i<m; i++) free(mat[i]); free(mat);
A funo que cria a matriz dinamicamente deve alocar a estrutura que representa a matriz e alocar o vetor dos elementos:
Matriz* cria (int m, int n) { Matriz* mat = (Matriz*) malloc(sizeof(Matriz)); mat->lin = m; mat->col = n; mat->v = (float*) malloc(m*n*sizeof(float));
Estruturas de Dados PUC-Rio 8-6
return mat; }
Poderamos ainda incluir na criao uma inicializao dos elementos da matriz, por exemplo atribuindo-lhes valores iguais a zero. A funo que libera a memria deve liberar o vetor de elementos e ento liberar a estrutura que representa a matriz:
void libera (Matriz* mat) { free(mat->v); free(mat); }
A funo de acesso e atribuio pode fazer um teste adicional para garantir que no haja invaso de memria. Se a aplicao que usa o mdulo tentar acessar um elemento fora das dimenses da matriz, podemos reportar um erro e abortar o programa. A implementao destas funes pode ser dada por:
float acessa (Matriz* mat, int i, int j) { int k; /* ndice do elemento no vetor */ if (i<0 || i>=mat->lin || j<0 || j>=mat->col) { printf("Acesso invlido!\n); exit(1); } k = i*mat->col + j; return mat->v[k]; } void atribui (Matriz* mat, int i, int j, float v) { int k; /* ndice do elemento no vetor */ if (i<0 || i>=mat->lin || j<0 || j>=mat->col) { printf("Atribuio invlida!\n); exit(1); } k = i*mat->col + j; mat->v[k] = v; }
Matriz com vetor de ponteiros O mdulo de implementao usando a estratgia de representar a matriz por um vetor de ponteiros apresentado a seguir. O tipo que representa a matriz, neste caso, pode ser dado por:
struct matriz { int lin; int col; float** v; };
8-7
As funes para criar uma nova matriz e para liberar uma matriz previamente criada podem ser dadas por:
Matriz* cria (int m, int n) { int i; Matriz mat = (Matriz*) malloc(sizeof(Matriz)); mat->lin = m; mat->col = n; mat->v = (float**) malloc(m*sizeof(float*)); for (i=0; i<m; i++) mat->v[i] = (float*) malloc(n*sizeof(float)); return mat; } void libera (Matriz* mat) { int i; for (i=0; i<mat->lin; i++) free(mat->v[i]); free(mat->v); free(mat); }
As funes para acessar e atribuir podem ser implementadas conforme ilustrado abaixo:
float acessa (Matriz* mat, int i, int j) { if (i<0 || i>=mat->lin || j<0 || j>=mat->col) { printf("Acesso invlido!\n); exit(1); } return mat->v[i][j]; } void atribui (Matriz* mat, int i, int j, float v) { if (i<0 || i>=mat->lin || j<0 || j>=mat->col) { printf("Atribuio invlida!\n); exit(1); } mat->v[i][j] = v; }
Exerccio: Escreva um programa que faa uso das operaes de matriz definidas acima. Note que a estratgia de implementao no deve alterar o uso das operaes. Exerccio: Implemente uma funo que, dada uma matriz, crie dinamicamente a matriz transposta correspondente, fazendo uso das operaes bsicas discutidas acima. Exerccio: Implemente uma funo que determine se uma matriz ou no simtrica quadrada, tambm fazendo uso das operaes bsicas.
8-8
(n 2 n) n (n + 1) = 2 2 Podemos tambm determinar s como sendo a soma de uma progresso aritmtica, pois temos que armazenar um elemento da primeira linha, dois elementos da segunda, trs da terceira, e assim por diante. s =n+
s = 1 + 2 + ... + n =
n (n + 1) 2
A implementao deste tipo abstrato tambm pode ser feita com um vetor simples ou um vetor de ponteiros. A seguir, discutimos a implementao das operaes para criar uma matriz e para acessar os elementos, agora para um tipo que representa uma matriz simtrica.
Matriz simtrica com vetor simples Usando um vetor simples para armazenar os elementos da matriz, dimensionamos o vetor com apenas s elementos. A estrutura que representa a matriz pode ser dada por:
struct matsim { int dim; float* v; }; /* matriz obrigatoriamente quadrada */
Uma funo para criar uma matriz simtrica pode ser dada por:
MatSim* cria (int n) { int s = n*(n+1)/2; MatSim* mat = (MatSim*) malloc(sizeof(MatSim)); mat->dim = n; mat->v = (float*) malloc(s*sizeof(float)); return mat; }
O acesso aos elementos da matriz deve ser feito como se estivssemos representando a matriz inteira. Se for um acesso a um elemento acima da diagonal (i<j), o valor de retorno o elemento simtrico da parte inferior, que est devidamente representado. O endereamento de um elemento da parte inferior da matriz feito saltando-se os elementos das linhas superiores. Assim, se desejarmos acessar um elemento da quinta linha (i=4),
Estruturas de Dados PUC-Rio 8-9
devemos saltar 1+2+3+4 elementos, isto , devemos saltar 1+2+...+i elementos, ou seja, i*(i+1)/2 elementos. Depois, usamos o ndice j para acessar a coluna.
float acessa (MatSim* mat, int i, int j) { int k; /* ndice do elemento no vetor */ if (i<0 || i>=mat->dim || j<0 || j>=mat->dim) { printf("Acesso invlido!\n); exit(1); } if (i>=j) k = i*(i+1)/2 + j; else k = j*(j+1)/2 + i; return mat->v[k]; }
Matriz simtrica com vetor de ponteiros A estratgia de trabalhar com vetores de ponteiros para matrizes alocadas dinamicamente muito adequada para a representao matrizes simtricas. Numa matriz simtrica, para otimizar o uso da memria, armazenamos apenas a parte triangular inferior da matriz. Isto significa que a primeira linha ser representada por um vetor de um nico elemento, a segunda linha ser representada por um vetor de dois elementos e assim por diante. Como o uso de um vetor de ponteiros trata as linhas como vetores independentes, a adaptao desta estratgia para matrizes simtricas fica simples.
Para criar a matriz, basta alocarmos um nmero varivel de elementos para cada linha. O cdigo abaixo ilustra uma possvel implementao:
MatSim* cria (int n) { int i; MatSim* mat = (MatSim*) malloc(sizeof(MatSim)); mat->dim = n; mat->v = (float**) malloc(n*sizeof(float*)); for (i=0; i<n; i++) mat->v[i] = (float*) malloc((i+1)*sizeof(float)); return mat; }
O acesso aos elementos natural, desde que tenhamos o cuidado de no acessar elementos que no estejam explicitamente alocados (isto , elementos com i<j).
8-10
float acessa (MatSim* mat, int i, int j) { if (i<0 || i>=mat->dim || j<0 || j>=mat->dim) { printf("Acesso invlido!\n); exit(1); } if (i>=j) return mat->v[i][j]; else return mat->v[j][i]; }
Finalmente, observamos que exatamente as mesmas tcnicas poderiam ser usadas para representar uma matriz triangular, isto , uma matriz cujos elementos acima (ou abaixo) da diagonal so todos nulos. Neste caso, a principal diferena seria na funo acessa, que teria como resultado o valor zero em um dos lados da diagonal, em vez acessar o valor simtrico. Exerccio: Escreva um cdigo para representar uma matriz triangular inferior. Exerccio: Escreva um cdigo para representar uma matriz triangular superior.
8-11
A partir desses dois arquivos fontes, podemos gerar um programa executvel compilando cada um dos arquivos separadamente e depois ligando-os em um nico
Estruturas de Dados PUC-Rio 9-1
arquivo executvel. Por exemplo, com o compilador Gnu C (gcc) utilizaramos a seguinte seqncia de comandos para gerar o arquivo executvel prog1.exe:
> gcc c str.c > gcc c prog1.c > gcc o prog1.exe str.o prog1.o
O mesmo arquivo str.c pode ser usado para compor outros programas que queiram utilizar suas funes. Para que as funes implementadas em str.c possam ser usadas por um outro mdulo C, este precisa conhecer os cabealhos das funes oferecidas por str.c . No exemplo anterior, isso foi resolvido pela repetio dos cabealhos das funes no incio do arquivo prog1.c. Entretanto, para mdulos que ofeream vrias funes ou que queiram usar funes de muitos outros mdulos, essa repetio manual pode ficar muito trabalhosa e sensvel a erros. Para contornar esse problema, todo mdulo de funes C costuma ter associado a ele um arquivo que contm apenas os cabealhos das funes oferecidas pelo mdulo e, eventualmente, os tipos de dados que ele exporte (typedefs, structs, etc). Esse arquivo de cabealhos segue o mesmo nome do mdulo ao qual est associado, s que com a extenso .h. Assim, poderamos definir um arquivo str.h para o mdulo do exemplo anterior, com o seguinte contedo:
/* Funes oferecidas pelo modulo str.c */ /* Funo comprimento ** Retorna o nmero de caracteres da string passada como parmetro */ int comprimento (char* str); /* Funo copia ** Copia os caracteres da string orig (origem) para dest (destino) */ void copia (char* dest, char* orig); /* Funo concatena ** Concatena a string orig (origem) na string dest (destino) */ void concatena (char* dest, char* orig);
Observe que colocamos vrios comentrios no arquivo str.h. Isso uma prtica muito comum, e tem como finalidade documentar as funes oferecidas por um mdulo. Esses comentrios devem esclarecer qual o comportamento esperado das funes exportadas por um mdulo, facilitando o seu uso por outros programadores (ou pelo mesmo programador algum tempo depois da criao do mdulo). Agora, ao invs de repetir manualmente os cabealhos dessas funes, todo mdulo que quiser usar as funes de str.c precisa apenas incluir o arquivo str.h. No exemplo anterior, o mdulo prog1.c poderia ser simplificado da seguinte forma:
#include <stdio.h> #include "str.h" int main (void) { char str[101], str1[51], str2[51]; printf("Entre com uma seqncia de caracteres: "); scanf(" %50[^\n]", str1); printf("Entre com outra seqncia de caracteres: "); scanf(" %50[^\n]", str2);
Estruturas de Dados PUC-Rio 9-2
Note que os arquivos de cabealhos das funes da biblioteca padro do C (que acompanham seu compilador) so includos da forma #include <arquivo.h>, enquanto que os arquivos de cabealhos dos seus mdulos so geralmente includos da forma #include "arquivo.h". O uso dos delimitadores < > e " " indica para o compilador onde ele deve procurar esses arquivos de cabealhos durante a compilao.
9-3
Arquivo ponto.h:
/* TAD: Ponto (x,y) */ /* Tipo exportado */ typedef struct ponto Ponto; /* Funes exportadas */ /* Funo cria ** Aloca e retorna um ponto com coordenadas (x,y) */ Ponto* cria (float x, float y); /* Funo libera ** Libera a memria de um ponto previamente criado. */ void libera (Ponto* p); /* Funo acessa ** Devolve os valores das coordenadas de um ponto */ void acessa (Ponto* p, float* x, float* y); /* Funo atribui ** Atribui novos valores s coordenadas de um ponto */ void atribui (Ponto* p, float x, float y); /* Funo distancia ** Retorna a distncia entre dois pontos */ float distancia (Ponto* p1, Ponto* p2);
Note que a composio da estrutura Ponto (struct ponto) no exportada pelo mdulo. Dessa forma, os demais mdulos que usarem esse TAD no podero acessar diretamente os campos dessa estrutura. Os clientes desse TAD s tero acesso s informaes que possam ser obtidas atravs das funes exportadas pelo arquivo ponto.h. Agora, mostraremos uma implementao para esse tipo abstrato de dados. O arquivo de implementao do mdulo (arquivo ponto.c ) deve sempre incluir o arquivo de interface do mdulo. Isto necessrio por duas razes. Primeiro, podem existir definies na interface que so necessrias na implementao. No nosso caso, por exemplo, precisamos da definio do tipo Ponto. A segunda razo garantirmos que as funes implementadas correspondem s funes da interface. Como o prottipo das funes exportadas includo, o compilador verifica, por exemplo, se os parmetros das funes implementadas equivalem aos parmetros dos prottipos. Alm da prpria interface, precisamos naturalmente incluir as interfaces das funes que usamos da biblioteca padro.
#include #include #include #include <stdlib.h> <stdio.h> <math.h> "ponto.h" /* malloc, free, exit */ /* printf */ /* sqrt */
9-4
Como s precisamos guardar as coordenadas de um ponto, podemos definir a estrutura ponto da seguinte forma:
struct ponto { float x; float y; };
A funo que cria um ponto dinamicamente deve alocar a estrutura que representa o ponto e inicializar os seus campos:
Ponto* cria (float x, float y) { Ponto* p = (Ponto*) malloc(sizeof(Ponto)); if (p == NULL) { printf("Memria insuficiente!\n"); exit(1); } p->x = x; p->y = y; return p; }
Para esse TAD, a funo que libera um ponto deve apenas liberar a estrutura que foi criada dinamicamente atravs da funo cria:
void libera (Ponto* p) { free(p); }
As funes para acessar e atribuir valores s coordenadas de um ponto so de fcil implementao, como pode ser visto a seguir.
void acessa (Ponto* p, float* x, float* y) { *x = p->x; *y = p->y; } void atribui (Ponto* p, float x, float y) { p->x = x; p->y = y; }
J a operao para calcular a distncia entre dois pontos pode ser implementada da seguinte forma:
float distancia (Ponto* p1, Ponto* p2) { float dx = p2->x p1->x; float dy = p2->y p1->y; return sqrt(dx*dx + dy*dy); }
Exerccio: Escreva um programa que faa uso do TAD ponto definido acima. Exerccio: Acrescente novas operaes ao TAD ponto, tais como soma e subtrao de pontos.
9-5
Exerccio: Acrescente novas operaes ao TAD ponto, de tal forma que seja possvel obter uma representao do ponto em coordenadas polares. Exerccio: Implemente um novo TAD para representar pontos no R3. Exemplo 2: TAD Matriz Como foi discutido anteriormente, a implementao de um TAD fica escondida dentro de seu mdulo. Assim, podemos experimentar diferentes maneiras de implementar um mesmo TAD, sem que isso afete os seus clientes. Para ilustrar essa independncia de implementao, vamos considerar a criao de um tipo abstrato de dados para representar matrizes de valores reais alocadas dinamicamente, com dimenses m por n fornecidas em tempo de execuo. Para tanto, devemos definir um tipo abstrato, que denominaremos de Matriz , e o conjunto de funes que operam sobre esse tipo. Neste exemplo, vamos considerar as seguintes operaes: cria: operao que cria uma matriz de dimenso m por n; libera: operao que libera a memria alocada para a matriz; acessa: operao que acessa o elemento da linha i e da coluna j da matriz; atribui: operao que atribui o elemento da linha i e da coluna j da matriz; linhas: operao que devolve o nmero de linhas da matriz; colunas: operao que devolve o nmero de colunas da matriz. A interface do mdulo pode ser dada pelo cdigo abaixo: Arquivo matriz.h:
/* TAD: matriz m por n */ /* Tipo exportado */ typedef struct matriz Matriz; /* Funes exportadas */ /* Funo cria ** Aloca e retorna uma matriz de dimenso m por n */ Matriz* cria (int m, int n); /* Funo libera ** Libera a memria de uma matriz previamente criada. */ void libera (Matriz* mat); /* Funo acessa ** Retorna o valor do elemento da linha i e coluna j da matriz */ float acessa (Matriz* mat, int i, int j); /* Funo atribui ** Atribui o valor dado ao elemento da linha i e coluna j da matriz */ void atribui (Matriz* mat, int i, int j, float v); /* Funo linhas ** Retorna o nmero de linhas da matriz */ int linhas (Matriz* mat);
Estruturas de Dados PUC-Rio 9-6
/* Funo colunas ** Retorna o nmero de colunas da matriz */ int colunas (Matriz* mat);
A seguir, mostraremos a implementao deste tipo abstrato usando as duas estratgias apresentadas no captulo 8: matrizes dinmicas representadas por vetores simples e matrizes dinmicas representadas por vetores de ponteiros. A interface do mdulo independe da estratgia de implementao adotada, o que altamente desejvel, pois podemos mudar a implementao sem afetar as aplicaes que fazem uso do tipo abstrato. O arquivo matriz1.c apresenta a implementao atravs de vetor simples e o arquivo matriz2.c apresenta a implementao atravs de vetor de ponteiros. Arquivo matriz1.c:
#include <stdlib.h> #include <stdio.h> #include "matriz.h" struct matriz { int lin; int col; float* v; }; Matriz* cria (int m, int n) { Matriz* mat = (Matriz*) malloc(sizeof(Matriz)); if (mat == NULL) { printf("Memria insuficiente!\n"); exit(1); } mat->lin = m; mat->col = n; mat->v = (float*) malloc(m*n*sizeof(float)); return mat; } void libera (Matriz* mat){ free(mat->v); free(mat); } float acessa (Matriz* mat, int i, int j) { int k; /* ndice do elemento no vetor */ if (i<0 || i>=mat->lin || j<0 || j>=mat->col) { printf("Acesso invlido!\n"); exit(1); } k = i*mat->col + j; return mat->v[k]; } void atribui (Matriz* mat, int i, int j, float v) { int k; /* ndice do elemento no vetor */ if (i<0 || i>=mat->lin || j<0 || j>=mat->col) { printf("Atribuio invlida!\n"); exit(1); }
Estruturas de Dados PUC-Rio 9-7
k = i*mat->col + j; mat->v[k] = v; } int linhas (Matriz* mat) { return mat->lin; } int colunas (Matriz* mat) { return mat->col; }
Arquivo matriz2.c:
#include <stdlib.h> #include <stdio.h> #include "matriz.h" struct matriz { int lin; int col; float** v; }; Matriz* cria (int m, int n) { int i; Matriz* mat = (Matriz*) malloc(sizeof(Matriz)); if (mat == NULL) { printf("Memria insuficiente!\n"); exit(1); } mat->lin = m; mat->col = n; mat->v = (float**) malloc(m*sizeof(float*)); for (i=0; i<m; i++) mat->v[i] = (float*) malloc(n*sizeof(float)); return mat; } void libera (Matriz* mat) { int i; for (i=0; i<mat->lin; i++) free(mat->v[i]); free(mat->v); free(mat); } float acessa (Matriz* mat, int i, int j) { if (i<0 || i>=mat->lin || j<0 || j>=mat->col) { printf("Acesso invlido!\n"); exit(1); } return mat->v[i][j]; } void atribui (Matriz* mat, int i, int j, float v) { if (i<0 || i>=mat->lin || j<0 || j>=mat->col) { printf("Atribuio invlida!\n"); exit(1); } mat->v[i][j] = v; } int linhas (Matriz* mat) { return mat->lin; } /* malloc, free, exit */ /* printf */
9-8
Exerccio: Escreva um programa que faa uso do TAD matriz definido acima. Teste o seu programa com as duas implementaes vistas. Exerccio: Usando apenas as operaes definidas pelo TAD matriz, implemente uma funo que determine se uma matriz ou no quadrada simtrica. Exerccio: Usando apenas as operaes definidas pelo TAD matriz, implemente uma funo que, dada uma matriz, crie dinamicamente a matriz transposta correspondente. Exerccio: Defina um TAD para implementar matrizes quadradas simtricas, de acordo com a representao sugerida no captulo 8.
9-9
Ao declararmos um vetor, reservamos um espao contguo de memria para armazenar seus elementos, conforme ilustra a figura abaixo.
vet
Figura 9.1: Um vetor ocupa um espao contguo de memria, permitindo que qualquer elemento seja acessado indexando-se o ponteiro para o primeiro elemento.
O fato de o vetor ocupar um espao contguo na memria nos permite acessar qualquer um de seus elementos a partir do ponteiro para o primeiro elemento. De fato, o smbolo vet, aps a declarao acima, como j vimos, representa um ponteiro para o primeiro elemento do vetor, isto , o valor de vet o endereo da memria onde o primeiro elemento do vetor est armazenado. De posse do ponteiro para o primeiro elemento, podemos acessar qualquer elemento do vetor atravs do operador de indexao vet[i]. Dizemos que o vetor uma estrutura que possibilita acesso randmico aos elementos, pois podemos acessar qualquer elemento aleatoriamente. No entanto, o vetor no uma estrutura de dados muito flexvel, pois precisamos dimension-lo com um nmero mximo de elementos. Se o nmero de elementos que precisarmos armazenar exceder a dimenso do vetor, teremos um problema, pois no existe uma maneira simples e barata (computacionalmente) para alterarmos a dimenso do vetor em tempo de execuo. Por outro lado, se o nmero de elementos que precisarmos armazenar no vetor for muito inferior sua dimenso, estaremos subutilizando o espao de memria reservado. A soluo para esses problemas utilizar estruturas de dados que cresam medida que precisarmos armazenar novos elementos (e diminuam medida que precisarmos retirar elementos armazenados anteriormente). Tais estruturas so chamadas dinmicas e armazenam cada um dos seus elementos usando alocao dinmica. Nas sees a seguir, discutiremos a estrutura de dados conhecida como lista encadeada. As listas encadeadas so amplamente usadas para implementar diversas outras estruturas de dados com semnticas prprias, que sero tratadas nos captulos seguintes.
Estruturas de Dados PUC-Rio 10-1
10.1. ULL
A estrutura consiste numa seqncia encadeada de elementos, em geral chamados de ns da lista. A lista representada por um ponteiro para o primeiro elemento (ou n). Do primeiro elemento, podemos alcanar o segundo seguindo o encadeamento, e assim por diante. O ltimo elemento da lista aponta para NULL, sinalizando que no existe um prximo elemento. Para exemplificar a implementao de listas encadeadas em C, vamos considerar um exemplo simples em que queremos armazenar valores inteiros numa lista encadeada. O n da lista pode ser representado pela estrutura abaixo:
struct lista { int info; struct lista* prox; }; typedef struct lista Lista;
Devemos notar que trata-se de uma estrutura auto-referenciada, pois, alm do campo que armazena a informao (no caso, um nmero inteiro), h um campo que um ponteiro para uma prxima estrutura do mesmo tipo. Embora no seja essencial, uma boa estratgia definirmos o tipo Lista como sinnimo de struct lista, conforme ilustrado acima. O tipo Lista representa um n da lista e a estrutura de lista encadeada representada pelo ponteiro para seu primeiro elemento (tipo Lista*). Considerando a definio de Lista, podemos definir as principais funes necessrias para implementarmos uma lista encadeada. Funo de inicializao A funo que inicializa uma lista deve criar uma lista vazia, sem nenhum elemento. Como a lista representada pelo ponteiro para o primeiro elemento, uma lista vazia
Estruturas de Dados PUC-Rio 10-2
representada pelo ponteiro NULL, pois no existem elementos na lista. A funo tem como valor de retorno a lista vazia inicializada, isto , o valor de retorno NULL. Uma possvel implementao da funo de inicializao mostrada a seguir:
/* funo de inicializao: retorna uma lista vazia */ Lista* inicializa (void) { return NULL; }
Funo de insero Uma vez criada a lista vazia, podemos inserir novos elementos nela. Para cada elemento inserido na lista, devemos alocar dinamicamente a memria necessria para armazenar o elemento e encade-lo na lista existente. A funo de insero mais simples insere o novo elemento no incio da lista. Uma possvel implementao dessa funo mostrada a seguir. Devemos notar que o ponteiro que representa a lista deve ter seu valor atualizado, pois a lista deve passar a ser representada pelo ponteiro para o novo primeiro elemento. Por esta razo, a funo de insero recebe como parmetros de entrada a lista onde ser inserido o novo elemento e a informao do novo elemento, e tem como valor de retorno a nova lista, representada pelo ponteiro para o novo elemento.
/* insero no incio: retorna a lista atualizada */ Lista* insere (Lista* l, int i) { Lista* novo = (Lista*) malloc(sizeof(Lista)); novo->info = i; novo->prox = l; return novo; }
Esta funo aloca dinamicamente o espao para armazenar o novo n da lista, guarda a informao no novo n e faz este n apontar para (isto , ter como prximo elemento) o elemento que era o primeiro da lista. A funo ento retorna o novo valor que representa a lista, que o ponteiro para o novo primeiro elemento. A Figura 9.3 ilustra a operao de insero de um novo elemento no incio da lista. prim
Novo
10.2. ULL
Info1
Info2
Info3
A seguir, ilustramos um trecho de cdigo que cria uma lista inicialmente vazia e insere nela novos elementos.
10-3
int main (void) { Lista* l; l = inicializa(); l = insere(l, 23); l = insere(l, 45); ... return 0; }
/* /* /* /*
declara uma lista no inicializada */ inicializa lista como vazia */ insere na lista o elemento 23 */ insere na lista o elemento 45 */
Observe que no podemos deixar de atualizar a varivel que representa a lista a cada insero de um novo elemento. Funo que percorre os elementos da lista Para ilustrar a implementao de uma funo que percorre todos os elementos da lista, vamos considerar a criao de uma funo que imprima os valores dos elementos armazenados numa lista. Uma possvel implementao dessa funo mostrada a seguir.
/* funo imprime: imprime valores dos elementos */ void imprime (Lista* l) { Lista* p; /* varivel auxiliar para percorrer a lista */ for (p = l; p != NULL; p = p->prox) printf(info = %d\n, p->info); }
Funo que verifica se lista est vazia Pode ser til implementarmos uma funo que verifique se uma lista est vazia ou no. A funo recebe a lista e retorna 1 se estiver vazia ou 0 se no estiver vazia. Como sabemos, uma lista est vazia se seu valor NULL. Uma implementao dessa funo mostrada a seguir:
/* funo vazia: retorna 1 se vazia ou 0 se no vazia */ int vazia (Lista* l) { if (l == NULL) return 1; else return 0; }
Essa funo pode ser re-escrita de forma mais compacta, conforme mostrado abaixo:
/* funo vazia: retorna 1 se vazia ou 0 se no vazia */ int vazia (Lista* l) { return (l == NULL); }
Funo de busca Outra funo til consiste em verificar se um determinado elemento est presente na lista. A funo recebe a informao referente ao elemento que queremos buscar e fornece como valor de retorno o ponteiro do n da lista que representa o elemento. Caso o elemento no seja encontrado na lista, o valor retornado NULL.
Estruturas de Dados PUC-Rio 10-4
/* funo busca: busca um elemento na lista */ Lista* busca (Lista* l, int v) { Lista* p; for (p=l; p!=NULL; p=p->prox) if (p->info == v) return p; return NULL; /* no achou o elemento */ }
Funo que retira um elemento da lista Para completar o conjunto de funes que manipulam uma lista, devemos implementar uma funo que nos permita retirar um elemento. A funo tem como parmetros de entrada a lista e o valor do elemento que desejamos retirar, e deve retornar o valor atualizado da lista, pois, se o elemento removido for o primeiro da lista, o valor da lista deve ser atualizado. A funo para retirar um elemento da lista mais complexa. Se descobrirmos que o elemento a ser retirado o primeiro da lista, devemos fazer com que o novo valor da lista passe a ser o ponteiro para o segundo elemento, e ento podemos liberar o espao alocado para o elemento que queremos retirar. Se o elemento a ser removido estiver no meio da lista, devemos fazer com que o elemento anterior a ele passe a apontar para o elemento seguinte, e ento podemos liberar o elemento que queremos retirar. Devemos notar que, no segundo caso, precisamos do ponteiro para o elemento anterior para podermos acertar o encadeamento da lista. As Figuras 9.4 e 9.5 ilustram as operaes de remoo. prim
10.3. ULL
Info1 Info2 Info3
prim
Info1
Info2
Info3
10.4. ULL Uma possvel implementao da funo para retirar um elemento da lista mostrada a
seguir. Inicialmente, busca-se o elemento que se deseja retirar, guardando uma referncia para o elemento anterior.
10-5
/* funo retira: retira Lista* retira (Lista* l, Lista* ant = NULL; /* Lista* p = l; /*
elemento da lista */ int v) { ponteiro para elemento anterior */ ponteiro para percorrer a lista*/
/* procura elemento na lista, guardando anterior */ while (p != NULL && p->info != v) { ant = p; p = p->prox; } /* verifica se achou elemento */ if (p == NULL) return l; /* no achou: retorna lista original */ /* retira elemento */ if (ant == NULL) { /* retira elemento do inicio */ l = p->prox; } else { /* retira elemento do meio da lista */ ant->prox = p->prox; } free(p); return l; }
O caso de retirar o ltimo elemento da lista recai no caso de retirar um elemento no meio da lista, conforme pode ser observado na implementao acima. Mais adiante, estudaremos a implementao de filas com listas encadeadas. Numa fila, devemos armazenar, alm do ponteiro para o primeiro elemento, um ponteiro para o ltimo elemento. Nesse caso, se for removido o ltimo elemento, veremos que ser necessrio atualizar a fila. Funo para liberar a lista Uma outra funo til que devemos considerar destri a lista, liberando todos os elementos alocados. Uma implementao dessa funo mostrada abaixo. A funo percorre elemento a elemento, liberando-os. importante observar que devemos guardar a referncia para o prximo elemento antes de liberar o elemento corrente (se liberssemos o elemento e depois tentssemos acessar o encadeamento, estaramos acessando um espao de memria que no estaria mais reservado para nosso uso).
void libera (Lista* l) { Lista* p = l; while (p != NULL) { Lista* t = p->prox; /* guarda referncia para o prximo elemento */ free(p); /* libera a memria apontada por p */ p = t; /* faz p apontar para o prximo */ } }
10-6
/* imprimir: 56 45 23 */ /* imprimir: 56 23 */
Mais uma vez, observe que no podemos deixar de atualizar a varivel que representa a lista a cada insero e a cada remoo de um elemento. Esquecer de atribuir o valor de retorno varivel que representa a lista pode gerar erros graves. Se, por exemplo, a funo retirar o primeiro elemento da lista, a varivel que representa a lista, se no fosse atualizada, estaria apontando para um n j liberado. Como alternativa, poderamos fazer com que as funes insere e retira recebessem o endereo da varivel que representa a lista. Nesse caso, os parmetros das funes seriam do tipo ponteiro para lista (Lista** l) e seu contedo poderia ser acessado/atualizado de dentro da funo usando o operador contedo (*l). Manuteno da lista ordenada A funo de insero vista acima armazena os elementos na lista na ordem inversa ordem de insero, pois um novo elemento sempre inserido no incio da lista. Se quisermos manter os elementos na lista numa determinada ordem, temos que encontrar a posio correta para inserir o novo elemento. Essa funo no eficiente, pois temos que percorrer a lista, elemento por elemento, para acharmos a posio de insero. Se a ordem de armazenamento dos elementos dentro da lista no for relevante, optamos por fazer inseres no incio, pois o custo computacional disso independe do nmero de elementos na lista. No entanto, se desejarmos manter os elementos em ordem, cada novo elemento deve ser inserido na ordem correta. Para exemplificar, vamos considerar que queremos manter nossa lista de nmeros inteiros em ordem crescente. A funo de insero, neste caso, tem a mesma assinatura da funo de insero mostrada, mas percorre os elementos da lista a fim de encontrar a posio correta para a insero do novo. Com isto, temos que saber inserir um elemento no meio da lista. A Figura 9.6 ilustra a insero de um elemento no meio da lista.
10-7
prim
Info1 Info2 Info3
10.5. ULL
Novo
Conforme ilustrado na figura, devemos localizar o elemento da lista que ir preceder o elemento novo a ser inserido. De posse do ponteiro para esse elemento, podemos encadear o novo elemento na lista. O novo apontar para o prximo elemento na lista e o elemento precedente apontar para o novo. O cdigo abaixo ilustra a implementao dessa funo. Neste caso, utilizamos uma funo auxiliar responsvel por alocar memria para o novo n e atribuir o campo da informao.
/* funo auxiliar: cria e inicializa um n */ Lista* cria (int v) { Lista* p = (Lista*) malloc(sizeof(Lista)); p->info = v; return p; } /* funo insere_ordenado: insere elemento em ordem */ Lista* insere_ordenado (Lista* l, int v) { Lista* novo = cria(v); /* cria novo n */ Lista* ant = NULL; /* ponteiro para elemento anterior */ Lista* p = l; /* ponteiro para percorrer a lista*/ /* procura posio de insero */ while (p != NULL && p->info < v) { ant = p; p = p->prox; } /* insere elemento */ if (ant == NULL) { /* insere elemento no incio */ novo->prox = l; l = novo; } else { /* insere elemento no meio da lista */ novo->prox = ant->prox; ant->prox = novo; } return l; }
Devemos notar que essa funo, analogamente ao observado para a funo de remoo, tambm funciona se o elemento tiver que ser inserido no final da lista.
10-8
fcil alterarmos o cdigo acima para obtermos a impresso dos elementos da lista em ordem inversa: basta invertermos a ordem das chamadas s funes printf e imprime_rec. A funo para retirar um elemento da lista tambm pode ser escrita de forma recursiva. Neste caso, s retiramos um elemento se ele for o primeiro da lista (ou da sub-lista). Se o elemento que queremos retirar no for o primeiro, chamamos a funo recursivamente para retirar o elemento da sub-lista.
/* Funo retira recursiva */ Lista* retira_rec (Lista* l, int v) { if (vazia(l)) return l; /* lista vazia: retorna valor original */ /* verifica se elemento a ser retirado o primeiro */ if (l->info == v) { Lista* t = l; /* temporrio para poder liberar */ l = l->prox; free(t); } else { /* retira de sub-lista */ l->prox = retira_rec(l->prox,v); } return l; }
A funo para liberar uma lista tambm pode ser escrita recursivamente, de forma bastante simples. Nessa funo, se a lista no for vazia, liberamos primeiro a sub-lista e depois liberamos a lista.
10-9
Exerccio: Implemente uma funo que verifique se duas listas encadeadas so iguais. Duas listas so consideradas iguais se tm a mesma seqncia de elementos. O prottipo da funo deve ser dado por:
int igual (Lista* l1, Lista* l2);
Exerccio: Implemente uma funo que crie uma cpia de uma lista encadeada. O prottipo da funo deve ser dado por:
Lista* copia (Lista* l);
Analogamente, se quisermos representar uma lista de nmeros reais, podemos definir a estrutura do n como sendo:
struct lista { float info; struct lista *prox; }; typedef struct lista Lista;
A informao armazenada na lista no precisa ser necessariamente um dado simples. Podemos, por exemplo, considerar a construo de uma lista para armazenar um conjunto de retngulos. Cada retngulo definido pela base b e pela altura h. Assim, a estrutura do n pode ser dada por:
struct lista { float b; float h; struct lista *prox; }; typedef struct lista Lista;
Esta mesma composio pode ser escrita de forma mais clara se definirmos um tipo adicional que represente a informao. Podemos definir um tipo Retangulo e us-lo para representar a informao armazenada na lista.
struct retangulo { float b; float h; }; typedef struct retangulo Retangulo;
10-10
struct lista { Retangulo info; struct lista *prox; }; typedef struct lista Lista;
Aqui, a informao volta a ser representada por um nico campo (info), que uma estrutura. Se p fosse um ponteiro para um n da lista, o valor da base do retngulo armazenado nesse n seria acessado por: p->info.b. Ainda mais interessante termos o campo da informao representado por um ponteiro para a estrutura, em vez da estrutura em si.
struct retangulo { float b; float h; }; typedef struct retangulo Retangulo; struct lista { Retangulo *info; struct lista *prox; }; typedef struct lista Lista;
Neste caso, para criarmos um n, temos que fazer duas alocaes dinmicas: uma para criar a estrutura do retngulo e outra para criar a estrutura do n. O cdigo abaixo ilustra uma funo para a criao de um n.
Lista* cria (void) { Retangulo* r = (Retangulo*) malloc(sizeof(Retangulo)); Lista* p = (Lista*) malloc(sizeof(Lista)); p->info = r; p->prox = NULL; return p; }
Naturalmente, o valor da base associado a um n p seria agora acessado por: p->info>b . A vantagem dessa representao (utilizando ponteiros) que, independente da informao armazenada na lista, a estrutura do n sempre composta por um ponteiro para a informao e um ponteiro para o prximo n da lista. A representao da informao por um ponteiro nos permite construir listas heterogneas, isto , listas em que as informaes armazenadas diferem de n para n. Diversas aplicaes precisam construir listas heterogneas, pois necessitam agrupar elementos afins mas no necessariamente iguais. Como exemplo, vamos considerar uma aplicao que necessite manipular listas de objetos geomtricos planos para clculos de reas. Para simplificar, vamos considerar que os objetos podem ser apenas retngulos, tringulos ou crculos. Sabemos que as reas desses objetos so dadas por:
r = b*h t= b*h 2 c = p r2
10-11
O n da lista deve ser composto por trs campos: um identificador de qual objeto est armazenado no n um ponteiro para a estrutura que contm a informao um ponteiro para o prximo n da lista importante salientar que, a rigor, a lista homognea, no sentido de que todos os ns contm as mesmas informaes. O ponteiro para a informao deve ser do tipo genrico, pois no sabemos a princpio para que estrutura ele ir apontar: pode apontar para um retngulo, um tringulo ou um crculo. Um ponteiro genrico em C representado pelo tipo void*. A funo do tipo ponteiro genrico pode representar qualquer endereo de memria, independente da informao de fato armazenada nesse espao. No entanto, de posse de um ponteiro genrico, no podemos acessar a memria por ele apontada, j que no sabemos a informao armazenada. Por esta razo, o n de uma lista genrica deve guardar explicitamente um identificador do tipo de objeto de fato armazenado. Consultando esse identificador, podemos converter o ponteiro genrico no ponteiro especfico para o objeto em questo e, ento, acessarmos os campos do objeto. Como identificador de tipo, podemos usar valores inteiros definidos como constantes simblicas:
#define RET 0 #define TRI 1 #define CIR 2
Assim, na criao do n, armazenamos o identificador de tipo correspondente ao objeto sendo representado. A estrutura que representa o n pode ser dada por:
/* Define o n da estrutura */ struct listagen { int tipo; void *info; struct listagen *prox; }; typedef struct listagen ListaGen;
A funo para a criao de um n da lista pode ser definida por trs variaes, uma para cada tipo de objeto que pode ser armazenado.
10-12
/* Cria um n com um retngulo, inicializando os campos base e altura */ ListaGen* cria_ret (float b, float h) { Retangulo* r; ListaGen* p; /* aloca retngulo */ r = (Retangulo*) malloc(sizeof(Retangulo)); r->b = b; r->h = h; /* aloca n */ p = (ListaGen*) malloc(sizeof(ListaGen)); p->tipo = RET; p->info = r; p->prox = NULL; return p; } /* Cria um n com um tringulo, inicializando os campos base e altura */ ListaGen* cria_tri (float b, float h) { Triangulo* t; ListaGen* p; /* aloca tringulo */ t = (Triangulo*) malloc(sizeof(Triangulo)); t->b = b; t->h = h; /* aloca n */ p = (ListaGen*) malloc(sizeof(ListaGen)); p->tipo = TRI; p->info = t; p->prox = NULL; return p; } /* Cria um n com um crculo, inicializando o campo raio */ ListaGen* cria_cir (float r) { Circulo* c; ListaGen* p; /* aloca crculo */ c = (Circulo*) malloc(sizeof(Circulo)); c->r = r; /* aloca n */ p = (ListaGen*) malloc(sizeof(ListaGen)); p->tipo = CIR; p->info = c; p->prox = NULL; return p; }
Uma vez criado o n, podemos inseri-lo na lista como j vnhamos fazendo com ns de listas homogneas. As constantes simblicas que representam os tipos dos objetos podem ser agrupadas numa enumerao (ver seo 7.5):
Estruturas de Dados PUC-Rio 10-13
Manipulao de listas heterogneas Para exemplificar a manipulao de listas heterogneas, considerando a existncia de uma lista com os objetos geomtricos apresentados acima, vamos implementar uma funo que fornea como valor de retorno a maior rea entre os elementos da lista. Uma implementao dessa funo mostrada abaixo, onde criamos uma funo auxiliar que calcula a rea do objeto armazenado num determinado n da lista:
#define PI 3.14159 /* funo auxiliar: calcula rea correspondente ao n */ float area (ListaGen *p) { float a; /* rea do elemento */ switch (p->tipo) { case RET: { /* converte para retngulo e calcula rea */ Retangulo *r = (Retangulo*) p->info; a = r->b * r->h; } break; case TRI: { /* converte para tringulo e calcula rea */ Triangulo *t = (Triangulo*) p->info; a = (t->b * t->h) / 2; } break; case CIR: { /* converte para crculo e calcula rea */ Circulo *c = (Circulo)p->info; a = PI * c->r * c->r; } break; } return a; } /* Funo para clculo da maior rea */ float max_area (ListaGen* l) { float amax = 0.0; /* maior rea */ ListaGen* p; for (p=l; p!=NULL; p=p->prox) { float a = area(p); /* rea do n */ if (a > amax) amax = a; } return amax; }
10-14
A funo para o clculo da rea mostrada acima pode ser subdivida em funes especficas para o clculo das reas de cada objeto geomtrico, resultando em um cdigo mais estruturado.
/* funo para clculo da rea de um retngulo */ float ret_area (Retangulo* r) { return r->b * r->h; } /* funo para clculo da rea de um tringulo */ float tri_area (Triangulo* t) { return (t->b * t->h) / 2; } /* funo para clculo da rea de um crculo */ float cir_area (Circulo* c) { return PI * c->r * c->r; } /* funo para clculo da rea do n (verso 2) */ float area (ListaGen* p) { float a; switch (p->tipo) { case RET: a = ret_area(p->info); break; case TRI: a = tri_area(p->info); break; case CIR: a = cir_area(p->info); break; } return a; }
Neste caso, a converso de ponteiro genrico para ponteiro especfico feita quando chamamos uma das funes de clculo da rea: passa-se um ponteiro genrico que atribudo, atravs da converso implcita de tipo, para um ponteiro especfico1. Devemos salientar que, quando trabalhamos com converso de ponteiros genricos, temos que garantir que o ponteiro armazene o endereo onde de fato existe o tipo especfico correspondente. O compilador no tem como checar se a converso vlida; a verificao do tipo passa a ser responsabilidade do programador.
Este cdigo no vlido em C++. A linguagem C++ no tem converso implcita de um ponteiro genrico para um ponteiro especfico. Para compilar em C++, devemos fazer a converso explicitamente. Por exemplo:
a = ret_area((Retangulo*)p->info);
Estruturas de Dados PUC-Rio 10-15
elemento. A lista pode ser representada por um ponteiro para um elemento inicial qualquer da lista. A Figura 9.7 ilustra o arranjo da memria para a representao de uma lista circular. ini
Info1 Info2 Info3
Para percorrer os elementos de uma lista circular, visitamos todos os elementos a partir do ponteiro do elemento inicial at alcanarmos novamente esse mesmo elemento. O cdigo abaixo exemplifica essa forma de percorrer os elementos. Neste caso, para simplificar, consideramos uma lista que armazena valores inteiros. Devemos salientar que o caso em que a lista vazia ainda deve ser tratado (se a lista vazia, o ponteiro para um elemento inicial vale NULL).
void imprime_circular (Lista* l) { Lista* p = l; /* faz p apontar para o n inicial */ /* testa se lista no vazia */ if (p) { { /* percorre os elementos at alcanar novamente o incio */ do { printf("%d\n", p->info); /* imprime informao do n */ p = p->prox; /* avana para o prximo n */ } while (p != l); }
Exerccio: Escreva as funes para inserir e retirar um elemento de uma lista circular.
10-16
prim
Info1 Info2 Info3
Para exemplificar a implementao de listas duplamente encadeadas, vamos novamente considerar o exemplo simples no qual queremos armazenar valores inteiros na lista. O n da lista pode ser representado pela estrutura abaixo e a lista pode ser representada atravs do ponteiro para o primeiro n.
struct lista2 { int info; struct lista2* ant; struct lista2* prox; }; typedef struct Lista2 Lista2;
Com base nas definies acima, exemplificamos a seguir a implementao de algumas funes que manipulam listas duplamente encadeadas. Funo de insero O cdigo a seguir mostra uma possvel implementao da funo que insere novos elementos no incio da lista. Aps a alocao do novo elemento, a funo acertar o duplo encadeamento.
/* insero no incio */ Lista2* insere (Lista2* l, int v) { Lista2* novo = (Lista2*) malloc(sizeof(Lista2)); novo->info = v; novo->prox = l; novo->ant = NULL; /* verifica se lista no est vazia */ if (l != NULL) l->ant = novo; return novo; }
Nessa funo, o novo elemento encadeado no incio da lista. Assim, ele tem como prximo elemento o antigo primeiro elemento da lista e como anterior o valor NULL. A seguir, a funo testa se a lista no era vazia, pois, neste caso, o elemento anterior do ento primeiro elemento passa a ser o novo elemento. De qualquer forma, o novo elemento passa a ser o primeiro da lista, e deve ser retornado como valor da lista atualizada. A Figura 9.9 ilustra a operao de insero de um novo elemento no incio da lista.
10-17
prim
Novo
Info1
Info2
Info3
Funo de busca A funo de busca recebe a informao referente ao elemento que queremos buscar e tem como valor de retorno o ponteiro do n da lista que representa o elemento. Caso o elemento no seja encontrado na lista, o valor retornado NULL.
/* funo busca: busca um elemento na lista */ Lista2* busca (Lista2* l, int v) { Lista2* p; for (p=l; p!=NULL; p=p->prox) if (p->info == v) return p; return NULL; /* no achou o elemento */ }
Funo que retira um elemento da lista A funo de remoo fica mais complicada, pois temos que acertar o encadeamento duplo. Em contrapartida, podemos retirar um elemento da lista conhecendo apenas o ponteiro para esse elemento. Desta forma, podemos usar a funo de busca acima para localizar o elemento e em seguida acertar o encadeamento, liberando o elemento ao final. Se p representa o ponteiro do elemento que desejamos retirar, para acertar o encadeamento devemos conceitualmente fazer:
p->ant->prox = p->prox; p->prox->ant = p->ant;
isto , o anterior passa a apontar para o prximo e o prximo passa a apontar para o anterior. Quando p apontar para um elemento no meio da lista, as duas atribuies acima so suficientes para efetivamente acertar o encadeamento da lista. No entanto, se p for um elemento no extremo da lista, devemos considerar as condies de contorno. Se p for o primeiro, no podemos escrever p->ant->prox, pois p->ant NULL; alm disso, temos que atualizar o valor da lista, pois o primeiro elemento ser removido.
10-18
Lista circular duplamente encadeada Uma lista circular tambm pode ser construda com encadeamento duplo. Neste caso, o que seria o ltimo elemento da lista passa ter como prximo o primeiro elemento, que, por sua vez, passa a ter o ltimo como anterior. Com essa construo podemos percorrer a lista nos dois sentidos, a partir de um ponteiro para um elemento qualquer. Abaixo, ilustramos o cdigo para imprimir a lista no sentido reverso, isto , percorrendo o encadeamento dos elementos anteriores.
void imprime_circular_rev (Lista2* l) { Lista2* p = l; /* faz p apontar para o n inicial */ /* testa se lista no vazia */ if (p) { { /* percorre os elementos at alcanar novamente o incio */ do { printf("%d\n", p->info); /* imprime informao do n */ p = p->ant; /* "avana" para o n anterior */ } while (p != l); }
Exerccio: Escreva as funes para inserir e retirar um elemento de uma lista circular duplamente encadeada.
10-19
11. Pilhas
W. Celes e J. L. Rangel Uma das estruturas de dados mais simples a pilha. Possivelmente por essa razo, a estrutura de dados mais utilizada em programao, sendo inclusive implementada diretamente pelo hardware da maioria das mquinas modernas. A idia fundamental da pilha que todo o acesso a seus elementos feito atravs do seu topo. Assim, quando um elemento novo introduzido na pilha, passa a ser o elemento do topo, e o nico elemento que pode ser removido da pilha o do topo. Isto faz com que os elementos da pilha sejam retirados na ordem inversa ordem em que foram introduzidos: o primeiro que sai o ltimo que entrou (a sigla LIFO last in, first out usada para descrever esta estratgia). Para entendermos o funcionamento de uma estrutura de pilha, podemos fazer uma analogia com uma pilha de pratos. Se quisermos adicionar um prato na pilha, o colocamos no topo. Para pegar um prato da pilha, retiramos o do topo. Assim, temos que retirar o prato do topo para ter acesso ao prximo prato. A estrutura de pilha funciona de maneira anloga. Cada novo elemento inserido no topo e s temos acesso ao elemento do topo da pilha. Existem duas operaes bsicas que devem ser implementadas numa estrutura de pilha: a operao para empilhar um novo elemento, inserindo-o no topo, e a operao para desempilhar um elemento, removendo-o do topo. comum nos referirmos a essas duas operaes pelos termos em ingls push (empilhar) e pop (desempilhar). A Figura 10.1 ilustra o funcionamento conceitual de uma pilha.
push (a) push (b) push (c) pop () retorna-se c push (d) pop () retorna-se d
topo
b a
topo
c b a
topo b a topo
d b a
topo
b a
topo
O exemplo de utilizao de pilha mais prximo a prpria pilha de execuo da linguagem C. As variveis locais das funes so dispostas numa pilha e uma funo s tem acesso s variveis que esto no topo (no possvel acessar as variveis da funo locais s outras funes). H vrias implementaes possveis de uma pilha, que se distinguem pela natureza dos seus elementos, pela maneira como os elementos so armazenados e pelas operaes disponveis para o tratamento da pilha.
10-1
A funo cria aloca dinamicamente a estrutura da pilha, inicializa seus campos e retorna seu ponteiro; as funes push e pop inserem e retiram, respectivamente, um valor real na pilha; a funo vazia informa se a pilha est ou no vazia; e a funo libera destri a pilha, liberando toda a memria usada pela estrutura.
10-2
A funo para criar a pilha aloca dinamicamente essa estrutura e inicializa a pilha como sendo vazia, isto , com o nmero de elementos igual a zero.
Pilha* cria (void) { Pilha* p = (Pilha*) malloc(sizeof(Pilha)); p->n = 0; /* inicializa com zero elementos */ return p; }
Para inserir um elemento na pilha, usamos a prxima posio livre do vetor. Devemos ainda assegurar que exista espao para a insero do novo elemento, tendo em vista que trata-se de um vetor com dimenso fixa.
void push (Pilha* p, float v) { if (p->n == MAX) { /* capacidade esgotada */ printf("Capacidade da pilha estourou.\n"); exit(1); /* aborta programa */ } /* insere elemento na prxima posio livre */ p->vet[p->n] = v; p->n++; }
A funo pop retira o elemento do topo da pilha, fornecendo seu valor como retorno. Podemos tambm verificar se a pilha est ou no vazia.
float pop (Pilha* p) { float v; if (vazia(p)) { printf("Pilha vazia.\n"); exit(1); /* aborta programa */ } /* retira elemento do topo */ v = p->vet[p->n-1]; p->n--; return v; }
A funo que verifica se a pilha est vazia pode ser dada por:
int vazia (Pilha* p) { return (p->n == 0); }
Finalmente, a funo para liberar a memria alocada pela pilha pode ser:
void libera (Pilha* p) { free(p); }
A funo cria aloca a estrutura da pilha e inicializa a lista como sendo vazia.
Pilha* cria (void) { Pilha* p = (Pilha*) malloc(sizeof(Pilha)); p->prim = NULL; return p; }
O primeiro elemento da lista representa o topo da pilha. Cada novo elemento inserido no incio da lista e, conseqentemente, sempre que solicitado, retiramos o elemento tambm do incio da lista. Desta forma, precisamos de duas funes auxiliares da lista: para inserir no incio e para remover do incio. Ambas as funes retornam o novo primeiro n da lista.
/* funo auxiliar: insere no incio */ No* ins_ini (No* l, float v) { No* p = (No*) malloc(sizeof(No)); p->info = v; p->prox = l; return p; } /* funo auxiliar: retira do incio */ No* ret_ini (No* l) { No* p = l->prox; free(l); return p; }
Por fim, a funo que libera a pilha deve antes liberar todos os elementos da lista.
void libera (Pilha* p) { No* q = p->prim; while (q!=NULL) { No* t = q->prox; free(q); q = t; } free(p); }
A rigor, pela definio da estrutura de pilha, s temos acesso ao elemento do topo. No entanto, para testar o cdigo, pode ser til implementarmos uma funo que imprima os valores armazenados na pilha. Os cdigos abaixo ilustram a implementao dessa funo nas duas verses de pilha (vetor e lista). A ordem de impresso adotada do topo para a base.
/* imprime: verso com vetor */ void imprime (Pilha* p) { int i; for (i=p->n-1; i>=0; i--) printf("%f\n",p->vet[i]); } /* imprime: verso com lista */ void imprime (Pilha* p) { No* q; for (q=p->prim; q!=NULL; q=q->prox) printf("%f\n",q->info); }
Como exemplo de aplicao de uma estrutura de pilha, vamos implementar uma calculadora ps-fixada. Ela deve ter uma pilha de valores reais para representar os operandos. Para enriquecer a implementao, vamos considerar que o formato com que os valores da pilha so impressos seja um dado adicional associado calculadora. Esse formato pode, por exemplo, ser passado quando da criao da calculadora. Para representar a interface exportada pela calculadora, podemos criar o arquivo calc.h:
/* Arquivo que define a interface da calculadora */ typedef struct calc Calc; /* funes exportadas */ Calc* cria_calc (char* f); void operando (Calc* c, float v); void operador (Calc* c, char op); void libera_calc (Calc* c);
Essas funes utilizam as funes mostradas acima, independente da implementao usada na pilha (vetor ou lista). O tipo que representa a calculadora pode ser dado por:
struct calc { char f[21]; Pilha* p; }; /* formato para impresso */ /* pilha de operandos */
A funo cria recebe como parmetro de entrada uma cadeia de caracteres com o formato que ser utilizado pela calculadora para imprimir os valores. Essa funo cria uma calculadora inicialmente sem operandos na pilha.
Calc* cria_calc (char* formato) { Calc* c = (Calc*) malloc(sizeof(Calc)); strcpy(c->f,formato); c->p = cria(); /* cria pilha vazia */ return c; }
A funo operando coloca no topo da pilha o valor passado como parmetro. A funo operador retira os dois valores do topo da pilha (s consideraremos operadores binrios), efetua a operao correspondente e coloca o resultado no topo da pilha. As operaes vlidas so: '+' para somar, '-' para subtrair, '*' para multiplicar e '/' para dividir. Se no existirem operandos na pilha, consideraremos que seus valores so zero. Tanto a funo operando quanto a funo operador imprimem, utilizando o formato especificado na funo cria, o novo valor do topo da pilha.
void operando (Calc* c, float v) { /* empilha operando */ push(c->p,v); /* imprime topo da pilha */ printf(c->f,v); }
10-6
void operador (Calc* c, char op) { float v1, v2, v; /* desempilha operandos */ if (vazia(c->p)) v2 = 0.0; else v2 = pop(c->p); if (vazia(c->p)) v1 = 0.0; else v1 = pop(c->p); /* faz operao switch (op) { case '+': v case '-': v case '*': v case '/': v } */ = = = = v1+v2; v1-v2; v1*v2; v1/v2; break; break; break; break;
Por fim, a funo para liberar a memria usada pela calculadora libera a pilha de operandos e a estrutura da calculadora.
void libera_calc (Calc* c) { libera(c->p); free(c); }
Esse programa cliente l os dados fornecidos pelo usurio e opera a calculadora. Para tanto, o programa l um caractere e verifica se um operador vlido. Em caso negativo, o programa devolve o caractere lido para o buffer de leitura, atravs da funo ungetc, e tenta ler um operando. O usurio finaliza a execuo do programa digitando q. Se executado, e considerando-se as expresses digitadas pelo usurio mostradas abaixo, esse programa teria como sada:
3 5 8 * + 3.00 5.00 8.00 40.00 43.00 7 / 7.00 6.14 q digitado pelo usurio
Exerccio: Estenda a funcionalidade da calculadora incluindo novos operadores unrios e binrios (sugesto: ~ como menos unrio, # como raiz quadrada, ^ como exponenciao).
10-8
12. Filas
W. Celes e J. L. Rangel
Outra estrutura de dados bastante usada em computao a fila. Na estrutura de fila, os acessos aos elementos tambm seguem uma regra. O que diferencia a fila da pilha a ordem de sada dos elementos: enquanto na pilha o ltimo que entra o primeiro que sai, na fila o primeiro que entra o primeiro que sai (a sigla FIFO first in, first out usada para descrever essa estratgia). A idia fundamental da fila que s podemos inserir um novo elemento no final da fila e s podemos retirar o elemento do incio. A estrutura de fila uma analogia natural com o conceito de fila que usamos no nosso dia a dia: quem primeiro entra numa fila o primeiro a ser atendido (a sair da fila). Um exemplo de utilizao em computao a implementao de uma fila de impresso. Se uma impressora compartilhada por vrias mquinas, deve-se adotar uma estratgia para determinar que documento ser impresso primeiro. A estratgia mais simples tratar todas as requisies com a mesma prioridade e imprimir os documentos na ordem em que foram submetidos o primeiro submetido o primeiro a ser impresso. De modo anlogo ao que fizemos com a estrutura de pilha, neste captulo discutiremos duas estratgias para a implementao de uma estrutura de fila: usando vetor e usando lista encadeada. Para implementar uma fila, devemos ser capazes de inserir novos elementos em uma extremidade, o fim, e retirar elementos da outra extremidade, o incio.
A funo cria aloca dinamicamente a estrutura da fila, inicializa seus campos e retorna seu ponteiro; a funo insere adiciona um novo elemento no final da fila e a funo
11-1
retira remove o elemento do incio; a funo vazia informa se a fila est ou no vazia; e a funo libera destri a estrutura, liberando toda a memria alocada.
1.4 ini
2.2
3.5
4.0 fim
3.5 ini
4.0 fim
Com essa estratgia, fcil observar que, em um dado instante, a parte ocupada do vetor pode chegar ltima posio. Para reaproveitar as primeiras posies livres do vetor sem implementarmos uma re-arrumao trabalhosa dos elementos, podemos incrementar as posies do vetor de forma circular: se o ltimo elemento da fila ocupa a ltima posio do vetor, inserimos os novos elementos a partir do incio do vetor. Desta forma, em um dado momento, poderamos ter quatro elementos, 20.0, 20.8, 21.2 e 24.3, distribudos dois no fim do vetor e dois no incio.
0 1 98 99
21.2
24.3
fim
20.0 ini
20.8
11-2
Para essa implementao, os ndices do vetor so incrementados de maneira que seus valores progridam circularmente. Desta forma, se temos 100 posies no vetor, os valores dos ndices assumem os seguintes valores:
0, 1, 2, 3, , 98, 99, 0, 1, 2, 3, , 98, 99, 0, 1,
Podemos definir uma funo auxiliar responsvel por incrementar o valor de um ndice. Essa funo recebe o valor do ndice atual e fornece com valor de retorno o ndice incrementado, usando o incremento circular. Uma possvel implementao dessa funo :
int incr (int i) { if (i == N-1) return 0; else return i+1; }
Essa mesma funo pode ser implementada de uma forma mais compacta, usando o operador mdulo:
int incr(int i) { return (i+1)%N; }
Com o uso do operador mdulo, muitas vezes optamos inclusive por dispensar a funo auxiliar e escrever diretamente o incremento circular:
... i=(i+1)%N; ...
Podemos declarar o tipo fila como sendo uma estrutura com trs componentes: um vetor vet de tamanho N, um ndice ini para o incio da fila e um ndice fim para o fim da fila. Conforme ilustrado nas figuras acima, usamos as seguintes convenes para a identificao da fila: ini marca a posio do prximo elemento a ser retirado da fila; fim marca a posio (vazia), onde ser inserido o prximo elemento. Desta forma, a fila vazia se caracteriza por ter ini == fim e a fila cheia (quando no possvel inserir mais elementos) se caracteriza por ter f i m e ini em posies consecutivas (circularmente): incr(fim) == ini. Note que, com essas convenes, a posio indicada por fim permanece sempre vazia, de forma que o nmero mximo de elementos na fila N-1. Isto necessrio porque a insero de mais um elemento faria ini == fim, e haveria uma ambigidade entre fila cheia e fila vazia. Outra estratgia possvel consiste em armazenar uma informao adicional, n, que indicaria explicitamente o nmero de elementos armazenados na fila. Assim, a fila estaria vazia se n == 0 e cheia se n == N-1. Nos exemplos que se seguem, optamos por no armazenar n explicitamente.
11-3
A funo para criar a fila aloca dinamicamente essa estrutura e inicializa a fila como sendo vazia, isto , com os ndices ini e fim iguais entre si (no caso, usamos o valor zero).
Fila* cria (void) { Fila* f = (Fila*) malloc(sizeof(Fila)); f->ini = f->fim = 0; /* inicializa fila vazia */ return f; }
Para inserir um elemento na fila, usamos a prxima posio livre do vetor, indicada por fim. Devemos ainda assegurar que h espao para a insero do novo elemento, tendo em vista que trata-se de um vetor com capacidade limitada. Consideraremos que a funo auxiliar que faz o incremento circular est disponvel.
void insere (Fila* f, float v) { if (incr(f->fim) == f->ini) { /* fila cheia: capacidade esgotada */ printf("Capacidade da fila estourou.\n"); exit(1); /* aborta programa */ } /* insere elemento na prxima posio livre */ f->vet[f->fim] = v; f->fim = incr(f->fim); }
A funo para retirar o elemento do incio da fila fornece o valor do elemento retirado como retorno. Podemos tambm verificar se a fila est ou no vazia.
float retira (Fila* f) { float v; if (vazia(f)) { printf("Fila vazia.\n"); exit(1); /* aborta programa */ } /* retira elemento do incio */ v = f->vet[f->ini]; f->ini = incr(f->ini); return v; }
A funo que verifica se a fila est vazia pode ser dada por:
int vazia (Fila* f) { return (f->ini == f->fim); }
11-4
Finalmente, a funo para liberar a memria alocada pela fila pode ser:
void libera (Fila* f) { free(f); }
Info1
Info2
Info3
A operao para retirar um elemento se d no incio da lista (fila) e consiste essencialmente em fazer com que, aps a remoo, ini aponte para o sucessor do n retirado. (Observe que seria mais complicado remover um n do fim da lista, porque o antecessor de um n no encontrado com a mesma facilidade que seu sucessor.) A insero tambm simples, pois basta acrescentar lista um sucessor para o ltimo n, apontado por fim, e fazer com que fim aponte para este novo n. O n da lista para armazenar valores reais, como j vimos, pode ser dado por:
struct no { float info; struct no* prox; }; typedef struct no No;
A funo cria aloca a estrutura da fila e inicializa a lista como sendo vazia.
Fila* cria (void) { Fila* f = (Fila*) malloc(sizeof(Fila)); f->ini = f->fim = NULL; return f; }
Cada novo elemento inserido no fim da lista e, sempre que solicitado, retiramos o elemento do incio da lista. Desta forma, precisamos de duas funes auxiliares de lista:
Estruturas de Dados PUC-Rio 11-5
para inserir no fim e para remover do incio. A funo para inserir no fim ainda no foi discutida, mas simples, uma vez que temos explicitamente armazenado o ponteiro para o ltimo elemento. Essa funo deve ter como valor de retorno o novo fim da lista. A funo para retirar do incio idntica funo usada na implementao de pilha.
/* funo auxiliar: insere no fim */ No* ins_fim (No* fim, float v) { No* p = (No*) malloc(sizeof(No)); p->info = v; p->prox = NULL; if (fim != NULL) /* verifica se lista no estava vazia */ fim->prox = p; return p; } /* funo auxiliar: retira do incio */ No* ret_ini (No* ini) { No* p = ini->prox; free(ini); return p; }
As funes que manipulam a fila fazem uso dessas funes de lista. Devemos salientar que a funo de insero deve atualizar ambos os ponteiros, ini e fim, quando da insero do primeiro elemento. Analogamente, a funo para retirar deve atualizar ambos se a fila tornar-se vazia aps a remoo do elemento:
void insere (Fila* f, float v) { f->fim = ins_fim(f->fim,v); if (f->ini==NULL) /* fila antes vazia? */ f->ini = f->fim; } float retira (Fila* f) { float v; if (vazia(f)) { printf("Fila vazia.\n"); exit(1); /* aborta programa */ } v = f->ini->info; f->ini = ret_ini(f->ini); if (f->ini == NULL) /* fila ficou vazia? */ f->fim = NULL; return v; }
11-6
Por fim, a funo que libera a fila deve antes liberar todos os elementos da lista.
void libera (Fila* f) { No* q = f->ini; while (q!=NULL) { No* t = q->prox; free(q); q = t; } free(f); }
Analogamente pilha, para testar o cdigo, pode ser til implementarmos uma funo que imprima os valores armazenados na fila. Os cdigos abaixo ilustram a implementao dessa funo nas duas verses de fila (vetor e lista). A ordem de impresso adotada do incio para o fim.
/* imprime: verso com vetor */ void imprime (Fila* f) { int i; for (i=f->ini; i!=f->fim; i=incr(i)) printf("%f\n",f->vet[i]); } /* imprime: verso com lista */ void imprime (Fila* f) { No* q; for (q=f->ini; q!=NULL; q=q->prox) printf("%f\n",q->info); }
A interface do tipo abstrato que representa uma fila dupla acrescenta novas funes para inserir e retirar elementos. Podemos enumerar as seguintes operaes: criar uma estrutura de fila dupla; inserir um elemento no incio; inserir um elemento no fim; retirar o elemento do incio; retirar o elemento do fim; verificar se a fila est vazia; liberar a fila. O arquivo fila2.h, que representa a interface do tipo, pode conter o seguinte cdigo:
typedef struct fila2 Fila2; Fila2* cria (void); void insere_ini (Fila2* f, float v); void insere_fim (Fila2* f, float v); float retira_ini (Fila2* f); float retira_fim (Fila2* f); int vazia (Fila2* f); void libera (Fila2* f);
A implementao dessa estrutura usando um vetor para armazenar os elementos no traz grandes dificuldades, pois o vetor permite acesso randmico aos elementos, e fica como exerccio. Exerccio: Implementar a estrutura de fila dupla com vetor. Obs: Note que o decremento circular no pode ser feito de maneira compacta como fizemos para incrementar. Devemos decrementar o ndice de uma unidade e testar se ficou negativo, atribuindo-lhe o valor N-1 em caso afirmativo.
11-8
O arranjo de memria para implementarmos a fila dupla com lista ilustrado na figura abaixo: ini fim
Info1
Info2
Info3
O n da lista duplamente encadeada para armazenar valores reais pode ser dado por:
struct no2 { float info; struct no2* ant; struct no2* prox; }; typedef struct no2 No2;
Interessa-nos discutir as funes para inserir e retirar elementos. As demais so praticamente idnticas s de fila simples. Podemos inserir um novo elemento em qualquer extremidade da fila. Portanto, precisamos de duas funes auxiliares de lista: para inserir no incio e para inserir no fim. Ambas as funes so simples e j foram exaustivamente discutidas para o caso da lista simples. No caso da lista duplamente encadeada, a diferena consiste em termos que atualizar tambm o encadeamento para o elemento anterior. Uma possvel implementao dessas funes mostrada a seguir. Essas funes retornam, respectivamente, o novo n inicial e final.
/* funo auxiliar: insere no incio */ No2* ins2_ini (No2* ini, float v) { No2* p = (No2*) malloc(sizeof(No2)); p->info = v; p->prox = ini; p->ant = NULL; if (ini != NULL) /* verifica se lista no estava vazia */ ini->ant = p; return p; } /* funo auxiliar: insere no fim */ No2* ins2_fim (No2* fim, float v) { No2* p = (No2*) malloc(sizeof(No2)); p->info = v; p->prox = NULL; p->ant = fim; if (fim != NULL) /* verifica se lista no estava vazia */ fim->prox = p; return p; }
Estruturas de Dados PUC-Rio 11-9
Uma possvel implementao das funes para remover o elemento do incio ou do fim mostrada a seguir. Essas funes tambm retornam, respectivamente, o novo n inicial e final.
/* funo auxiliar: retira do incio */ No2* ret2_ini (No2* ini) { No2* p = ini->prox; if (p != NULL) /* verifica se lista no ficou vazia */ p->ant = NULL; free(ini); return p; } /* funo auxiliar: retira do fim */ No2* ret2_fim (No2* fim) { No2* p = fim->ant; if (p != NULL) /* verifica se lista no ficou vazia */ p->prox = NULL; free(fim); return p; }
As funes que manipulam a fila fazem uso dessas funes de lista, atualizando os ponteiros ini e fim quando necessrio.
void insere_ini (Fila2* f, float v) { f->ini = ins2_ini(f->ini,v); if (f->fim==NULL) /* fila antes vazia? */ f->fim = f->ini; } void insere_fim (Fila2* f, float v) { f->fim = ins2_fim(f->fim,v); if (f->ini==NULL) /* fila antes vazia? */ f->ini = f->fim; } float retira_ini (Fila2* f) { float v; if (vazia(f)) { printf("Fila vazia.\n"); exit(1); /* aborta programa */ } v = f->ini->info; f->ini = ret2_ini(f->ini); if (f->ini == NULL) /* fila ficou vazia? */ f->fim = NULL; return v; } float retira_fim (Fila2* f) { float v; if (vazia(f)) { printf("Fila vazia.\n"); exit(1); /* aborta programa */ } v = f->fim->info; f->fim = ret2_fim(f->fim); if (f->fim == NULL) /* fila ficou vazia? */ f->ini = NULL; return v; }
11-10
13. rvores
W. Celes e J. L. Rangel
Nos captulos anteriores examinamos as estruturas de dados que podem ser chamadas de unidimensionais ou lineares, como vetores e listas. A importncia dessas estruturas inegvel, mas elas no so adequadas para representarmos dados que devem ser dispostos de maneira hierrquica. Por exemplo, os arquivos (documentos) que criamos num computador so armazenados dentro de uma estrutura hierrquica de diretrios (pastas). Existe um diretrio base dentro do qual podemos armazenar diversos subdiretrios e arquivos. Por sua vez, dentro dos sub-diretrios, podemos armazenar outros sub-diretrios e arquivos, e assim por diante, recursivamente. A Figura 13.1 mostra uma imagem de uma rvore de diretrio no Windows 2000.
Neste captulo, vamos introduzir rvores, que so estruturas de dados adequadas para a representao de hierarquias. A forma mais natural para definirmos uma estrutura de rvore usando recursividade. Uma rvore composta por um conjunto de ns. Existe um n r , denominado raiz, que contm zero ou mais sub-rvores, cujas razes so ligadas diretamente a r. Esses ns razes das sub-rvores so ditos filhos do n pai, r. Ns com filhos so comumente chamados de ns internos e ns que no tm filhos so chamados de folhas, ou ns externos. tradicional desenhar as rvores com a raiz para cima e folhas para baixo, ao contrrio do que seria de se esperar. A Figura 13.2 exemplifica a estrutura de uma rvore.
n raiz
...
Figura 13.2: Estrutura de rvore.
sub-rvores
Observamos que, por adotarmos essa forma de representao grfica, no representamos explicitamente a direo dos ponteiros, subentendendo que eles apontam sempre do pai para os filhos.
Estruturas de Dados PUC-Rio 12-1
O nmero de filhos permitido por n e as informaes armazenadas em cada n diferenciam os diversos tipos de rvores existentes. Neste captulo, estudaremos dois tipos de rvores. Primeiro, examinaremos as rvores binrias, onde cada n tem, no mximo, dois filhos. Depois examinaremos as chamadas rvores genricas, onde o nmero de filhos indefinido. Estruturas recursivas sero usadas como base para o estudo e a implementao das operaes com rvores.
* + 3 6 4 1
Numa rvore binria, cada n tem zero, um ou dois filhos. De maneira recursiva, podemos definir uma rvore binria como sendo: uma rvore vazia; ou um n raiz tendo duas sub-rvores, identificadas como a sub-rvore da direita (sad) e a sub-rvore da esquerda (sae). A Figura 13.4 ilustra a definio de rvore binria. Essa definio recursiva ser usada na construo de algoritmos, e na verificao (informal) da correo e do desempenho dos mesmos.
12-2
raiz
vazia
sae
sad
A Figura 13.5 a seguir ilustra uma estrutura de rvore binria. Os ns a, b, c, d, e, f formam uma rvore binria da seguinte maneira: a rvore composta do n a, da subrvore esquerda formada por b e d, e da sub-rvore direita formada por c, e e f. O n a representa a raiz da rvore e os ns b e c as razes das sub-rvores. Finalmente, os ns d, e e f so folhas da rvore.
a b d e c f
Para descrever rvores binrias, podemos usar a seguinte notao textual: a rvore vazia representada por <>, e rvores no vazias por <raiz sae sad>. Com essa notao, a rvore da Figura 13.5 representada por:
<a<b<><d<><>>><c<e<><>><f<><>>>>.
Pela definio, uma sub-rvore de uma rvore binria sempre especificada como sendo a sae ou a sad de uma rvore maior, e qualquer das duas sub-rvores pode ser vazia. Assim, as duas rvores da Figura 13.6 so distintas.
a b
a b
Isto tambm pode ser visto pelas representaes textuais das duas rvores, que so, respectivamente: <a <b<><>> <> > e <a <> <b<><>> >.
Estruturas de Dados PUC-Rio 12-3
Uma propriedade fundamental de todas as rvores que s existe um caminho da raiz para qualquer n. Com isto, podemos definir a altura de uma rvore como sendo o comprimento do caminho mais longo da raiz at uma das folhas. Por exemplo, a altura da rvore da Figura 13.5 2, e a altura das rvores da Figura 13.6 1. Assim, a altura de uma rvore com um nico n raiz zero e, por conseguinte, dizemos que a altura de uma rvore vazia negativa e vale -1. Exerccio: Mostrar que uma rvore binria de altura h tem, no mnimo, h+1 ns, e, no mximo, 2h+1 1. Representao em C Anlogo ao que fizemos para as demais estruturas de dados, podemos definir um tipo para representar uma rvore binria. Para simplificar a discusso, vamos considerar que a informao que queremos armazenar nos ns da rvore so valores de caracteres simples. Vamos inicialmente discutir como podemos representar uma estrutura de rvore binria em C. Que estrutura podemos usar para representar um n da rvore? Cada n deve armazenar trs informaes: a informao propriamente dita, no caso um caractere, e dois ponteiros para as sub-rvores, esquerda e direita. Ento a estrutura de C para representar o n da rvore pode ser dada por:
struct arv { char info; struct arv* esq; struct arv* dir; };
Da mesma forma que uma lista encadeada representada por um ponteiro para o primeiro n, a estrutura da rvore como um todo representada por um ponteiro para o n raiz. Como acontece com qualquer TAD (tipo abstrato de dados), as operaes que fazem sentido para uma rvore binria dependem essencialmente da forma de utilizao que se pretende fazer da rvore. Nesta seo, em vez de discutirmos a interface do tipo abstrato para depois mostrarmos sua implementao, vamos optar por discutir algumas operaes mostrando simultaneamente suas implementaes. Ao final da seo apresentaremos um arquivo que pode representar a interface do tipo. Nas funes que se seguem, consideraremos que existe o tipo Arv definido por:
typedef struct arv Arv;
Como veremos as funes que manipulam rvores so, em geral, implementadas de forma recursiva, usando a definio recursiva da estrutura. Vamos procurar identificar e descrever apenas operaes cuja utilidade seja a mais geral possvel. Uma operao que provavelmente dever ser includa em todos os casos a inicializao de uma rvore vazia. Como uma rvore representada pelo endereo do n raiz, uma rvore vazia tem que ser representada pelo valor NULL. Assim, a funo que inicializa uma rvore vazia pode ser simplesmente:
12-4
Para criar rvores no vazias, podemos ter uma operao que cria um n raiz dadas a informao e suas duas sub-rvores, esquerda e direita. Essa funo tem como valor de retorno o endereo do n raiz criado e pode ser dada por:
Arv* cria(char c, Arv* sae, Arv* sad){ Arv* p=(Arv*)malloc(sizeof(Arv)); p->info = c; p->esq = sae; p->dir = sad; return p; }
As duas funes inicializa e cria representam os dois casos da definio recursiva de rvore binria: uma rvore binria (Arv* a;) vazia (a = inicializa();) ou composta por uma raiz e duas sub-rvores (a = cria(c,sae,sad);). Assim, com posse dessas duas funes, podemos criar rvores mais complexas. Exemplo: Usando as operaes inicializa e cria, crie uma estrutura que represente a rvore da Figura 13.5. O exemplo da figura pode ser criada pela seguinte seqncia de atribuies.
Arv* */ Arv* */ Arv* */ Arv* */ Arv* */ Arv* */ a1= cria('d',inicializa(),inicializa()); a2= cria('b',inicializa(),a1); a3= cria('e',inicializa(),inicializa()); a4= cria('f',inicializa(),inicializa()); a5= cria('c',a3,a4); a = cria('a',a2,a5 ); /* sub-rvore com 'd' /* sub-rvore com 'b' /* sub-rvore com 'e' /* sub-rvore com 'f' /* sub-rvore com 'c' /* rvore com raiz 'a'
Alternativamente, a rvore poderia ser criada com uma nica atribuio, seguindo a sua estrutura, recursivamente:
Arv* a = cria('a', cria('b', inicializa(), cria('d', inicializa(), inicializa()) ), cria('c', cria('e', inicializa(), inicializa()), cria('f', inicializa(), inicializa()) ) );
Para tratar a rvore vazia de forma diferente das outras, importante ter uma operao que diz se uma rvore ou no vazia. Podemos ter:
12-5
Uma outra funo muito til consiste em exibir o contedo da rvore. Essa funo deve percorrer recursivamente a rvore, visitando todos os ns e imprimindo sua informao. A implementao dessa funo usa a definio recursiva da rvore. Vimos que uma rvore binria ou vazia ou composta pela raiz e por duas sub-rvores. Portanto, para imprimir a informao de todos os ns da rvore, devemos primeiro testar se a rvore vazia. Se no for, imprimimos a informao associada a raiz e chamamos (recursivamente) a funo para imprimir os ns das sub-rvores.
void imprime (Arv* a) { if (!vazia(a)){ printf("%c ", a->info); imprime(a->esq); imprime(a->dir); } }
Exerccio: (a) simule a chamada da funo imprime aplicada arvore ilustrada pela Figura 13.5 para verificar que o resultado da chamada a impresso de a b d c e f. (b) Repita a experincia executando um programa que crie e mostre a rvore, usando o seu compilador de C favorito. Exerccio: Modifique a implementao de imprime, de forma que a sada impressa reflita, alm do contedo de cada n, a estrutura da rvore, usando a notao introduzida anteriormente. Assim, a sada da funo seria: <a<b<><d<><>>><c<e<><>><f<><>>>>. Uma outra operao que pode ser acrescentada a operao para liberar a memria alocada pela estrutura da rvore. Novamente, usaremos uma implementao recursiva. Um cuidado essencial a ser tomado que as sub-rvores devem ser liberadas antes de se liberar o n raiz, para que o acesso s sub-rvores no seja perdido antes de sua remoo. Neste caso, vamos optar por fazer com que a funo tenha como valor de retorno a rvore atualizada, isto , uma rvore vazia, representada por NULL.
Arv* libera (Arv* a){ if (!vazia(a)){ libera(a->esq); libera(a->dir); free(a); } return NULL; }
Devemos notar que a definio de rvore, por ser recursiva, no faz distino entre rvores e sub-rvores. Assim, cria pode ser usada para acrescentar (enxertar) uma sub-rvore em um ramo de uma rvore, e libera pode ser usada para remover (podar) uma sub-rvore qualquer de uma rvore dada. Exemplo: Considerando a criao da rvore feita anteriormente:
Estruturas de Dados PUC-Rio 12-6
Arv* a = cria('a', cria('b', inicializa(), cria('d', inicializa(), inicializa()) ), cria('c', cria('e', inicializa(), inicializa()), cria('f', inicializa(), inicializa()) ) );
Deixamos como exerccio a verificao do resultado final dessas operaes. importante observar que, anlogo ao que fizemos para a lista, o cdigo cliente que chama a funo libera responsvel por atribuir o valor atualizado retornado pela funo, no caso uma rvore vazia. No exemplo acima, se no tivssemos feito a atribuio, o endereo armazenado em r->dir->esq seria o de uma rea de memria no mais em uso. Exerccio: Escreva uma funo que percorre uma rvore binria para determinar sua altura. O prottipo da funo pode ser dado por:
int altura(Arv* a);
Uma outra funo que podemos considerar percorre a rvore buscando a ocorrncia de um determinado caractere c em um de seus ns. Essa funo tem como retorno um valor booleano (um ou zero) indicando a ocorrncia ou no do caractere na rvore.
int busca (Arv* a, char c){ if (vazia(a)) return 0; /* rvore vazia: no encontrou */ else return a->info==c || busca(a->esq,c) || busca(a->dir,c); }
Note que esta forma de programar busca, em C, usando o operador lgico || (ou) faz com que a busca seja interrompida assim que o elemento encontrado. Isto acontece porque se c==a->info for verdadeiro, as duas outras expresses no chegam a ser avaliadas. Analogamente, se o caractere for encontrado na sub-rvore da esquerda, a busca no prossegue na sub-rvore da direita. Podemos dizer que a expresso:
return c==a->info || busca(a->esq,c) || busca(a->dir,c);
equivalente a:
12-7
Finalmente, considerando que as funes discutidas e implementadas acima formam a interface do tipo abstrato para representar uma rvore binria, um arquivo de interface arvbin.h pode ser dado por:
typedef struct arv Arv; Arv* Arv* int void Arv* int inicializa (void); cria (char c, Arv* e, Arv* d); vazia (Arv* a); imprime (Arv* a); libera (Arv* a); busca (Arv* a, char c);
Ordens de percurso em rvores binrias A programao da operao imprime, vista anteriormente, seguiu a ordem empregada na definio de rvore binria para decidir a ordem em que as trs aes seriam executadas: Entretanto, dependendo da aplicao em vista, esta ordem poderia no ser a prefervel, podendo ser utilizada uma ordem diferente desta, por exemplo:
imprime(a->esq); imprime(a->dir); printf("%c ", a->info); /* mostra sae */ /* mostra sad */ /* mostra raiz */
Muitas operaes em rvores binrias envolvem o percurso de todas as sub-rvores, executando alguma ao de tratamento em cada n, de forma que comum percorrer uma rvore em uma das seguintes ordens: pr-ordem: trata raiz, percorre sae, percorre sad; ordem simtrica: percorre sae, trata raiz, percorre sad; ps-ordem: percorre sae, percorre sad, trata raiz. Para funo para liberar a rvore, por exemplo, tivemos que adotar a ps-ordem:
libera(a->esq); libera(a->dir); free(a); /* libera sae */ /* libera sad */ /* libera raiz */
Na terceira parte do curso, quando tratarmos de rvores binrias de busca, apresentaremos um exemplo de aplicao de rvores binrias em que a ordem de percurso importante a ordem simtrica. Algumas outras ordens de percurso podem ser definidas, mas a maioria das aplicaes envolve uma dessas trs ordens, percorrendo a sae antes da sad. Exerccio: Implemente verses diferentes da funo imprime, percorrendo a rvore em ordem simtrica e em ps-ordem. Verifique o resultado da aplicao das duas funes na rvore da Figura 13.5.
Estruturas de Dados PUC-Rio 12-8
Nesse exemplo, podemos notar que o a rvore com raiz no n a tem 3 sub-rvores, ou, equivalentemente, o n a tem 3 filhos. Os ns b e g tem dois filhos cada um; os ns c e i tem um filho cada, e os ns d, e, h e j so folhas, e tem zero filhos.
12-9
De forma semelhante ao que foi feito no caso das rvores binrias, podemos representar essas rvores atravs de notao textual, seguindo o padro: <raiz sa1 sa2 ... san>. Com esta notao, a rvore da Figura 13.7 seria representada por:
a = <a <b <c <d>> <e>> <f> <g <h> <i <j>>>>
Podemos verificar que a representa a rvore do exemplo seguindo a seqncia de definio a partir das folhas:
a1 = <d> a2 = <c a1> = <c a3 = <e> a4 = <b a2 a3> = a5 = <f> a6 = <h> a7 = <j> a8 = <i a7> = <i a9 = <g a6 a8> = a = <a a4 a5 a9> <d>> <b <c <d>> <e>>
<j>> <g <h> <i <j>>> = <a <b <c <d>> <e>> <f> <g <h> <i <j>>>>
Representao em C Dependendo da aplicao, podemos usar vrias estruturas para representar rvores, levando em considerao o nmero de filhos que cada n pode apresentar. Se soubermos, por exemplo, que numa aplicao o nmero mximo de filhos que um n pode apresentar 3, podemos montar uma estrutura com 3 campos para apontadores para os ns filhos, digamos, f1 , f2 e f3 . Os campos no utilizados podem ser preenchidos com o valor nulo NULL, sendo sempre utilizados os campos em ordem. Assim, se o n n tem 2 filhos, os campos f1 e f2 seriam utilizados, nessa ordem, para apontar para eles, ficando f3 vazio. Prevendo um nmero mximo de filhos igual a 3, e considerando a implementao de rvores para armazenar valores de caracteres simples, a declarao do tipo que representa o n da rvore poderia ser:
struct arv3 { char val; struct no *f1, *f2, *f3; };
A Figura 13.8 indica a representao da rvore da Figura 13.7 com esta organizao. Como se pode ver no exemplo, em cada um dos ns que tem menos de trs filhos, o espao correspondente aos filhos inexistentes desperdiado. Alm disso, se no existe um limite superior no nmero de filhos, esta tcnica pode no ser aplicvel. O mesmo acontece se existe um limite no nmero de ns, mas esse limite ser raramente alcanado, pois estaramos tendo um grande desperdcio de espao de memria com os campos no utilizados.
12-10
Uma soluo que leva a um aproveitamento melhor do espao utiliza uma lista de filhos: um n aponta apenas para seu primeiro (prim) filho, e cada um de seus filhos, exceto o ltimo, aponta para o prximo (prox) irmo. A declarao de um n pode ser:
struct arvgen { char info; struct arvgen *prim; struct arvgen *prox; };
A Figura 13.9 mostra o mesmo exemplo representado de acordo com esta estrutura. Uma das vantagens dessa representao que podemos percorrer os filhos de um n de forma sistemtica, de maneira anloga ao que fizemos para percorrer os ns de uma lista simples.
12-11
Com o uso dessa representao, a generalizao da rvore apenas conceitual, pois, concretamente, a rvore foi transformada em uma rvore binria, com filhos esquerdos apontados por prim e direitos apontados por prox . Naturalmente, continuaremos a fazer referncia aos ns nos termos da definio original. Por exemplo, os ns b, f e g continuaro a ser considerados filhos do n a, como indicado na Figura 13.7, mesmo que a representao usada na Figura 13.9 os coloque a distncias variveis do n pai. Tipo abstrato de dado Para exemplificar a implementao de funes que manipulam uma rvore genrica, vamos considerar a criao de um tipo abstrato de dados para representar rvores onde a informao associada a cada n um caractere simples. Nessa implementao, vamos optar por armazenar os filhos de um n numa lista encadeada. Podemos definir o seguinte conjunto de operaes: cria: cria um n folha, dada a informao a ser armazenada; insere: insere uma nova sub-rvore como filha de um dado n; imprime: percorre todos os ns e imprime suas informaes; busca: verifica a ocorrncia de um determinado valor num dos ns da rvore; libera: libera toda a memria alocada pela rvore. A interface do tipo pode ento ser definida no arquivo arvgen.h dado por:
typedef struct arvgen ArvGen; ArvGen* void void int void cria (char c); insere (ArvGen* a, ArvGen* sa); imprime (ArvGen* a); busca (ArvGen* a, char c); libera (ArvGen* a);
12-12
A estrutura arvgen , que representa o n da rvore, definida conforme mostrado anteriormente. A funo para criar uma folha deve alocar o n e inicializar seus campos, atribuindo NULL para os campos prim e prox, pois trata-se de um n folha.
ArvGen* cria { ArvGen *a a->info = a->prim = a->prox = return a; } (char c) =(ArvGen *) malloc(sizeof(ArvGen)); c; NULL; NULL;
A funo que insere uma nova sub-rvore como filha de um dado n muito simples. Como no vamos atribuir nenhum significado especial para a posio de um n filho, a operao de insero pode inserir a sub-rvore em qualquer posio. Neste caso, vamos optar por inserir sempre no incio da lista que, como j vimos, a maneira mais simples de inserir um novo elemento numa lista encadeada.
void insere (ArvGen* a, ArvGen* sa) { sa->prox = a->prim; a->prim = sa; }
Com essas duas funes, podemos construir a rvore do exemplo da Figura 13.7 com o seguinte fragmento de cdigo:
/* cria ns como folhas */ ArvGen* a = cria('a'); ArvGen* b = cria('b'); ArvGen* c = cria('c'); ArvGen* d = cria('d'); ArvGen* e = cria('e'); ArvGen* f = cria('f'); ArvGen* g = cria('g'); ArvGen* h = cria('h'); ArvGen* i = cria('i'); ArvGen* j = cria('j'); /* monta a hierarquia */ insere(c,d); insere(b,e); insere(b,c); insere(i,j); insere(g,i); insere(g,h); insere(a,g); insere(a,f); insere(a,b);
Para imprimir as informaes associadas aos ns da rvore, temos duas opes para percorrer a rvore: pr-ordem, primeiro a raiz e depois as sub-rvores, ou ps-ordem, primeiro as sub-rvores e depois a raiz. Note que neste caso no faz sentido a ordem simtrica, uma vez que o nmero de sub-rvores varivel. Para essa funo, vamos optar por imprimir o contedo dos ns em pr-ordem:
void imprime (ArvGen* a) { ArvGen* p; printf("%c\n",a->info); for (p=a->prim; p!=NULL; p=p->prox) imprime(p); }
Estruturas de Dados PUC-Rio 12-13
A operao para buscar a ocorrncia de uma dada informao na rvore exemplificada abaixo:
int busca (ArvGen* a, char c) { ArvGen* p; if (a->info==c) return 1; else { for (p=a->prim; p!=NULL; p=p->prox) { if (busca(p,c)) return 1; } } return 0; }
A ltima operao apresentada a que libera a memria alocada pela rvore. O nico cuidado que precisamos tomar na programao dessa funo a de liberar as subrvores antes de liberar o espao associado a um n (isto , usar ps-ordem).
void libera (ArvGen* a) { ArvGen* p = a->prim; while (p!=NULL) { ArvGen* t = p->prox; libera(p); p = t; } free(a); }
para criar dinamicamente uma cpia da rvore. Exerccio: Escreva uma funo com o prottipo
int igual(ArvGen*a, ArvGen*b);
12-14
14. Arquivos
W. Celes e J. L. Rangel
Neste captulo, apresentaremos alguns conceitos bsicos sobre arquivos, e alguns detalhes da forma de tratamento de arquivos em disco na linguagem C. A finalidade desta apresentao discutir variadas formas para salvar (e recuperar) informaes em arquivos. Com isto, ser possvel implementar funes para salvar (e recuperar) as informaes armazenadas nas estruturas de dados que temos discutido. Um arquivo em disco representa um elemento de informao do dispositivo de memria secundria. A memria secundria (disco) difere da memria principal em diversos aspectos. As duas diferenas mais relevantes so: eficincia e persistncia. Enquanto o acesso a dados armazenados na memria principal muito eficiente do ponto de vista de desempenho computacional, o acesso a informaes armazenadas em disco , em geral, extremamente ineficiente. Para contornar essa situao, os sistemas operacionais trabalham com buffers, que representam reas da memria principal usadas como meio de transferncia das informaes de/para o disco. Normalmente, trechos maiores (alguns kbytes) so lidos e armazenados no buffer a cada acesso ao dispositivo. Desta forma, uma subseqente leitura de dados do arquivo, por exemplo, possivelmente no precisar acessar o disco, pois o dado requisitado pode j se encontrar no buffer. Os detalhes de como estes acessos se realizam dependem das caractersticas do dispositivo e do sistema operacional empregado. A outra grande diferena entre memria principal e secundria (disco) consiste no fato das informaes em disco serem persistentes, e em geral so lidas por programas e pessoas diferentes dos que as escreveram, o que faz com que seja mais prtico atribuir nomes aos elementos de informao armazenados do disco (em vez de endereos), falando assim em arquivos e diretrios (pastas). Cada arquivo identificado por seu nome e pelo diretrio onde encontra-se armazenado numa determinada unidade de disco. Os nomes dos arquivos so, em geral, compostos pelo nome em si, seguido de uma extenso. A extenso pode ser usada para identificar a natureza da informao armazenada no arquivo ou para identificar o programa que gerou (e capaz de interpretar) o arquivo. Assim, a extenso .c usada para identificar arquivos que tm cdigos fontes da linguagem C e a extenso .doc , no Windows, usada para identificar arquivos gerados pelo editor Word da Microsoft. Um arquivo pode ser visto de duas maneiras, na maioria dos sistemas operacionais: em modo texto, como um texto composto de uma seqncia de caracteres, ou em modo binrio, como uma seqncia de bytes (nmeros binrios). Podemos optar por salvar (e recuperar) informaes em disco usando um dos dois modos, texto ou binrio. Uma vantagem do arquivo texto que pode ser lido por uma pessoa e editado com editores de textos convencionais. Em contrapartida, com o uso de um arquivo binrio possvel salvar (e recuperar) grandes quantidades de informao de forma bastante eficiente. O sistema operacional pode tratar arquivos texto de maneira diferente da utilizada para tratar arquivos binrios. Em casos especiais, pode ser interessante tratar arquivos de um tipo como se fossem do outro, tomando os cuidados apropriados. Para minimizar a dificuldade com que arquivos so manipulados, os sistemas operacionais oferecem um conjunto de servios para ler e escrever informaes do
Estruturas de Dados PUC-Rio 13-1
disco. A linguagem C disponibiliza esses servios para o programador atravs de um conjunto de funes. Os principais servios que nos interessam so: abertura de arquivos: o sistema operacional encontra o arquivo com o nome dado e prepara o buffer na memria. leitura do arquivo: o sistema operacional recupera o trecho solicitado do arquivo. Como o buffer contm parte da informao do arquivo, parte ou toda a informao solicitada pode vir do buffer. escrita no arquivo: o sistema operacional acrescenta ou altera o contedo do arquivo. A alterao no contedo do arquivo feita inicialmente no buffer para depois ser transferida para o disco. fechamento de arquivo: toda a informao constante do buffer atualizada no disco e a rea do buffer utilizada na memria liberada. Uma das informaes que mantida pelo sistema operacional um ponteiro de arquivo (file pointer), que indica a posio de trabalho no arquivo. Para ler um arquivo, este apontador percorre o arquivo, do incio at o fim, conforme os dados vo sendo recuperados (lidos) para a memria. Para escrever, normalmente, os dados so acrescentados quando o apontador se encontra no fim do arquivo. Nas sees subseqentes, vamos apresentar as funes mais utilizadas em C para acessar arquivos e vamos discutir diferentes estratgias para tratar arquivos. Todas as funes da biblioteca padro de C que manipulam arquivos encontram-se na biblioteca de entrada e sada, com interface em stdio.h.
FILE um tipo definido pela biblioteca padro que representa uma abstrao do
arquivo. Quando abrimos um arquivo, a funo tem como valor de retorno um ponteiro para o tipo FILE , e todas as operaes subseqentes nesse arquivo recebero este endereo como parmetro de entrada. Se o arquivo no puder ser aberto, a funo tem como retorno o valor NULL. Devemos passar o nome do arquivo a ser aberto. O nome do arquivo pode ser relativo, e o sistema procura o arquivo a partir do diretrio corrente (diretrio de trabalho do programa), ou pode ser absoluto, onde especificamos o nome completo do arquivo, incluindo os diretrios, desde o diretrio raiz. Existem diferentes modos de abertura de um arquivo. Podemos abrir um arquivo para leitura ou para escrita, e devemos especificar se o arquivo ser aberto em modo texto ou em modo binrio. O parmetro modo da funo fopen uma cadeia de caracteres onde espera-se a ocorrncia de caracteres que identificam o modo de abertura. Os caracteres interpretados no modo so: r read-only Indica modo apenas para leitura, no pode ser alterado. w write Indica modo para escrita. a append Indica modo para escrita ao final do existente. t text Indica modo texto.
Estruturas de Dados PUC-Rio 13-2
binary
Se o arquivo j existe e solicitamos a sua abertura para escrita com modo w, o arquivo apagado e um novo, inicialmente vazio, criado. Quando solicitamos com modo a, o mesmo preservado e novos contedos podem ser escritos no seu fim. Com ambos os modos, se o arquivo no existe, um novo criado. Os modos b e t podem ser combinados com os demais. Maiores detalhes e outros modos de abertura de arquivos podem ser encontrados nos manuais da linguagem C. Em geral, quando abrimos um arquivo, testamos o sucesso da abertura antes de qualquer outra operao, por exemplo:
... FILE* fp; fp = fopen("entrada.txt","rt"); if (fp == NULL) { printf("Erro na abertura do arquivo!\n"); exit(1); } ...
Aps ler/escrever as informaes de um arquivo, devemos fech-lo. Para fechar um arquivo, devemos usar a funo fclose, que espera como parmetro o ponteiro do arquivo que se deseja fechar. O prottipo da funo :
int fclose (FILE* fp);
O valor de retorno dessa funo zero, se o arquivo for fechado com sucesso, ou a constante EOF (definida pela biblioteca), que indica a ocorrncia de um erro.
Conforme pode ser observado, o primeiro parmetro deve ser o ponteiro do arquivo do qual os dados sero lidos. Os demais parmetros so os j discutidos para a funo scanf: o formato e a lista de endereos de variveis que armazenaro os valores lidos. Como a funo scanf, a funo fscanf tambm tem como valor de retorno o nmero de dados lidos com sucesso.
Estruturas de Dados PUC-Rio 13-3
Uma outra funo de leitura muito usada em modo texto a funo fgetc que, dado o ponteiro do arquivo, captura o prximo caractere do arquivo. O prottipo dessa funo :
int fgetc (FILE* fp);
Apesar do tipo do valor de retorno ser int, o valor retornado o caractere lido. Se o fim do arquivo for alcanado, a constante EOF (end of file) retornada. Uma outra funo muito utilizada para ler linhas de um arquivo a funo fgets. Essa funo recebe como parmetros trs valores: a cadeia de caracteres que armazenar o contedo lido do arquivo, o nmero mximo de caracteres que deve ser lido e o ponteiro do arquivo. O prottipo da funo :
char* fgets (char* s, int n, FILE* fp);
A funo l do arquivo uma seqncia de caracteres, at que um caractere '\n' seja encontrado ou que o mximo de caracteres especificado seja alcanado. A especificao de um nmero mximo de caracteres importante para evitarmos que se invada memria quando a linha do arquivo for maior do que supnhamos. Assim, se dimensionarmos nossa cadeia de caracteres, que receber o contedo da linha lida, com 121 caracteres, passaremos esse valor para a funo, que ler no mximo 120 caracteres, pois o ltimo ser ocupado pelo finalizador de string o caractere '\0'. O valor de retorno dessa funo o ponteiro da prpria cadeia de caracteres passada como parmetro ou NULL no caso de ocorrer erro de leitura (por exemplo, quando alcanar o final do arquivo). Funes para escrever dados Dentre as funes que existem para escrever (salvar) dados em um arquivo, vamos considerar as duas mais freqentemente utilizadas: fprintf e fputc , que so anlogas, mas para escrita, s funes que vimos para leitura. A funo fprintf anloga a funo printf que temos usado para imprimir dados na sada padro em geral, o monitor. A diferena consiste na presena do parmetro que indica o arquivo para o qual o dado ser salvo. O valor de retorno dessa funo representa o nmero de bytes escritos no arquivo. O prottipo da funo dado por:
int fprintf(FILE* fp, char* formato, ...);
O valor de retorno dessa funo o prprio caractere escrito, ou EOF se ocorrer um erro na escrita.
13-4
a forma de tratar as informaes dependem da aplicao. A seguir, apresentaremos trs formas de representarmos e acessarmos dados armazenados em arquivos: caractere a caractere, linha a linha, e usando palavras chaves. Acesso caractere a caractere Para exemplificar o acesso caractere a caractere, vamos discutir duas aplicaes simples. Inicialmente, vamos considerar o desenvolvimento de um programa que conta as linhas de um determinado arquivo (para simplificar, vamos supor um arquivo fixo, com o nome entrada.txt). Para calcular o nmero de linhas do arquivo, podemos ler, caractere a caractere, todo o contedo do arquivo, contando o nmero de ocorrncias do caractere que indica mudana de linha, isto , o nmero de ocorrncias do caractere '\n'.
/* Conta nmero de linhas de um arquivo */ #include <stdio.h> int main (void) { int c; int nlinhas = 0; FILE *fp;
/* abre arquivo para leitura */ fp = fopen("entrada.txt","rt"); if (fp==NULL) { printf("No foi possivel abrir arquivo.\n"); return 1; } /* l caractere a caractere */ while ((c = fgetc(fp)) != EOF) { if (c == '\n') nlinhas++; } /* fecha arquivo */ fclose(fp); /* exibe resultado na tela */ printf("Numero de linhas = %d\n", nlinhas); return 0; }
Como segundo exemplo, vamos considerar o desenvolvimento de um programa que l o contedo do arquivo e cria um arquivo com o mesmo contedo, mas com todas as letras minsculas convertidas para maisculas. Os nomes dos arquivos sero fornecidos, via teclado, pelo usurio. Uma possvel implementao desse programa mostrada a seguir:
13-5
/* Converte arquivo para maisculas */ #include <stdio.h> #include <ctype.h> /* funo toupper */
int main (void) { int c; char entrada[121]; char saida[121]; FILE* e; FILE* s;
/* /* /* /*
nome do arquivo de entrada */ nome do arquivo de sada */ do arquivo de entrada */ do arquivo de sada */
/* pede ao usurio os nomes dos arquivos */ printf("Digite o nome do arquivo de entrada: "); scanf("%120s",entrada); printf("Digite o nome do arquivo de saida: "); scanf("%120s",saida); /* abre arquivos para leitura e para escrita */ e = fopen(entrada,"rt"); if (e == NULL) { printf("No foi possvel abrir arquivo de entrada.\n"); return 1; } s = fopen(saida,"wt"); if (s == NULL) { printf("No foi possvel abrir arquivo de saida.\n"); fclose(e); return 1; } /* l da entrada e escreve na sada */ while ((c = fgetc(e)) != EOF) fputc(toupper(c),s); /* fecha arquivos */ fclose(e); fclose(s); return 0; }
Acesso linha a linha Em diversas aplicaes, mais adequado tratar o contedo do arquivo linha a linha. Um caso simples que podemos mostrar consiste em procurar a ocorrncia de uma sub-cadeia de caracteres dentro de um arquivo (anlogo a o que feito pelo utilitrio grep dos sistemas Unix). Se a sub-cadeia for encontrada, apresentamos como sada o nmero da linha da primeira ocorrncia. Para implementar esse programa, vamos utilizar a funo strstr , que procura a ocorrncia de uma sub-cadeia numa cadeia de caracteres maior. A funo retorna o endereo da primeira ocorrncia ou NULL , se a sub-cadeia no for encontrada. O prottipo dessa funo :
char* strstr (char* s, char* sub);
13-6
A nossa implementao consistir em ler, linha a linha, o contedo do arquivo, contanto o nmero da linha. Para cada linha, verificamos se a ocorrncia da sub-cadeia, interrompendo a leitura em caso afirmativo.
/* Procura ocorrncia de sub-cadeia no arquivo */ #include <stdio.h> #include <string.h> /* funo strstr */
int main (void) { int n = 0; int achou = 0; char entrada[121]; char subcadeia[121]; char linha[121]; FILE* fp;
/* /* /* /* /* /*
nmero da linha corrente */ indica se achou sub-cadeia */ armazena nome do arquivo de entrada */ armazena sub-cadeia */ armazena cada linha do arquivo */ ponteiro do arquivo de entrada */
/* pede ao usurio o nome do arquivo e a sub-cadeia */ printf("Digite o nome do arquivo de entrada: "); scanf("%120s",entrada); printf("Digite a sub-cadeia: "); scanf("%120s",subcadeia); /* abre arquivos para leitura */ fp = fopen(entrada,"rt"); if (fp == NULL) { printf("No foi possvel abrir arquivo de entrada.\n"); return 1; } /* l linha a linha */ while (fgets(linha,121,fp) != NULL) { n++; if (strstr(linha,subcadeia) != NULL) { achou = 1; break; } } /* fecha arquivo */ fclose(fp); /* exibe sada */ if (achou) printf("Achou na linha %d.\n", n); else printf("Nao achou."); return 0; }
Como segundo exemplo de arquivos manipulados linha a linha, podemos citar o caso em que salvamos os dados com formatao por linha. Para exemplificar, vamos considerar que queremos salvar as informaes da lista de figuras geomtricas que discutimos na seo 9.3. A lista continha retngulos, tringulos e crculos. Para salvar essas informaes num arquivo, temos que escolher um formato apropriado, que nos permita posteriormente recuperar a informao salva. Para exemplificar um formato vlido, vamos adotar uma formatao por linha: em cada linha salvamos um caractere que indica o tipo da figura (r, t ou c), seguido dos parmetros que definem a figura, base e altura para os retngulos e tringulos ou raio para os crculos. Para
Estruturas de Dados PUC-Rio 13-7
enriquecer o formato, podemos considerar que as linhas iniciadas com o caractere # representam comentrios e devem ser desconsideradas na leitura. Por fim, linhas em branco so permitidas e desprezadas. Um exemplo do contedo de um arquivo com esse formato apresentado na Figura 14.1 (note a presena de linhas em branco e linhas que so comentrios):
# Lista de figuras geometricas r c # t 2.0 1.2 5.8 t 1.23 12 4 1.02
Para recuperarmos as informaes contidas num arquivo com esse formato, podemos ler do arquivo cada uma das linhas e depois ler os dados contidos na linha. Para tanto, precisamos introduzir uma funo adicional muito til. Trata-se da funo que permite ler dados de uma cadeia de caracteres. A funo sscanf similar s funes scanf e fscanf, mas captura os valores armazenados numa string. O prottipo dessa funo :
int sscanf (char* s, char* formato, ...);
A primeira cadeia de caracteres passada como parmetro representa a string da qual os dados sero lidos. Com essa funo, possvel ler uma linha de um arquivo e depois ler as informaes contidas na linha. (Analogamente, existe a funo sprintf que permite escrever dados formatados numa string.) Faremos a interpretao do arquivo da seguinte forma: para cada linha lida do arquivo, tentaremos ler do contedo da linha um caractere (desprezando eventuais caracteres brancos iniciais) seguido de dois nmeros reais. Se nenhum dado for lido com sucesso, significa que temos uma linha vazia e devemos desprez-la. Se pelo menos um dado (no caso, o caractere) for lido com sucesso, podemos interpretar o tipo da figura geomtrica armazenada na linha, ou detectar a ocorrncia de um comentrio. Se for um retngulo ou um tringulo, os dois valores reais tambm devero ter sido lidos com sucesso. Se for um crculo, apenas um valor real dever ter sido lido com sucesso. O fragmento de cdigo abaixo ilustra essa implementao. Supe-se que fp representa um ponteiro para um arquivo com formato vlido aberto para leitura, em modo texto.
char c; float v1, v2; FILE* fp; char linha[121]; ... while (fgets(linha,121,fp)) { int n = sscanf(linha," %c %f %f",&c,&v1,&v2); if (n>0) { switch(c) { case '#': /* desprezar linha de comentrio */ break; case 'r': if (n!=3) { /* tratar erro de formato do arquivo */
13-8
... } else { /* interpretar ... } break; case 't': if (n!=3) { /* tratar erro ... } else { /* interpretar ... } break; case 'c': if (n!=2) { /* tratar erro ... } else { /* interpretar ... } break; default: /* tratar erro de ... break; } } } ...
de formato do arquivo */
de formato do arquivo */
crculo: raio = v1 */
formato do arquivo */
A rigor, para o formato descrito, no precisvamos fazer a interpretao do arquivo linha a linha. O arquivo poderia ter sido interpretado capturando-se inicialmente um caractere que ento indicaria qual a prxima informao a ser lida. No entanto, em algumas situaes a interpretao linha a linha ilustrada acima a nica forma possvel. Para exemplificar, vamos considerar um arquivo que representa um conjunto de pontos no espao 3D. Esses pontos podem ser dados pelas suas trs coordenadas x, y e z. Um formato bastante flexvel para esse arquivo considera que cada ponto dado em uma linha e permite a omisso da terceira coordenada, se essa for igual a zero. Dessa forma, o formato atende tambm a descrio de pontos no espao 2D. Um exemplo desse formato ilustrado abaixo:
2.3 1.2 7.4 ... 4.5 10.4 1.3 6.0 9.6
Para interpretar esse formato, devemos ler cada uma das linhas e tentar ler trs valores reais de cada linha (aceitando o caso de apenas dois valores serem lidos com sucesso). Exerccio: Faa um programa que interprete o formato de pontos 3D descrito acima, armazenando-os num vetor.
13-9
Acesso via palavras chaves Quando os objetos num arquivo tm descries de tamanhos variados, comum adotarmos uma formatao com o uso de palavras chaves. Cada objeto precedido por uma palavra chave que o identifica. A interpretao desse tipo de arquivo pode ser feita lendo-se as palavras chaves e interpretando a descrio do objeto correspondente. Para ilustrar, vamos considerar que, alm de retngulos, tringulos e crculos, tambm temos polgonos quaisquer no nosso conjunto de figuras geomtricas. Cada polgono pode ser descrito pelo nmero de vrtices que o compe, seguido das respectivas coordenadas desses vrtices. A Figura 14.2 ilustra esse formato.
RETANGULO b h TRIANGULO b h CIRCULO r POLIGONO n x 1 y1 x 2 y2 x n yn
O fragmento de cdigo a seguir ilustra a interpretao desse formato, onde fp representa o ponteiro para o arquivo aberto para leitura.
... FILE* fp; char palavra[121]; ... while (fscanf(fp,"%120s",palavra) == 1) { if (strcmp(palavra,"RETANGULO")==0) { /* interpreta retngulo */ } else if (strcmp(palavra,"TRIANGULO")==0) { /* interpreta tringulo */ } else if (strcmp(palavra,"CIRCULO")==0) { /* interpreta crculo */ } else if (strcmp(palavra,"POLIGONO")==0) { /* interpreta polgono */ } else { /* trata erro de formato */ } }
13-10
O primeiro parmetro dessa funo representa o endereo de memria cujo contedo deseja-se salvar em arquivo. O parmetro tam indica o tamanho, em bytes, de cada elemento e o parmetro nelem indica o nmero de elementos. Por fim, passa-se o ponteiro do arquivo binrio para o qual o contedo da memria ser copiado. A funo para ler (recuperar) dados de arquivos binrios anloga, sendo que agora o contedo do disco copiado para o endereo de memria passado como parmetro. O prottipo da funo pode ser dado por:
int fread (void* p, int tam, int nelem, FILE* fp);
Para exemplificar a utilizao dessas funes, vamos considerar que uma aplicao tem um conjunto de pontos armazenados num vetor. O tipo que define o ponto pode ser:
struct ponto { float x, y, z; }; typedef struct ponto Ponto;
Uma funo para salvar o contedo de um vetor de pontos pode receber como parmetros o nome do arquivo, o nmero de pontos no vetor, e o ponteiro para o vetor. Uma possvel implementao dessa funo ilustrada abaixo:
void salva (char* arquivo, int n, Ponto* vet) { FILE* fp = fopen(arquivo,"wb"); if (fp==NULL) { printf("Erro na abertura do arquivo.\n"); exit(1); } fwrite(vet,sizeof(Ponto),n,fp); fclose(fp); }
A rigor, os tipos int so substitudos pelo tipo size_t , definido pela biblioteca padro, sendo, em geral, sinnimo para um inteiro sem sinal (unsigned int).
Estruturas de Dados PUC-Rio 13-11
13-12
15. Ordenao
W. Celes e J. L. Rangel
Em diversas aplicaes, os dados devem ser armazenados obedecendo uma determinada ordem. Alguns algoritmos podem explorar a ordenao dos dados para operar de maneira mais eficiente, do ponto de vista de desempenho computacional. Para obtermos os dados ordenados, temos basicamente duas alternativas: ou inserimos os elementos na estrutura de dados respeitando a ordenao (dizemos que a ordenao garantida por construo), ou, a partir de um conjunto de dados j criado, aplicamos um algoritmo para ordenar seus elementos. Neste captulo, vamos discutir dois algoritmos de ordenao que podem ser empregados em aplicaes computacionais. Devido ao seu uso muito freqente, importante ter disposio algoritmos de ordenao (sorting) eficientes tanto em termos de tempo (devem ser rpidos) como em termos de espao (devem ocupar pouca memria durante a execuo). Vamos descrever os algoritmos de ordenao considerando o seguinte cenrio: a entrada um vetor cujos elementos precisam ser ordenados; a sada o mesmo vetor com seus elementos na ordem especificada; o espao que pode ser utilizado apenas o espao do prprio vetor. Portanto, vamos discutir ordenao de vetores. Como veremos, os algoritmos de ordenao podem ser aplicados a qualquer informao, desde que exista uma ordem definida entre os elementos. Podemos, por exemplo, ordenar um vetor de valores inteiros, adotando uma ordem crescente ou decrescente. Podemos tambm aplicar algoritmos de ordenao em vetores que guardam informaes mais complexas, por exemplo um vetor que guarda os dados relativos a alunos de uma turma, com nome, nmero de matrcula, etc. Nesse caso, a ordem entre os elementos tem que ser definida usando uma das informaes do aluno como chave da ordenao: alunos ordenados pelo nome, alunos ordenados pelo nmero de matrcula, etc. Nos casos em que a informao complexa, raramente se encontra toda a informao relevante sobre os elementos do vetor no prprio vetor; em vez disso, cada componente do vetor pode conter apenas um ponteiro para a informao propriamente dita, que pode ficar em outra posio na memria. Assim, a ordenao pode ser feita sem necessidade de mover grandes quantidades de informao, para re-arrumar as componentes do vetor na sua ordem correta. Para trocar a ordem entre dois elementos, apenas os ponteiros so trocados. Em muitos casos, devido ao grande volume, a informao pode ficar em um arquivo de disco, e o elemento do vetor ser apenas uma referncia para a posio da informao nesse arquivo. Neste captulo, examinaremos os algoritmos de ordenao conhecidos como ordenao bolha (bubble sort) e ordenao rpida (quick sort), ou, mais precisamente, verses simplificadas desses algoritmos.
h uma inverso e esses dois elementos so trocados de posio, ficando em ordem correta. Assim, o primeiro elemento comparado com o segundo. Se uma inverso for encontrada, a troca feita. Em seguida, independente se houve ou no troca aps a primeira comparao, o segundo elemento comparado com o terceiro, e, caso uma inverso seja encontrada, a troca feita. O processo continua at que o penltimo elemento seja comparado com o ltimo. Com este processo, garante-se que o elemento de maior valor do vetor ser levado para a ltima posio. A ordenao continua, posicionando o segundo maior elemento, o terceiro, etc., at que todo o vetor esteja ordenado. Para exemplificar, vamos considerar que os elementos do vetor que queremos ordenar so valores inteiros. Assim, consideremos a ordenao do seguinte vetor:
25 48 37 12 57 86 33 92
Neste ponto, o segundo maior elemento, 86, j est na sua posio final.
25 12 12 12 12 12 12 25 25 25 25 25 37 37 37 37 37 37 48 48 48 48 33 33 33 33 33 33 48 48 57 57 57 57 57 57 86 86 86 86 86 86 92 92 92 92 92 92 25x12 troca 25x37 37x48 48x33 troca 48x57 final da terceira passada
14-2
A parte sabidamente j ordenada do vetor est sublinhada. Na realidade, aps a troca de 37x33 , o vetor se encontra totalmente ordenado, mas esse fato no levado em considerao por esta verso do algoritmo. Uma funo que implementa esse algoritmo apresentada a seguir. A funo recebe como parmetros o nmero de elementos e o ponteiro do primeiro elemento do vetor que se deseja ordenar. Vamos considerar o ordenao de um vetor de valores inteiros.
/* Ordenao bolha */ void bolha (int n, int* v) { int i,j; for (i=n-1; i>=1; i--) for (j=0; j<i; j++) if (v[j]>v[j+1]) { /* troca */ int temp = v[j]; v[j] = v[j+1]; v[j+1] = temp; } }
Uma funo cliente para testar esse algoritmo pode ser dada por:
/* Testa algoritmo de ordenao bolha */ #include <stdio.h> int main (void) { int i; int v[8] = {25,48,37,12,57,86,33,92}; bolha(8,v); printf("Vetor ordenado: "); for (i=0; i<8; i++) printf("%d ",v[i]); printf("\n); return 0; }
Para evitar que o processo continue mesmo depois de o vetor estar ordenado, podemos interromper o processo quando houver uma passagem inteira sem trocas, usando uma variante do algoritmo apresentado acima:
14-3
/* Ordenao bolha (2a. verso) */ void bolha2 (int n, int* v) { int i, j; for (i=n-1; i>0; i--) { int troca = 0; for (j=0; j<i; j++) if (v[j]>v[j+1]) { /* troca */ int temp = v[j]; v[j] = v[j+1]; v[j+1] = temp; troca = 1; } if (troca == 0) /* nao houve troca */ return; } }
A varivel troca guarda o valor 0 (falso) quando uma passada do vetor (no for interno) se faz sem nenhuma troca. O esforo computacional despendido pela ordenao de um vetor por este procedimento fcil de se determinar, pelo nmero de comparaes, que serve tambm para estimar o nmero mximo de trocas que podem ser realizadas. Na primeira passada, fazemos n-1 comparaes; na segunda, n-2; na terceira n-3; e assim por diante. Logo, o tempo total gasto pelo algoritmo proporcional a (n-1) + (n-2) + ... + 2 + 1. A soma desses termos proporcional ao quadrado de n. Dizemos que o algoritmo de ordem quadrtica e representamos isso escrevendo O(n2). Implementao recursiva Analisando a forma com que a ordenao bolha funciona, verificamos que o algoritmo procura resolver o problema da ordenao por partes. Inicialmente, o algoritmo coloca em sua posio (no final do vetor) o maior elemento, e o problema restante semelhante ao inicial, s que com um vetor com menos elementos, formado pelos elementos v[0],,v[n-2]. Baseado nessa observao, fcil implementar um algoritmo de ordenao bolha recursivamente. Embora no seja a forma mais adequada de implementarmos esse algoritmo, o estudo dessa recurso nos ajudar a entender a idia por trs do prximo algoritmo de ordenao que veremos mais adiante. O algoritmo recursivo de ordenao bolha posiciona o elemento de maior valor e chama, recursivamente, o algoritmo para ordenar o vetor restante, com n-1 elementos.
14-4
/* Ordenao bolha recursiva */ void bolha_rec (int n, int* v) { int j; int troca = 0; for (j=0; j<n-1; j++) if (v[j]>v[j+1]) { /* troca */ int temp = v[j]; v[j] = v[j+1]; v[j+1] = temp; troca = 1; } if (troca != 0) /* houve troca */ bolha_rec(n-1,v); }
Algoritmo genrico** Esse mesmo algoritmo pode ser aplicado a vetores que guardam outras informaes. O cdigo escrito acima pode ser reaproveitado, a menos de alguns detalhes. Primeiro, a assinatura da funo deve ser alterada, pois deixamos de ter um vetor de inteiros; segundo, a forma de comparao entre os elementos tambm deve ser alterada, pois no podemos, por exemplo, comparar duas cadeias de caracteres usando simplesmente o operador relacional maior que (>). Para aumentar o potencial de reuso do nosso cdigo, podemos re-escrever o algoritmo de ordenao apresentado acima tornando-o independente da informao armazenada no vetor. Vamos inicialmente discutir como podemos abstrair a funo de comparao. O mesmo algoritmo para ordenao de inteiros apresentado acima pode ser re-escrito usando-se uma funo auxiliar que faz a comparao. Em vez de compararmos diretamente dois elementos com o operador maior que, usamos uma funo auxiliar que, dados dois elementos, verifica se o primeiro maior que o segundo.
/* Funo auxiliar de comparao */ int compara (int a, int b) { if (a > b) return 1; else return 0; } /* Ordenao bolha (3a. verso) */ void bolha (int n, int* v) { int i, j; for (i=n-1; i>0; i--) { int troca = 0; for (j=0; j<i; j++) if (compara(v[j],v[j+1])) { /* troca */ int temp = v[j]; v[j] = v[j+1]; v[j+1] = temp; troca = 1; } if (troca == 0) /* nao houve troca */ return; } }
Desta forma, j aumentamos o potencial de reuso do algoritmo. Podemos, por exemplo, arrumar os elementos em ordem decrescente simplesmente re-escrevendo a funo compara. A idia fundamental escrever uma funo de comparao que recebe dois
Estruturas de Dados PUC-Rio 14-5
elementos e verifica se h uma inverso de ordem entre o primeiro e o segundo. Assim, se tivssemos um vetor de cadeia de caracteres para ordenar, poderamos usar a seguinte funo de ordenao.
int compara (char* a, char* b) { if (strcmp(a,b) > 0) return 1; else return 0; }
Uma funo de comparao, neste caso, receberia como parmetros dois ponteiros para a estrutura que representa um aluno e, considerando uma ordenao que usa o nome do aluno como chave de comparao, poderia ter a seguinte implementao:
int compara (Aluno* a, Aluno* b) { if (strcmp(a->nome,b->nome) > 0) return 1; else return 0; }
Portanto, o uso de uma funo auxiliar para realizar a comparao entre os elementos ajuda para a obteno de um cdigo reusvel. No entanto, isto s no suficiente. Para o mesmo cdigo poder ser aplicado a qualquer tipo de informao armazenada no vetor, precisamos tornar a implementao independente do tipo do elemento, isto , precisamos tornar tanto a prpria funo de ordenao (bolha ) quanto a funo de comparao (compara) independentes do tipo do elemento. Em C, a forma de generalizar o tipo usar o tipo void*. Escreveremos o cdigo de ordenao considerando que temos um ponteiro de qualquer tipo e passaremos para a funo de comparao dois ponteiros genricos, um para cada elemento que se deseja comparar. A funo de ordenao, no entanto, precisa percorrer o vetor e para tanto precisamos passar para a funo uma informao adicionalo tamanho, em nmero de bytes, de cada elemento. A assinatura da funo de ordenao poderia ento ser dada por:
void buble (int n, void* v, int tam);
Assim, se estamos ordenando vetores de inteiros, escrevemos a nossa funo de comparao convertendo o ponteiro genrico para um ponteiro de inteiro e fazendo o teste apropriado:
14-6
/* funo de comparao para inteiros */ int compara (void* a, void* b) { int* p1 = (int*) a; int* p2 = (int*) b; int i1 = *p1; int i2 = *p2; if (i1 > i2) return 1; else return 0; }
Se os elementos do vetor fossem ponteiros para a estrutura aluno, a funo de comparao poderia ser:
/* funo de comparao para ponteiros de alunos */ int compara (void* a, void* b) { Aluno** p1 = (Aluno**) a; Aluno** p2 = (Aluno**) b; Aluno* i1 = *p1; Aluno* i2 = *p2; if (strcmp(i1->nome,i2->nome) > 0) return 1; else return 0; }
O cdigo da funo de ordenao necessita percorrer os elementos do vetor. O acesso a um determinado elemento i do vetor no pode mais ser feito diretamente por v[i]. Dado o endereo do primeiro elemento do vetor, devemos incrementar este endereo de i*tam bytes para termos o endereo do elemento i . Podemos ento escrever uma funo auxiliar que faz esse incremento de endereo. Essa funo recebe como parmetros o endereo inicial do vetor, o ndice do elemento cujo endereo se quer alcanar e o tamanho (em bytes) de cada elemento. A funo retorna o endereo do elemento especificado. Uma parte sutil, porm necessria, dessa funo que para incrementar o endereo genrico de um determinado nmero de bytes, precisamos antes, temporariamente, converter esse ponteiro para ponteiro para caractere (pois um caractere ocupa um byte). O cdigo dessa funo auxiliar pode ser dado por:
void* acessa (void* v, int i, int tam) { char* t = (char*)v; t += tam*i; return (void*)t; }
A funo de ordenao identifica a ocorrncia de inverses entre elementos e realiza uma troca entre os valores. O cdigo que realiza a troca tambm tem que ser pensado de forma genrica, pois, como no sabemos o tipo de cada elemento, no temos como declarar a varivel temporria para poder realizar a troca. Uma alternativa fazer a troca dos valores byte a byte (ou caractere a caractere). Para tanto, podemos definir uma outra funo auxiliar que recebe os ponteiros genricos dos dois elementos que devem ter seus valores trocados, alm do tamanho de cada elemento.
14-7
void troca (void* a, void* b, int tam) { char* v1 = (char*) a; char* v2 = (char*) b; int i; for (i=0; i<tam; i++) { char temp = v1[i]; v1[i] = v2[i]; v2[i] = temp; } }
Assim, podemos escrever o cdigo da nossa funo de ordenao genrica. Falta, no entanto, um ltimo detalhe. As funes auxiliares acessa e troca so realmente genricas, e independem da informao efetivamente armazenada no vetor. Porm, a funo de comparao deve ser especializada para cada tipo de informao, conforme ilustramos acima. A assinatura dessa funo genrica, mas a sua implementao deve, naturalmente, levar em conta a informao armazenada para que a comparao tenha sentido. Portanto, para generalizar a implementao da funo de ordenao, no podemos chamar uma funo de comparao especfica. A soluo passar, via parmetro, qual funo de ordenao deve ser chamada. Para tanto, temos que introduzir o conceito de ponteiro para funo. O nome de uma funo representa o endereo dessa funo. A nossa funo de comparao tem a assinatura:
int compara (void*, void*);
Uma varivel ponteiro para armazenar o endereo dessa funo declarada como:
int(*cmp)(void*,void*);
onde cmp representa a varivel do tipo ponteiro para a funo em questo. Agora sim, podemos escrever nossa funo de ordenao genrica, recebendo como parmetro adicional o ponteiro da funo de comparao:
/* Ordenao bolha (genrica) */ void bolha_gen (int n, void* v, int tam, int(*cmp)(void*,void*)) { int i, j; for (i=n-1; i>0; i--) { int fez_troca = 0; for (j=0; j<i; j++) { void* p1 = acessa(v,j,tam); void* p2 = acessa(v,j+1,tam); if (cmp(p1,p2)) troca(p1,p2,tam); fez_troca = 1; } if (fez_troca == 0) /* nao houve troca */ return; } }
Esse cdigo genrico pode ser usado para ordenar vetores com qualquer informao. Para exemplificar, vamos us-lo para ordenar um vetor de nmeros reais. Para isso, temos que escrever o cdigo da funo que faz a comparao, agora especializada para nmero reais:
14-8
int compara_reais (void* a, void* b) { float* p1 = (float*) a; float* p2 = (float*) b; float f1 = *p1; float f2 = *p2; if (f1 > f2) return 1; else return 0; }
Neste ponto, v[a] e v[b] so trocados, e a busca continua, para cima a partir de v [ a + 1 ] , e para baixo, a partir de v[b-1] . Em algum momento, a busca termina, porque os pontos de busca se encontraro. Neste momento, a posio correta de x est definida, e os valores v[0] e v[a] so trocados. Vamos usar o mesmo exemplo da seo anterior:
(0-7) 25 48 37 12 57 86 33 92
onde indicamos atravs de (0-7) que se trata do vetor inteiro, de v[0] a v[7] . Podemos comear a executar o algoritmo procurando determinar a posio correta de x=v[0]=25. Partindo do incio do vetor, j temos, na primeira comparao, 48>25 (a=1). Partindo do final do vetor, na direo oposta, temos 25<92, 25<33, 25<86, 25<57 e finalmente, 12<=25 (b=3).
(0-7) 25 48 37 12 57 86 33 92 a b
Trocamos ento v [ a ] = 4 8 e v[b]=12 , incrementando a de uma unidade e decrementando b de uma unidade. Os elementos do vetor ficam com a seguinte disposio:
(0-7) 25 12 37 48 57 86 33 92 a,b
Na continuao, temos 37>25 (a=2). Pelo outro lado, chegamos tambm a 37 e temos 37>25 e 12<=25. Neste ponto, verificamos que os ndices a e b se cruzaram, agora com b<a.
(0-7) 25 12 37 48 57 86 33 92 b a
Assim, todos os elementos de 37 (inclusive) em diante so maiores que 25, e todos os elementos de 12 (inclusive) para trs so menores que 25. Com exceo do prprio 25, claro. A prxima etapa troca o piv, v[0]=25, com o ltimo dos valores menores que 25 encontrado: v[b]=12. Temos:
(0-7)12 25 37 48 57 86 33 92
com 25 em sua posio correta, e dois vetores menores para ordenar. Valores menores que 25:
(0-0) 12
E valores maiores:
(2-7) 37 48 57 86 33 92
Neste caso, em particular, o primeiro vetor (com apenas um elemento: (0-0)) j se encontra ordenado. O segundo vetor (2-7) pode ser ordenado de forma semelhante:
(2-7) 37 48 57 86 33 92
14-10
Devemos decidir qual a posio correta de 37 . Para isso identificamos o primeiro elemento maior que 37, ou seja, 48, e o ltimo menor que 37, ou seja, 33.
(2-7) 37 48 57 86 33 92 a b
Continuando o processo, verificamos que 37<57 e 37<86 , 37<57 , mas 37>=33 . Identificamos novamente que a e b se cruzaram.
(2-7) 37 33 57 86 48 92 b a
Assim, a posio correta de 37 a posio ocupada por v[b] , e os dois elementos devem ser trocados:
(2-7) 33 37 57 86 48 92
restando os vetores
(2-2) 33
e
(4-7) 57 86 48 92
para serem ordenados. O processo continua at que o vetor original esteja totalmente ordenado.
(0-7) 12 25 33 37 48 57 86 92
A implementao do quick sort normalmente recursiva, para facilitar a ordenao dos dois vetores menores encontrados. A seguir, apresentamos uma possvel implementao do algoritmo, adotando como piv o primeiro elemento.
14-11
/* Ordenao rpida */ void rapida (int n, int* v) { if (n <= 1) return; else { int x = v[0]; int a = 1; int b = n-1; do { while (a < n && v[a] <= x) a++; while (v[b] > x) b--; if (a < b) { /* faz troca */ int temp = v[a]; v[a] = v[b]; v[b] = temp; a++; b--; } } while (a <= b); /* troca piv */ v[0] = v[b]; v[b] = x; /* ordena sub-vetores restantes */ rapida(b,v); rapida(n-a,&v[a]); } }
Devemos observar que para deslocar o ndice a para a direita, fizemos o teste:
while (a < n && v[a] <= x)
O teste adicional no deslocamento para a direita necessrio porque o piv pode ser o elemento de maior valor, nunca ocorrendo a situao v[a]<=x, o que nos faria acessar posies alm dos limites do vetor. No deslocamento para a esquerda, um teste adicional tipo b>=0 no necessrio, pois, na nossa implementao, v[0] o piv, impedindo que b assuma valores negativos (teremos, pelo menos, x>=v[0]). Algoritmo genrico da biblioteca padro O quicksort o algoritmo de ordenao mais utilizado no desenvolvimento de aplicaes. Mesmo quando temos os dados organizados em listas encadeadas, e precisamos coloc-los de forma ordenada, em geral, optamos por criar um vetor temporrio com ponteiros para os ns da lista, fazer a ordenao usando quicksort e reencadear os ns montando a lista ordenada. Devido a sua grande utilidade, a biblioteca padro de C disponibiliza, via a interface stdlib.h , uma funo que ordena vetores usando esse algoritmo. A funo disponibilizada pela biblioteca independe do tipo de informao armazenada no vetor. A implementao dessa funo genrica segue os princpios discutidos na implementao do algoritmo de ordenao bolha genrico. O prottipo da funo disponibilizada pela biblioteca :
14-12
void qsort (void *v, int n, int tam, int (*cmp)(const void*, const void*));
Os parmetros de entrada dessa funo so: v : o ponteiro para o primeiro elemento do vetor que se deseja ordenar. Como no se sabe, a priori, o tipo dos elementos do vetor, temos um ponteiro genrico void*. n: o nmero de elementos do vetor. tam: o tamanho, em bytes, de cada elemento do vetor. cmp : o ponteiro para a funo responsvel por comparar dois elementos do vetor. Em C, o nome de uma funo representa o ponteiro da funo. Esse ponteiro pode ser armazenado numa varivel, possibilitando chamar a funo indiretamente. Como era de se esperar, a biblioteca no sabe comparar dois elementos do vetor (ela desconhece o tipo desses elementos). Fica a cargo do cliente da funo de ordenao escrever a funo de comparao. Essa funo de comparao tem que ter o seguinte prottipo:
int nome (const void*, const void*);
O parmetro cmp recebido pela funo qsort um ponteiro para uma funo com esse prottipo. Assim, para usarmos a funo de ordenao da biblioteca temos que escrever uma funo que receba dois ponteiros genricos, void*, os quais representam ponteiros para os dois elementos que se deseja comparar. O modificador de tipo const aparece no prottipo apenas para garantir que essa funo no modificar os valores dos elementos (devem ser tratados como valores constantes). Essa funo deve ter como valor de retorno 1 , 0 , ou 1 , dependendo se o primeiro elemento for menor, igual, ou maior que o segundo, respectivamente. Para ilustrar a utilizao da funo qsort vamos considerar alguns exemplos. O cdigo a seguir ilustra a utilizao da funo para ordenar valores reais. Neste caso, os dois ponteiros genricos passados para a funo de comparao representam ponteiros para float.
/* Ilustra uso do algoritmo qsort */ #include <stdio.h> #include <stdlib.h> /* funo de comparao de reais */ int comp_reais (const void* p1, const void* p2) { /* converte ponteiros genricos para ponteiros de float */ float *f1 = (float*)p1; float *f2 = (float*)p2; /* dados os ponteiros de float, faz a comparao */ if (*f1 < *f2) return 1; else if (*f1 > * f2) return 1; else return 0; }
14-13
/* programa que faz a ordenao de um vetor */ int main (void) { int i; float v[8] = {25.6,48.3,37.7,12.1,57.4,86.6,33.3,92.8}; qsort(v,8,sizeof(float),comp_reais); printf("Vetor ordenado: "); for (i=0; i<8; i++) printf("%g ",v[i]); printf("\n); return 0; }
Vamos agora considerar que temos um vetor de alunos e que desejamos ordenar o vetor usando o nome do aluno como chave de comparao. A estrutura que representa um aluno pode ser dada por:
struct aluno { char nome[81]; char mat[8]; char turma; char email[41]; }; typedef struct aluno Aluno;
Vamos analisar duas situaes. Na primeira, consideraremos a existncia de um vetor da estrutura (por exemplo, Aluno vet[N];). Neste caso, cada elemento do vetor do tipo Aluno e os dois ponteiros genricos passados para a funo de comparao representam ponteiros para Aluno. Essa funo de comparao pode ser dada por:
/* Funo de comparao: elemento do tipo Aluno */ int comp_alunos (const void* p1, const void* p2) /* converte ponteiros genricos para ponteiros de Aluno */ Aluno *a1 = (Aluno*)p1; Aluno *a2 = (Aluno*)p2; /* dados os ponteiros de Aluno, faz a comparao */ return strcmp(a1->nome,a2->nome); }
Numa segunda situao, podemos considerar que temos um vetor de ponteiros para a estrutura aluno (por exemplo, Aluno* vet[N];). Agora, cada elemento do vetor um ponteiro para o tipo Aluno e a funo de comparao tem que tratar uma indireo a mais. Aqui, os dois ponteiros genricos passados para a funo de comparao representam ponteiros de ponteiros para Aluno.
/* Funo de comparao: elemento do tipo Aluno* */ int comp_alunos (const void* p1, const void* p2) /* converte ponteiros genricos para ponteiros de ponteiros de Aluno */ Aluno **pa1 = (Aluno**)p1; Aluno **pa2 = (Aluno**)p2; /* acessa ponteiro de Aluno */ Aluno *a1 = *p1; Aluno *a2 = *p2; /* dados os ponteiros de Aluno, faz a comparao */ return strcmp(a1->nome,a2->nome); }
14-14
16. Busca
W. Celes e J. L. Rangel
Neste captulo, discutiremos diferentes estratgias para efetuarmos a busca de um elemento num determinado conjunto de dados. A operao de busca encontrada com muita freqncia em aplicaes computacionais, sendo portanto importante estudar estratgias distintas para efetu-la. Por exemplo, um programa de controle de estoque pode buscar, dado um cdigo numrico ou um nome, a descrio e as caractersticas de um determinado produto. Se temos um grande nmero de produtos cadastrados, o mtodo para efetuar a busca deve ser eficiente, caso contrrio a busca pode ser muito demorada, inviabilizando sua utilizao. Neste captulo, estudaremos algumas estratgias de busca. Inicialmente, consideraremos que temos nossos dados armazenados em um vetor e discutiremos os algoritmos de busca que podemos empregar. A seguir, discutiremos a utilizao de rvores binrias de busca, que so estruturas de rvores projetadas para darem suporte a operaes de busca de forma eficiente. No prximo captulo, discutiremos as estruturas conhecidas como tabelas de disperso (hash) que podem, como veremos, realizar buscas de forma extremamente eficiente, fazendo uso de espao de memria adicional.
Esse algoritmo de busca extremamente simples, mas pode ser muito ineficiente quando o nmero de elementos no vetor for muito grande. Isto porque o algoritmo (a funo, no caso) pode ter que percorrer todos os elementos do vetor para verificar que um determinado elemento est ou no presente. Dizemos que no pior caso ser necessrio realizar n comparaes, onde n representa o nmero de elementos no vetor.
Estruturas de Dados PUC-Rio 16-1
Portanto, o desempenho computacional desse algoritmo varia linearmente com relao ao tamanho do problema chamamos esse algoritmo de busca linear. Em geral, usamos a notao Big-O para expressarmos como a complexidade de um algoritmo varia com o tamanho do problema. Assim, nesse caso em que o tempo computacional varia linearmente com o tamanho do problema, dizemos que trata-se de um algoritmo de ordem linear e expressamos isto escrevendo O(n). No melhor caso, se dermos sorte do elemento procurado ocupar a primeira posio do vetor, o algoritmo acima necessitaria de apenas uma nica comparao. Esse fato, no entanto, no pode ser usado para fazermos uma anlise de desempenho do algoritmo, pois o melhor caso representa um situao muito particular. Alm do pior caso, devemos analisar o caso mdio, isto , o caso que ocorre na mdia. J vimos que o algoritmo em questo requer n comparaes quando o elemento no est presente no vetor. E no caso do elemento estar presente, quantas operaes de comparao so, em mdia, necessrias? Na mdia, podemos concluir que so necessrias n/2 comparaes. Em termos de ordem de complexidade, no entanto, continuamos a ter uma variao linear, isto , O(n), pois dizemos que O(k n), onde k uma constante, igual a O(n). Em diversas aplicaes reais, precisamos de algoritmos de busca mais eficientes. Seria possvel melhorarmos a eficincia do algoritmo de busca mostrado acima? Infelizmente, se os elementos estiverem armazenados em uma ordem aleatria no vetor, no temos como melhorar o algoritmo de busca, pois precisamos verificar todos os elementos. No entanto, se assumirmos, por exemplo, que os elementos esto armazenados em ordem crescente, podemos concluir que um elemento no est presente no vetor se acharmos um elemento maior, pois se o elemento que buscamos estivesse presente ele precederia um elemento maior na ordem do vetor. O cdigo abaixo ilustra a implementao da busca linear assumindo que os elementos do vetor esto ordenados (vamos assumir ordem crescente).
int busca_ord (int n, int* vet, int elem) { int i; for (i=0; i<n; i++) { if (elem == vet[i]) return i; else if (elem < vet[i]) return -1; }
No caso do elemento procurado no pertencer ao vetor, esse segundo algoritmo apresenta um desempenho ligeiramente superior ao primeiro, mas a ordem dessa verso do algoritmo continua sendo linear O(n). No entanto, se os elementos do vetor esto ordenados, existe um algoritmo muito mais eficiente que ser apresentado a seguir.
16-2
Busca binria No caso dos elementos do vetor estarem em ordem, podemos aplicar um algoritmo mais eficiente para realizarmos a busca. Trata-se do algoritmo de busca binria. A idia do algoritmo testar o elemento que buscamos com o valor do elemento armazenado no meio do vetor. Se o elemento que buscamos for menor que o elemento do meio, sabemos que, se o elemento estiver presente no vetor, ele estar na primeira parte do vetor; se for maior, estar na segunda parte do vetor; se for igual, achamos o elemento no vetor. Se concluirmos que o elemento est numa das partes do vetor, repetimos o procedimento considerando apenas a parte que restou: comparamos o elemento que buscamos com o elemento armazenado no meio dessa parte. Este procedimento continuamente repetido, subdividindo a parte de interesse, at encontrarmos o elemento ou chegarmos a uma parte do vetor com tamanho zero. O cdigo a seguir ilustra uma implementao de busca binria num vetor de valores inteiros ordenados de forma crescente.
int busca_bin (int n, int* vet, int elem) { /* no inicio consideramos todo o vetor */ int ini = 0; int fim = n-1; int meio; /* enquanto a parte restante for maior que zero */ while (ini <= fim) { meio = (ini + fim) / 2; if (elem < vet[meio]) fim = meio 1; /* ajusta posico final */ else if (elem > vet[meio]) ini = meio + 1; /* ajusta posico inicial */ else return meio; /* elemento encontrado */ } /* no encontrou: restou parte de tamanho zero */ return -1; }
O desempenho desse algoritmo muito superior ao de busca linear. Novamente, o pior caso caracteriza-se pela situao do elemento que buscamos no estar no vetor. Quantas vezes precisamos repetir o procedimento de subdiviso para concluirmos que o elemento no est presente no vetor? A cada repetio, a parte considerada na busca dividida metade. A tabela abaixo mostra o tamanho do vetor a cada repetio do lao do algoritmo. Repetio 1 2 3 ... log n Tamanho do problema n n/2 n/4 ... 1
Sendo assim necessrias log n repeties. Como fazemos um nmero constante de comparaes a cada ciclo (duas comparaes por ciclo), podemos concluir que a ordem desse algoritmo O(log n).
16-3
O algoritmo de busca binria consiste em repetirmos o mesmo procedimento recursivamente, podendo ser naturalmente implementado de forma recursiva. Embora a implementao no recursiva seja mais eficiente e mais adequada para esse algoritmo, a implementao recursiva mais sucinta e vale a pena ser apresentada. Na implementao recursiva, temos dois casos a serem tratados. No primeiro, a busca deve continuar na primeira metade do vetor, logo chamamos a funo recursivamente passando como parmetros o nmero de elementos dessa primeira parte restante e o mesmo ponteiro para o primeiro elemento, pois a primeira parte tem o mesmo primeiro elemento do que o vetor como um todo. No segundo caso, a busca deve continuar apenas na segunda parte do vetor, logo passamos na chamada recursiva, alm do nmero de elementos restantes, um ponteiro para o primeiro elemento dessa segunda parte. Para simplificar, a funo de busca apenas informa se o elemento pertence ou no ao vetor, tendo como valor de retorno falso (0 ) ou verdadeiro (1 ). Uma possvel implementao usando essa estratgia mostrada a seguir.
int busca_bin_rec (int n, int* vet, int elem) { /* testa condio de contorno: parte com tamanho zero */ if (n <= 0) return 0; else { /* deve buscar o elemento entre os ndices 0 e n-1 */ int meio = (n - 1) / 2; if (elem < vet[meio]) return busca_bin_rec(meio,vet,elem); else if (elem > vet[meio]) return busca_bin_rec(n1-meio, &vet[meio+1],elem); else return 1; /* elemento encontrado */ } }
Em particular, devemos notar a expresso &vet[meio+1] que, como sabemos, resulta num ponteiro para o primeiro elemento da segunda parte do vetor. Se quisermos que a funo tenha como valor de retorno o ndice do elemento, devemos acertar o valor retornado pela chamada recursiva na segunda parte do vetor. Uma implementao com essa modificao apresentada abaixo:
int busca_bin_rec (int n, int* vet, int elem) { /* testa condio de contorno: parte com tamanho zero */ if (n <= 0) return -1; else { /* deve buscar o elemento entre os ndices 0 e n-1 */ int meio = (n - 1) / 2; if (elem < vet[meio]) return busca_bin_rec(meio,vet,elem); else if (elem > vet[meio]) { int r = busca_bin_rec(n-1-meio, &vet[meio+1],elem); if (r<0) return -1; else return meio+1+r; } else return meio; /* elemento encontrado */ } }
Estruturas de Dados PUC-Rio 16-4
Algoritmo genrico A biblioteca padro de C disponibiliza, via a interface stdlib.h, uma funo que faz a busca binria de um elemento num vetor. A funo disponibilizada pela biblioteca independe do tipo de informao armazenada no vetor. A implementao dessa funo genrica segue os mesmos princpios discutidos no captulo anterior. O prottipo da funo de busca binria da biblioteca :
void* bsearch (void* info, void *v, int n, int tam, int (*cmp)(const void*, const void*) );
Se o elemento for encontrado no vetor, a funo tem como valor de retorno o endereo do elemento no vetor; caso o elemento no seja encontrado, o valor de retorno NULL. Anlogo a funo qsort, apresentada no captulo anterior, os parmetros de entrada dessa funo so: info: o ponteiro para a informao que se deseja buscar no vetor representa a chave de busca; v : o ponteiro para o primeiro elemento do vetor onde a busca ser feita. Os elementos do vetor tm que estar ordenados, segundo o critrio de ordenao adotado pela funo de comparao descrita abaixo. n: o nmero de elementos do vetor. tam: o tamanho, em bytes, de cada elemento do vetor. cmp: o ponteiro para a funo responsvel por comparar a informao buscada e um elemento do vetor. O primeiro parmetro dessa funo sempre o endereo da informao buscada, e o segundo um ponteiro para um dos elementos do vetor. O critrio de comparao adotado por essa funo deve ser compatvel com o critrio de ordenao do vetor. Essa funo deve ter como valor de retorno 1, 0, ou 1, dependendo se a informao buscada for menor, igual, ou maior que a informao armazenada no elemento, respectivamente. Para ilustrar a utilizao da funo bsearch vamos, inicialmente, considerar um vetor de valores inteiros. Neste caso, os dois ponteiros genricos passados para a funo de comparao representam ponteiros para int.
/* Ilustra uso do algoritmo bsearch */ #include <stdio.h> #include <stdlib.h> /* funo de comparao de inteiros */ int comp_int (const void* p1, const void* p2) { /* converte ponteiros genricos para ponteiros de int */ int *i1 = (int*)p1; int *i2 = (int*)p2; /* dados os ponteiros de int, faz a comparao */ if (*i1 < *i2) return 1; else if (*i1 > *i2) return 1; else return 0; }
16-5
/* programa que faz a busca em um vetor */ int main (void) { int v[8] = {12,25,33,37,48,57,86,92}; int e = 57; /* informao que se deseja buscar */ int* p; p = (int*)bsearch(&e,v,8,sizeof(int),comp_int); if (p == NULL) printf("Elemento nao encontrado.\n"); else printf("Elemento encontrado no indice: %d\n", p-v); return 0; }
Devemos notar que o ndice do elemento, se encontrado no vetor, pode ser extrado subtraindo-se o ponteiro do elemento do ponteiro do primeiro elemento (p-v). Essa aritmtica de ponteiros vlida aqui pois podemos garantir que ambos os ponteiros armazenam endereos de memria de um mesmo vetor. A diferena entre os ponteiros representa a distncia em que os elementos esto armazenados na memria. Vamos agora considerar que queremos efetuar uma busca num vetor de ponteiros para alunos. A estrutura que representa um aluno pode ser dada por:
struct aluno { char nome[81]; char mat[8]; char turma; char email[41]; }; typedef struct aluno Aluno;
Considerando que o vetor est ordenado segundo os nomes dos alunos, podemos buscar a ocorrncia de um determinado aluno passando para a funo de busca um nome e o vetor. A funo de comparao ento receber dois ponteiros: um ponteiro para uma cadeia de caracteres e um ponteiro para um elemento do vetor (no caso ser um ponteiro para ponteiro de aluno, ou seja, um Aluno**).
/* Funo de comparao: char* e Aluno** */ int comp_alunos (const void* p2, const void* p2) /* converte ponteiros genricos para ponteiros especficos */ char* s = (char*)p1; Aluno **pa = (Aluno**)p2; /* faz a comparao */ return strcmp(s,(*pa)->nome); }
Conforme observamos, o tipo de informao a ser buscada nem sempre igual ao tipo do elemento; para dados complexos, em geral no . A informao buscada geralmente representa um campo da estrutura armazenada no vetor (ou da estrutura apontada por elementos do vetor). Devemos finalmente salientar que se tivermos os dados armazenados em uma lista encadeada, s temos a alternativa de implementar um algoritmo de busca linear, mesmo se os elementos estiverem ordenados. Portanto, lista encadeada no uma boa opo para estruturarmos nossos dados, se desejarmos realizar muitas operaes de busca. A estrutura dinmica apropriada para a realizao de busca a rvore binria de busca que ser discutida a seguir.
Estruturas de Dados PUC-Rio 16-6
16-7
Usando essa propriedade de ordem, a busca de um valor em uma rvore pode ser simplificada. Para procurar um valor numa rvore, comparamos o valor que buscamos com o valor associado raiz. Em caso de igualdade, o valor foi encontrado; se o valor dado for menor que o valor associado raiz, a busca continua na sae; caso contrrio, se o valor associado raiz for menor, a busca continua na sad. Por essa razo, estas rvores so freqentemente chamadas de rvores binrias de busca. Naturalmente, a ordem a que fizemos referncia acima dependente da aplicao. Se a informao a ser armazenada em cada n da rvore for um nmero inteiro podemos usar o habitual operador relacional menor que (<). Porm, se tivermos que considerar casos em que a informao mais complexa, j vimos que uma funo de comparao deve ser definida pelo programador, especificamente para cada caso. Operaes em rvores binrias de busca Para exemplificar a implementao de operaes em rvores binrias de busca, vamos considerar o caso em que a informao associada a um n um nmero inteiro, e no vamos considerar a possibilidade de repetio de valores associados aos ns da rvore. A Figura 16.1 ilustra uma rvore de busca de valores inteiros.
6 2 1 3
Figura 16.1: Exemplo de rvore binria de busca.
8 4
A rvore representada pelo ponteiro para o n raiz. A rvore vazia inicializada atribuindo-se NULL a varivel que representa a rvore. Uma funo simples de inicializao mostrada abaixo:
Arv* init (void) { return NULL; }
16-8
Uma vez construda uma rvore de busca, podemos imprimir os valores da rvore em ordem crescente percorrendo os ns em ordem simtrica:
void imprime (Arv* a) { if (a != NULL) { imprime(a->esq); printf("%d\n",a->info); imprime(a->dir); } }
Essas so funes anlogas s vistas para rvores binrias comuns, pois no exploram a propriedade de ordenao das rvores de busca. No entanto, as operaes que nos interessa analisar em detalhes so: busca: funo que busca um elemento na rvore; insere: funo que insere um novo elemento na rvore; retira: funo que retira um elemento da rvore. Operao de busca A operao para buscar um elemento na rvore explora a propriedade de ordenao da rvore, tendo um desempenho computacional proporcional a sua altura (O(log n) para o caso de rvore balanceada). Uma implementao da funo de busca dada por:
Arv* busca (Arv* r, int v) { if (r == NULL) return NULL; else if (r->info > v) return busca (r->esq, v); else if (r->info < v) return busca (r->dir, v); else return r; }
Operao de insero A operao de insero adiciona um elemento na rvore na posio correta para que a propriedade fundamental seja mantida. Para inserir um valor v em uma rvore usamos sua estrutura recursiva, e a ordenao especificada na propriedade fundamental. Se a (sub-) rvore vazia, deve ser substituda por uma rvore cujo nico n (o n raiz) contm o valor v. Se a rvore no vazia, comparamos v com o valor na raiz da rvore, e inserimos v na sae ou na sad, conforme o resultado da comparao. A funo abaixo ilustra a implementao dessa operao. A funo tem como valor de retorno o eventual novo n raiz da (sub-) rvore.
Arv* insere (Arv* a, int v) { if (a==NULL) { a = (Arv*)malloc(sizeof(Arv)); a->info = v; a->esq = a->dir = NULL; } else if (v < a->info) a->esq = insere(a->esq,v); else /* v < a->info */ a->dir = insere(a->dir,v); return a; }
16-9
Operao de remoo Outra operao a ser analisada a que permite retirar um determinado elemento da rvore. Essa operao um pouco mais complexa que a de insero. Existem trs situaes possveis. A primeira, e mais simples, quando se deseja retirar um elemento que folha da rvore (isto , um elemento que no tem filhos). Neste caso, basta retirar o elemento da rvore e atualizar o pai, pois seu filho no existe mais. A segunda situao, ainda simples, acontece quando o n a ser retirado possui um nico filho. Para retirar esse elemento necessrio antes acertar o ponteiro do pai, pulando o n: o nico neto passa a ser filho direto. A Figura 16.2 ilustra esse procedimento.
6 2 1 3 4 8
Retira n 4
6 2 1 3 4 8
O caso complicado ocorre quando o n a ser retirado tem dois filhos. Para poder retirar esse n da rvore, devemos proceder da seguinte forma: a) encontramos o elemento que precede o elemento a ser retirado na ordenao. Isto equivale a encontrar o elemento mais direita da sub-rvore esquerda; b) trocamos a informao do n a ser retirado com a informao do n encontrado; c) retiramos o n encontrado (que agora contm a informao do n que se deseja retirar). Observa-se que retirar o n mais direita trivial, pois esse um n folha ou um n com um nico filho (no caso, o filho da direita nunca existe). O procedimento descrito acima deve ser seguido para no haver violao da ordenao da rvore. Observamos que, anlogo ao que foi feito com o n mais direita da subrvore esquerda, pode ser feito com o n mais esquerda da sub-rvore direita (que o n que segue o n a ser retirado na ordenao). A Figura 16.3 exemplifica a retirada de um n com dois filhos. Na figura mostrada a estratgia de retirar o elemento que precede o elemento a ser retirado na ordenao.
16-10
Destruio do n 6
6 2 1 3 4 8
troca 6 com 4
4 2 1 3 6 8
retira n 6
4 2 1 3 6 8
Figura 16.3: Exemplo da operao para retirar o elemento com informao igual a 6.
O cdigo abaixo ilustra a implementao da funo para retirar um elemento da rvore binria de busca. A funo tem como valor de retorno a eventual nova raiz da (sub-) rvore.
Arv* retira (Arv* r, int v) { if (r == NULL) return NULL; else if (r->info > v) r->esq = retira(r->esq, v); else if (r->info < v) r->dir = retira(r->dir, v); else { /* achou o elemento */ if (r->esq == NULL && r->dir == NULL) { /* elemento sem filhos */ free (r); r = NULL; } else if (r->esq == NULL) { /* s tem filho direita */ Arv* t = r; r = r->dir; free (t); } else if (r->dir == NULL) { /* s tem filho esquerda */ Arv* t = r; r = r->esq; free (t); } else { /* tem os dois filhos */ Arv* pai = r; Arv* f = r->esq; while (f->dir != NULL) { pai = f; f = f->dir; } r->info = f->info; /* troca as informaes */ f->info = v; r->esq = retira(r->esq,v); } } return r; }
Exerccio: Escreva um programa que utilize as funes de rvore binria de busca mostradas acima.
16-11
rvores balanceadas fcil prever que, aps vrias operaes de insero/remoo, a rvore tende a ficar desbalanceada, j que essas operaes, conforme descritas, no garantem o balanceamento. Em especial, nota-se que a funo de remoo favorece uma das subrvores (sempre retirando um n da sub-rvore esquerda, por exemplo). Uma estratgia que pode ser utilizada para amenizar o problema intercalar de qual subrvore ser retirado o n. No entanto, isso ainda no garante o balanceamento da rvore. Para que seja possvel usar rvores binrias de busca mantendo sempre a altura das rvores no mnimo, ou prximo dele, necessrio um processo de insero e remoo de ns mais complicado, que mantenha as rvores balanceadas, ou equilibradas, tendo as duas sub-rvores de cada n o mesmo peso, ou pesos aproximadamente iguais. No caso de um nmero de ns par, podemos aceitar uma diferena de um n entre a sae (sub-rvore esquerda) e a sad (sub-rvore direita). A idia central de um algoritmo para balancear (equilibrar) uma rvore binria de busca pode ser a seguinte: se tivermos uma rvore com m elementos na sae, e n m + 2 elementos na sad, podemos tornar a rvore menos desequilibrada movendo o valor da raiz para a sae, onde ele se tornar o maior valor, e movendo o menor elemento da sad para a raiz. Dessa forma, a rvore continua com os mesmos elementos na mesma ordem. A situao em que a sad tem menos elementos que a sae semelhante. Esse processo pode ser repetido at que a diferena entre os nmeros de elementos das duas subrvores seja menor ou igual a 1 . Naturalmente, o processo deve continuar (recursivamente) com o balanceamento das duas sub-rvores de cada rvore. Um ponto a observar que remoo do menor (ou maior) elemento de uma rvore mais simples do que a remoo de um elemento qualquer. Exerccio: Implemente o algoritmo para balanceamento de rvore binria descrito acima.
16-12
No captulo anterior, discutimos diferentes estruturas e algoritmos para buscar um determinado elemento num conjunto de dados. Para obtermos algoritmos eficientes, armazenamos os elementos ordenados e tiramos proveito dessa ordenao para alcanar eficientemente o elemento procurado. Chegamos a concluso que os algoritmos eficientes de busca demandam um esforo computacional de O(log n). Neste captulo, vamos estudar as estruturas de dados conhecidas como tabelas de disperso (hash tables), que, se bem projetadas, podem ser usadas para buscar um elemento da tabela em ordem constante: O(1). O preo pago por essa eficincia ser um uso maior de memria, mas, como veremos, esse uso excedente no precisa ser to grande, e proporcional ao nmero de elementos armazenados. Para apresentar a idia das tabelas de disperso, vamos considerar um exemplo onde desejamos armazenar os dados referentes aos alunos de uma disciplina. Cada aluno individualmente identificado pelo seu nmero de matrcula. Podemos ento usar o nmero de matrcula como chave de busca do conjunto de alunos armazenados. Na PUC-Rio, o nmero de matrcula dos alunos dado por uma seqncia de oito dgitos, sendo que o ltimo representa um dgito de controle, no sendo portanto parte efetiva do nmero de matrcula. Por exemplo, na matricula 9711234-4, o ultimo dgito 4, aps o hfen, representa o dgito de controle. O nmero de matrcula efetivo nesse caso composto pelos primeiros sete dgitos: 9711234. Para permitir um acesso a qualquer aluno em ordem constante, podemos usar o nmero de matrcula do aluno como ndice de um vetor vet. Se isso for possvel, acessamos os dados do aluno cuja matrcula dado por mat indexando o vetor vet[mat]. Dessa forma, o acesso ao elemento se d em ordem constante, imediata. O problema que encontramos que, nesse caso, o preo pago para se ter esse acesso rpido muito grande. Vamos considerar que a informao associada a cada aluno seja representada pela estrutura abaixo:
struct aluno { int mat; char nome[81]; char email[41]; char turma; }; typedef struct aluno Aluno;
Como a matrcula composta por sete dgitos, o nmero inteiro que conceitualmente representa uma matrcula varia de 0000000 a 9999999. Portanto, precisamos dimensionar nosso vetor com dez milhes (10.000.000) de elementos. Isso pode ser feito por:
#define MAX 10000000 Aluno vet[MAX];
Dessa forma, o nome do aluno com matrcula mat acessado simplesmente por: vet[mat].nome . Temos um acesso rpido, mas pagamos um preo em uso de
Estruturas de Dados PUC-Rio 17-1
memria proibitivo. Como a estrutura de cada aluno, no nosso exemplo, ocupa pelo menos 127 bytes, estamos falando num gasto de 1.270.000.000 bytes, ou seja, acima de 1 Gbyte de memria. Como na prtica teremos, digamos, em torno de 50 alunos cadastrados, precisaramos apenas de algo em torno de 6.350 (=127*50) bytes. Para amenizar o problema, j vimos que podemos ter um vetor de ponteiros, em vez de um vetor de estruturas. Desta forma, as posies do vetor que no correspondem a alunos cadastrados teriam valores NULL . Para cada aluno cadastrado, alocaramos dinamicamente a estrutura de aluno e armazenaramos um ponteiro para essa estrutura no vetor. Neste caso, acessaramos o nome do aluno de matrcula mat por vet[mat]>nome. Assim, considerando que cada ponteiro ocupe 4 bytes, o gasto excedente de memria seria, no mximo, aproximadamente 40 Mbytes. Apesar de menor, esse gasto de memria ainda proibitivo. A forma de resolver o problema de gasto excessivo de memria, mas ainda garantindo um acesso rpido, atravs do uso de tabelas de disperso (hash table) que discutiremos a seguir.
Numa turma de aluno, comum existirem vrios alunos com o mesmo ano e perodo de ingresso. Portanto, esses trs primeiros dgitos no so bons candidatos para identificar individualmente cada aluno. Reduzimos nosso problema para uma chave com os quatro dgitos seqenciais. Podemos ir alm e constatar que os nmeros seqenciais mais significativos so os ltimos, pois num universo de uma turma de alunos, o dgito que representa a unidade varia mais do que o dgito que representa o milhar. Desta forma, podemos usar um nmero de matrcula parcial, de acordo com a dimenso que queremos que tenha nossa tabela (ou nosso vetor). Por exemplo, para dimensionarmos nossa tabela com apenas 100 elementos, podemos usar apenas os ltimos dois dgitos seqenciais do nmero de matrcula. A tabela pode ento ser declarada por: Aluno* tab[100]. Para acessarmos o nome do aluno cujo nmero de matrcula dado por mat, usamos como ndice da tabela apenas os dois ltimos dgitos. Isso pode ser conseguido aplicando-se o operador modulo (%): vet[mat%100]->nome.
Estruturas de Dados PUC-Rio 17-2
Desta forma, o uso de memria excedente pequeno e o acesso a um determinado aluno, a partir do nmero de matrcula, continua imediato. O problema que surge que provavelmente existiro dois ou mais alunos da turma que apresentaro os mesmos ltimos dois dgitos no nmero de matrcula. Dizemos que h uma coliso, pois alunos diferentes so mapeados para o mesmo ndice da tabela. Para que a estrutura funcione de maneira adequada, temos que resolver esse problema, tratando as colises. Existem diferentes mtodos para tratarmos as colises em tabelas de disperso, e estudaremos esses mtodos mais adiante. No momento, vale salientar que no h como eliminar completamente a ocorrncia de colises em tabelas de disperso. Devemos minimizar as colises e usar um mtodo que, mesmo com colises, saibamos identificar cada elemento da tabela individualmente.
Podemos generalizar essa funo para tabelas de disperso com dimenso N . Basta avaliar o modulo do nmero de matrcula por N:
int hash (int mat) { return (mat%N); }
Uma funo de hash deve, sempre que possvel, apresentar as seguintes propriedades: Ser eficientemente avaliada: isto necessrio para termos acesso rpido, pois temos que avaliar a funo de hash para determinarmos a posio onde o elemento se encontra armazenado na tabela. Espalhar bem as chaves de busca: isto necessrio para minimizarmos as ocorrncias de colises. Como veremos, o tratamento de colises requer um procedimento adicional para encontrarmos o elemento. Se a funo de hash resulta em muitas colises, perdemos o acesso rpido aos elementos. Um exemplo de funo de hash ruim seria usar, como ndice da tabela, os dois dgitos iniciais do nmero de matrcula todos os alunos iriam ser mapeados para apenas trs ou quatro ndices da tabela. Ainda para minimizarmos o nmero de colises, a dimenso da tabela deve guardar uma folga em relao ao nmero de elementos efetivamente armazenados. Como regra emprica, no devemos permitir que a tabela tenha uma taxa de ocupao superior a
17-3
75%. Uma taxa de 50% em geral traz bons resultados. Uma taxa menor que 25% pode representar um gasto excessivo de memria.
Uso da posio consecutiva livre Nas duas primeiras estratgias que discutiremos, os elementos que colidem so armazenados em outros ndices, ainda no ocupados, da prpria tabela. A escolha da posio ainda no ocupada para armazenar um elemento que colide diferencia as estratgias que iremos discutir. Numa primeira estratgia, se a funo de disperso mapeia para um ndice j ocupado, procuramos o prximo (usando incremento circular) ndice livre da tabela para armazenar o novo elemento. A Figura 17.2 ilustra essa estratgia. Nessa figura, os ndices da tabela que no tm elementos associados so preenchidos com o valor NULL. x h(x)
busca posio livre
Vale lembrar que uma tabela de disperso nunca ter todos os elementos preenchidos (j mencionamos que uma ocupao acima de 75% eleva o nmero de colises, descaracterizando a idia central da estrutura). Portanto, podemos garantir que sempre existir uma posio livre na tabela. Na operao de busca, considerando a existncia de uma tabela j construda, se uma chave x for mapeada pela funo de disperso (funo de hash h) para um determinado ndice h(x), procuramos a ocorrncia do elemento a partir desse ndice, at que o elemento seja encontrado ou que uma posio vazia seja encontrada. Uma possvel implementao mostrada a seguir. Essa funo de busca recebe, alm da
Estruturas de Dados PUC-Rio 17-4
tabela, a chave de busca do elemento que se busca, e tem como valor de retorno o ponteiro do elemento, se encontrado, ou NULL no caso do elemento no estar presente na tabela.
Aluno* busca (Hash tab, int mat) { int h = hash(mat); while (tab[h] != NULL) { if (tab[h]->mat == mat) return tab[h]; h = (h+1) % N; } return NULL; }
Devemos notar que a existncia de algum elemento mapeado para o mesmo ndice no garante que o elemento que buscamos esteja presente. A partir do ndice mapeado, temos que buscar o elemento utilizando, como chave de comparao, a real chave de busca, isto , o nmero de matrcula completo. A funo que insere ou modifica um determinado elemento tambm simples. Fazemos o mapeamento da chave de busca (no caso, nmero de matrcula) atravs da funo de disperso e verificamos se o elemento j existe na tabela. Se o elemento existir, modificamos o seu contedo; se no existir, inserimos um novo na primeira posio que encontrarmos na tabela, a partir do ndice mapeado. Uma possvel implementao dessa funo mostrada a seguir. Essa funo recebe como parmetros a tabela e os dados do elemento sendo inserido (ou os novos dados de um elemento j existente). A funo tem como valor de retorno o ponteiro do aluno modificado ou do novo aluno inserido.
Aluno* insere (Hash tab, int mat, char* nome, char* email, char turma) { int h = hash(mat); while (tab[h] != NULL) { if (tab[h]->mat == mat) break; h = (h+1) % N; } if (tab[h]==NULL) { /* no encontrou o elemento */ tab[h] = (Aluno*) malloc(sizeof(Aluno)); tab[h]->mat = mat; } /* atribui informao */ strcpy(tab[h]->nome,nome); strcpy(tab[h]->email,email); tab[h]->turma = turma; return tab[h]; }
Apesar de bastante simples, essa estratgia tende a concentrar os lugares ocupados na tabela, enquanto que o ideal seria dispersar. Uma estratgia que visa melhorar essa concentrao conhecida como disperso dupla (double hash) e ser apresentada a seguir. Uso de uma segunda funo de disperso Para evitar a concentrao de posies ocupadas na tabela, essa segunda estratgia faz uma variao na forma de procurar uma posio livre a fim armazenar o elemento que
17-5
colidiu. Aqui, usamos uma segunda funo de disperso, h. Para chaves de busca dadas por nmeros inteiros, uma possvel segunda funo de disperso definida por:
h' ( x) = N - 2 - x%( N - 2)
Nesta frmula, x representa a chave de busca e N a dimenso da tabela. De posse dessa segunda funo, procuramos uma posio livre na tabela com incrementos, ainda circulares, dados por h(x). Isto , em vez de tentarmos (h(x)+1)%N, tentamos (h(x)+h(x))%N. Dois cuidados devem ser tomados na escolha dessa segunda funo de disperso: primeiro, ela nunca pode retornar zero, pois isso no varia com que o ndice fosse incrementado; segundo, de preferncia, ela no pode retornar um nmero divisor da dimenso da tabela, pois isso nos limitaria a procurar uma posio livre num subconjunto restrito dos ndices da tabela. A implementao da funo de busca com essa estratgia uma pequena variao da funo de busca apresentada para a estratgia anterior.
int hash2 (int mat) { return N - 2 - mat%(N-2); } Aluno* busca (Hash tab, int mat) { int h = hash(mat); int h2 = hash2(mat); while (tab[h] != NULL) { if (tab[h]->mat == mat) return tab[h]; h = (h+h2) % N; } return NULL; }
Exerccio: Implemente a funo para inserir (ou modificar) um elemento usando a estratgia de uma segunda funo de disperso. Uso de listas encadeadas Uma estratgia diferente, mas ainda simples, consiste em fazer com que cada elemento da tabela hash represente um ponteiro para uma lista encadeada. Todos os elementos mapeados para um mesmo ndice seriam armazenados na lista encadeada. A Figura 17.1 ilustra essa estratgia. Nessa figura, os ndices da tabela que no tm elementos associados representam listas vazias.
17-6
h(x)
Com essa estratgia, cada elemento armazenado na tabela ser um elemento de uma lista encadeada. Portanto, devemos prever, na estrutura da informao, um ponteiro adicional para o prximo elemento da lista. Nossa estrutura de aluno passa a ser dada por:
struct aluno { int mat; char nome[81]; char turma; char email[41]; struct aluno* prox; /* encadeamento na lista de coliso */ }; typedef struct aluno Aluno;
Na operao de busca, procuramos a ocorrncia do elemento na lista representada no ndice mapeado pela funo de disperso. Uma possvel implementao mostrada a seguir.
Aluno* busca (Hash tab, int mat) { int h = hash(mat); Aluno* a = tab[h]; while (a != NULL) { if (a->mat == mat) return a; a = a->prox; } return NULL; }
A funo que insere ou modifica um determinado elemento tambm simples e pode ser dada por:
17-7
Aluno* insere (Hash tab, int mat, char* nome, char turma) { int h = hash(mat); Aluno* p = NULL; /* ponteiro para anterior */ Aluno* a = tab[h]; while (a != NULL) { if (a->mat == mat) break; p = a; a = a->prox; } if (a==NULL) { /* no encontrou o elemento */ a = (Aluno*) malloc(sizeof(Aluno)); a->mat = mat; a->prox = NULL; if (p==NULL) tab[h] = a; else p->prox = a; } /* atribui informao */ strcpy(a->nome,nome); a->turma = turma; return a; }
Exerccio: Faa um programa que utilize as funes de tabelas de disperso vistas acima.
17-8
Vamos considerar que uma palavra se caracteriza por uma seqncia de uma ou mais letras (maisculas ou minsculas). Para contar o nmero de ocorrncias de cada palavra, podemos armazenar as palavras lidas numa tabela de disperso, usando a prpria palavra como chave de busca. Guardaremos na estrutura de dados quantas vezes cada palavra foi encontrada. Para isso, podemos prever a construo de uma funo que acessa uma palavra armazenada na tabela; se a palavra ainda no existir, a funo armazena uma nova palavra na tabela. Dessa forma, para cada palavra lida, conseguiremos incrementar o nmero de ocorrncias. Para exibir as ocorrncias em ordem decrescente, criaremos um vetor e armazenaremos todas as palavras que existem na tabela de disperso no vetor. Esse vetor pode ento ser ordenado e seu contedo exibido. Tipo dos dados Conforme discutido acima, usaremos uma tabela de disperso para contar o nmero de ocorrncias de cada palavra no texto. Vamos optar por empregar a estratgia que usa lista encadeada para o tratamento de colises. Dessa forma, a dimenso da tabela de disperso no compromete o nmero mximo de palavras distintas (no entanto, a dimenso da tabela no pode ser muito justa em relao ao nmero de elementos armazenados, pois aumentaria o nmero de colises, degradando o desempenho). A estrutura que define a tabela de disperso pode ser dada por:
#define NPAL 64 #define NTAB 127 /* dimenso mxima de cada palavra */ /* dimenso da tabela de disperso */
/* tipo que representa cada palavra */ struct palavra { char pal[NPAL]; int n; struct palavra* prox; /* tratamento de colisao com listas */ }; typedef struct palavra Palavra; /* tipo que representa a tabela de disperso */ typedef Palavra* Hash[NTAB];
Leitura de palavras A primeira funo que vamos discutir responsvel por capturar a prxima seqncia de letras do arquivo texto. Essa funo receber como parmetros o ponteiro para o arquivo de entrada e a cadeia de caracteres que armazenar a palavra capturada. A funo tem como valor de retorno um inteiro que indica se a leitura foi bem sucedida (1) ou no (0). A prxima palavra capturada pulando os caracteres que no so letras e, ento, capturando a seqncia de letras do arquivo. Para identificar se um caractere ou no letra, usaremos a funo isalpha disponibilizada pela interface ctype.h.
17-9
int le_palavra (FILE* fp, char* s) { int i = 0; int c; /* pula caracteres que nao sao letras */ while ((c = fgetc(fp)) != EOF) { if (isalpha(c)) break; }; if (c == EOF) return 0; else s[i++] = c;
/* l os prximos caracteres que so letras */ while ( i<NPAL-1 && (c = fgetc(fp)) != EOF && isalpha(c)) s[i++] = c; s[i] = '\0'; return 1; }
Tabela de disperso com cadeia de caracteres Devemos implementar as funes responsveis por construir e manipular a tabela de disperso. A primeira funo que precisamos responsvel por inicializar a tabela, atribuindo o valor NULL para cada elemento.
void inicializa (Hash tab) { int i; for (i=0; i<NTAB; i++) tab[i] = NULL; }
Tambm precisamos definir uma funo de disperso, responsvel por mapear a chave de busca, uma cadeia de caracteres, em um ndice da tabela. Uma funo de disperso simples para cadeia de caracteres consiste em somar os cdigo dos caracteres que compem a cadeia e tirar o mdulo dessa soma para se obter o ndice da tabela. A implementao abaixo ilustra essa funo.
int hash (char* s) { int i; int total = 0; for (i=0; s[i]!='\0'; i++) total += s[i]; return total % NTAB; }
Precisamos ainda da funo que acessa os elementos armazenados na tabela. Criaremos uma funo que, dada uma palavra (chave de busca), fornece como valor de retorno o ponteiro da estrutura Palavra associada. Se a palavra ainda no existir na tabela, essa funo cria uma nova palavra e fornece como retorno essa nova palavra criada.
17-10
Palavra *acessa (Hash tab, char* s) { int h = hash(s); Palavra* p; for (p=tab[h]; p!=NULL; p=p->prox) { if (strcmp(p->pal,s) == 0) return p; } /* insere nova palavra no inicio da lista */ p = (Palavra*) malloc(sizeof(Palavra)); strcpy(p->pal,s); p->n = 0; p->prox = tab[h]; tab[h] = p; return p; }
Dessa forma, a funo cliente ser responsvel por acessar cada palavra e incrementar o seu nmero de ocorrncias. Transcrevemos abaixo o trecho da funo principal reponsvel por fazer essa contagem (a funo completa ser mostrada mais adiante).
... inicializa(tab); while (le_palavra(fp,s)) { Palavra* p = acessa(tab,s); p->n++; } ...
Com a execuo desse trecho de cdigo, cada palavra encontrada no texto de entrada ser armazenada na tabela, associada ao nmero de vezes de sua ocorrncia. Resta-nos arrumar o resultado obtido para podermos exibir as palavras em ordem decrescente do nmero de ocorrncias. Exibio do resultado ordenado Para colocarmos o resultado na ordem desejada, criaremos dinamicamente um vetor para armazenar as palavras. Optaremos por construir um vetor de ponteiros para a estrutura Palavra. Esse vetor ser ento ordenado em ordem decrescente do nmero de ocorrncias de cada palavra; se duas palavras tiverem o mesmo nmero de ocorrncias, usaremos a ordem alfabtica como critrio de desempate. Para criar o vetor, precisamos conhecer o nmero de palavras armazenadas na tabela de disperso. Podemos implementar uma funo que percorre a tabela e conta o nmero de palavras existentes. Essa funo pode ser dada por:
int conta_elems (Hash tab) { int i; int total = 0; Palavra* p; for (i=0; i<NTAB; i++) { for (p=tab[i]; p!=NULL; p=p->prox) total++; } return total; }
Podemos agora implementar a funo que cria dinamicamente o vetor de ponteiros. Em seguida, a funo percorre os elementos da tabela e preenche o contedo do vetor. Essa
17-11
Para ordenar o vetor (de poteiros para a estrutura Palavra ) utilizaremos a funo qsort da biblioteca padro. Precisamos ento definir a funo de comparao, que mostrada abaixo.
int compara (const void* v1, const void* v2) { Palavra** pp1 = (Palavra**)v1; Palavra** pp2 = (Palavra**)v2; Palavra* p1 = *pp1; Palavra* p2 = *pp2; if (p1->n > p2->n) return -1; else if (p1->n < p2->n) return 1; else return strcmp(p1->pal,p2->pal); }
Por fim, podemos escrever a funo que, dada a tabela de disperso j preenchida e utilizando as funes mostradas acima, conta o nmero de elementos, cria o vetor, ordena-o e exibe o resultado na ordem desejada. Ao final, a funo libera o vetor criado dinamicamente.
void imprime (Hash tab) { int i; int n; Palavra** vet; /* cria e ordena vetor */ n = conta_elems(tab); vet = cria_vetor(n,tab); qsort(vet,n,sizeof(Palavra*),compara); /* imprime ocorrencias */ for (i=0; i<n; i++) printf("%s = %d\n",vet[i]->pal,vet[i]->n); /* libera vetor */ free(vet); }
Funo principal Uma possvel funo principal desse programa mostrada a seguir. Esse programa espera receber como dado de entrada o nome do arquivo cujas palavras queremos contar o nmero de ocorrncias. Para exemplificar a utilizao dos parmetros da funo
17-12
principal, utilizamos esses parmetros para receber o nome do arquivo de entrada (para detalhes, veja seo 6.3).
#include #include #include #include ... <stdio.h> <string.h> <ctype.h> <stdlib.h>
int main (int argc, char** argv) { FILE* fp; Hash tab; char s[NPAL]; if (argc != 2) { printf("Arquivo de entrada nao fornecido.\n"); return 0; } /* abre arquivo para leitura */ fp = fopen(argv[1],"rt"); if (fp == NULL) { printf("Erro na abertura do arquivo.\n"); return 0; } /* conta ocorrencia das palavras */ inicializa(tab); while (le_palavra(fp,s)) { Palavra* p = acessa(tab,s); p->n++; } /* imprime ordenado */ imprime (tab); return 0; }
17-13
Podemos observar que as estruturas dessas funes so as mesmas. Como tambm teriam as mesmas estruturas funes para percorrer os elementos de uma lista encadeada, de uma rvore, etc. Nesses casos, podemos separar a funo que percorre os elementos da ao que realizamos a cada elemento. Assim, a funo que percorre os elementos nica e pode ser usada para diversos fins. A ao que deve ser executada passada como parmetro, via um ponteiro para uma funo. Essa funo usualmente chamada de callback pois uma funo do cliente (quem usa a funo que percorre os elementos) que chamada de volta a cada elemento encontrado na estrutura de dados. Usualmente essa funo callback recebe como parmetro o elemento encontrado na estrutura. No nosso exemplo, como os elementos so ponteiros para a estrutura Palavra , a funo receberia o ponteiro para cada palavra encontrada na tabela. Uma funo genrica para percorrer os elementos da tabela de disperso do nosso exemplo pode ser dada por:
void percorre (Hash tab, void (*cb)(Palavra*) ) { int i; Palavra* p; for (i=0; i<NTAB; i++) { for (p=tab[i]; p!=NULL; p=p->prox) cb(p); } }
Para ilustrar sua utlizao, podemos usar essa funo para imprimir os elementos. Para tanto, devemos escrever a funo que executa a ao de imprimir cada elemento. Essa funo pode ser dada por:
void imprime_elemento (Palavra* p) { printf("%s = %d\n",p->pal,p->n); }
Assim, para imprimir os elementos da tabela bastaria chamar a funo percorre com a ao acima passada como parmetro.
... percorre(tab,imprime_elemento); ...
Essa mesma funo percorre pode ser usada para, por exemplo, contar o nmero de elementos que existe armazenado na tabela. A ao associada aqui precisa apenas incrementar um contador do nmero de vezes que a callback chamada. Para tanto, devemos usar uma varivel global que representa esse contador e fazer a callback incrementar esse contador cada vez que for chamada. Nesse caso, o ponteiro do elemento passado como parmetro para a callback no utilizado, pois o incremento ao contador independe do elemento. Assumindo que Total uma varivel global inicializada com o valor zero, a ao para contar o nmero de elementos dada simplesmente por:
17-14
J mencionamos que o uso de variveis globais deve, sempre que possvel, ser evitado, pois seu uso indiscriminado torna um programa ilegvel e difcil de ser mantido. Para evitar o uso de variveis globais nessas funes callbacks devemos arrumar um meio de transferir, para a funo callback, um dado do cliente. A funo que percorre os elementos no manipula esse dado, apenas o transfere para a funo callback. Como no sabemos a priori o tipo de dado que ser necessrio, definimos a callback recebendo dois parmetros: o elemento sendo visitado e um ponteiro genrico (void*). O cliente chama a funo que percorre os elementos passando como parmetros a funo callback e um ponteiro a ser repassado para essa mesma callback. Vamos exemplificar o uso dessa estratgia re-implementando a funo que percorre os elementos.
void percorre (Hash tab, void (*cb)(Palavra*, void*), void* dado) { int i; Palavra* p; for (i=0; i<NTAB; i++) { for (p=tab[i]; p!=NULL; p=p->prox) cb(p,dado); /* passa para a callback o ponteiro recebido */ } }
Agora, podemos usar essa nova verso da funo para contar o nmero de elementos, sem usar varivel global. Primeiro temos que definir a callback, que, nesse caso, receber um ponteiro para um inteiro que representa o contador.
void conta_elemento (Palavra* p, void* dado) { int *contador = (int*)dado; (*contador)++; }
Por fim, uma funo que conta o nmero de elemento, usando as funes acima, mostrada a seguir.
int total_elementos (Hash tab) { int total = 0; percorre(tab,conta_elemento,&total); return total; }
17-15