Você está na página 1de 202

Apostila de Estruturas de Dados

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

2. Expresses ............................................................................................. 2-1


2.1. Variveis............................................................................................................... 2-1 2.2. Operadores .......................................................................................................... 2-4 2.3. Entrada e sada bsicas....................................................................................... 2-8

3. Controle de fluxo.................................................................................... 3-1


3.1. Decises com if .................................................................................................... 3-1 3.2. Construes com laos ........................................................................................ 3-4 3.3. Seleo ................................................................................................................ 3-8

4. Funes .................................................................................................. 4-1


4.1. Definio de funes............................................................................................ 4-1 4.2. Pilha de execuo ................................................................................................ 4-3 4.3. Ponteiro de variveis............................................................................................ 4-6 4.4. Recursividade....................................................................................................... 4-10 4.5. Variveis estticas dentro de funes ................................................................ 4-11 4.6. Pr-processador e macros .................................................................................. 4-12

5. Vetores e alocao dinmica ................................................................ 5-1


5.1. Vetores ................................................................................................................. 5-1 5.2. Alocao dinmica ............................................................................................... 5-3

6. Cadeia de caracteres ............................................................................. 6-1


6.1. Caracteres............................................................................................................ 6-1 6.2. Cadeia de caracteres (strings) ............................................................................. 6-3 6.3. Vetor de cadeia de caracteres ............................................................................. 6-11

7. Tipos estruturados................................................................................. 7-1


7.1. O tipo estrutura..................................................................................................... 7-1 7.2. Definio de "novos" tipos.................................................................................... 7-4 7.3. Vetores de estruturas ........................................................................................... 7-6 7.4. Vetores de ponteiros para estruturas ................................................................... 7-7 7.5. Tipo unio............................................................................................................. 7-9 7.6. Tipo enumerao ................................................................................................ 7-10

8. Matrizes ................................................................................................... 8-1


8.1. Alocao esttica versus dinmica ...................................................................... 8-1 8.2. Vetores bidimensionais Matrizes....................................................................... 8-2 8.3. Matrizes dinmicas............................................................................................... 8-4 8.4. Representao de matrizes ................................................................................. 8-6 8.5. Representao de matrizes simtricas ................................................................ 8-9

9. Tipos Abstratos de Dados ..................................................................... 9-1

9.1. Mdulos e Compilao em Separado .................................................................. 9-1 9.2. Tipo Abstrato de Dados........................................................................................ 9-3

10. Listas Encadeadas ............................................................................... 10-1


10.1. Lista encadeada ................................................................................................. 10-2 10.2. Implementaes recursivas ............................................................................... 10-9 10.3. Listas genricas ................................................................................................. 10-10 10.4. Listas circulares.................................................................................................. 10-15 10.5. Listas duplamente encadeadas.......................................................................... 10-16

11. Pilhas..................................................................................................... 10-1


11.1. Interface do tipo pilha ......................................................................................... 10-2 11.2. Implementao de pilha com vetor .................................................................... 10-2 11.3. Implementao de pilha com lista ...................................................................... 10-3 11.4. Exemplo de uso: calculadora ps-fixada............................................................ 10-5

12. Filas ....................................................................................................... 11-1


12.1. Interface do tipo fila ............................................................................................ 11-1 12.2. Implementao de fila com vetor ....................................................................... 11-2 12.3. Implementao de fila com lista ......................................................................... 11-5 12.4. Fila dupla............................................................................................................ 11-7 12.5. Implementao de fila dupla com lista ............................................................... 11-8

13. rvores.................................................................................................. 12-1


13.1. rvores binrias ................................................................................................. 12-2 13.2. rvores genricas .............................................................................................. 12-9

14. Arquivos ................................................................................................ 13-1


14.1. Funes para abrir e fechar arquivos ................................................................ 13-2 14.2. Arquivos em modo texto..................................................................................... 13-3 14.3. Estruturao de dados em arquivos textos........................................................ 13-4 14.4. Arquivos em modo binrio.................................................................................. 13-11

15. Ordenao ............................................................................................ 14-1


15.1. Ordenao bolha................................................................................................ 14-1 15.2. Ordenao Rpida ............................................................................................. 14-9

16. Busca .................................................................................................... 16-1


16.1. Busca em Vetor.................................................................................................. 16-1 16.2. rvore binria de busca ..................................................................................... 16-7

17. Tabelas de disperso........................................................................... 17-1


17.1. Idia central........................................................................................................ 17-2 17.2. Funo de disperso.......................................................................................... 17-3 17.3. Tratamento de coliso........................................................................................ 17-4 17.4. Exemplo: Nmero de Ocorrncias de Palavras ................................................. 17-8 17.5. Uso de callbacks ................................................................................................ 17-13

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.

1.2. Modelo de um computador


Existem diversos tipos de computadores. Embora no seja nosso objetivo estudar hardware, identificamos, nesta seo, os elementos essenciais de um computador. O
Estruturas de Dados PUC-Rio 1-1

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

Figura 1.1: Elementos bsicos de um computador tpico.

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.

1.3. Interpretao versus Compilao


Uma diferena importante entre as linguagens C e Scheme que, via de regra, elas so implementadas de forma bastante diferente. Normalmente, Scheme interpretada e C compilada. Para entender a diferena entre essas duas formas de implementao, necessrio lembrar que os computadores s executam realmente programas em sua linguagem de mquina, que especfica para cada modelo (ou famlia de modelos) de computador. Ou seja, em qualquer computador, programas em C ou em Scheme no podem ser executados em sua forma original; apenas programas na linguagem de mquina ( qual vamos nos referir como M) podem ser efetivamente executados. No caso da interpretao de Scheme, um programa interpretador (IM), escrito em M, l o programa PS escrito em Scheme e simula cada uma de suas instrues, modificando os dados do programa da forma apropriada. No caso da compilao da linguagem C, um programa compilador (CM), escrito em M, l o programa PC, escrito em C, e traduz cada uma de suas instrues para M, escrevendo um programa PM cujo efeito o desejado. Como conseqncia deste processo, PM, por ser um programa escrito em M, pode ser executado em qualquer mquina com a mesma linguagem de mquina M, mesmo que esta mquina no possua um compilador. Na prtica, o programa fonte e o programa objeto so armazenados em arquivos em disco, aos quais nos referimos como arquivo fonte e arquivo objeto. As duas figuras a seguir esquematizam as duas formas bsicas de implementao de linguagens de programao.
Execuo PS Programa Fonte

IM Interpretador

Sada

Dados de Entrada

Figura 1.2: Execuo de programas com linguagem interpretada.

Estruturas de Dados PUC-Rio

1-3

Compilao PC Programa Fonte PM Programa Objeto

CM Compilador

Execuo Dados de Entrada PM Programa Objeto

Sada

Figura 1.3: Execuo de programas com linguagem compilada.

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. Exemplo de cdigo em C


Para exemplificar cdigos escritos em C, consideremos um programa que tem a finalidade de converter valores de temperatura dados em Celsius para Fahrenheit. Este programa define uma funo principal que captura um valor de temperatura em Celsius, fornecido via teclado pelo usurio, e exibe como sada a temperatura correspondente em Fahrenheit. Para fazer a converso, utilizada uma funo auxiliar. O cdigo C deste programa exemplo mostrado abaixo.
/* Programa para converso de temperatura */ #include <stdio.h> float converte (float c) { float f; f = 1.8*c + 32; return f; }

Estruturas de Dados PUC-Rio

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.

1.5. Compilao de programas em C


Para desenvolvermos programas em uma linguagem como C, precisamos de, no mnimo, um editor e um compilador. Estes programas tm finalidades bem definidas: com o editor de textos, escrevemos os programas fontes, que so salvos em arquivos1; com o compilador, transformamos os programas fontes em programas objetos, em linguagem de mquina, para poderem ser executados. Os programas fontes so, em geral, armazenados em arquivos cujo nome tem a extenso .c. Os programas executveis possuem extenses que variam com o sistema operacional: no Windows, tm extenso .exe; no Unix (Linux), em geral, no tm extenso. Para exemplificar o ciclo de desenvolvimento de um programa simples, consideremos que o cdigo apresentado na seo anterior tenha sido salvo num arquivo com o nome prog.c. Devemos ento compilar o programa para gerarmos um executvel. Para ilustrar este processo, usaremos o compilador gcc. Na linha de comando do sistema operacional, fazemos:
> gcc o prog prog.c

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).

Estruturas de Dados PUC-Rio

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

Estruturas de Dados PUC-Rio

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.

1.6. Ciclo de desenvolvimento


Programas como editores, compiladores e ligadores so s vezes chamados de ferramentas, usadas na Engenharia de Software. Exceto no caso de programas muito pequenos (como o caso de nosso exemplo), raro que um programa seja composto de um nico arquivo fonte. Normalmente, para facilitar o projeto, os programas so divididos em vrios arquivos. Como vimos, cada um desses arquivos pode ser compilado em separado, mas para sua execuo necessrio reunir os cdigos de todos eles, sem esquecer das bibliotecas necessrias, e esta a funo do ligador. A tarefa das bibliotecas permitir que funes de interesse geral estejam disponveis com facilidade. Nosso exemplo usa a biblioteca de entrada/sada padro do C, stdio, que oferece funes que permitem a captura de dados a partir do teclado e a sada de dados para a tela. Alm de bibliotecas preparadas pelo fornecedor do compilador, ou por outros fornecedores de software, podemos ter bibliotecas preparadas por um usurio qualquer, que pode empacotar funes com utilidades relacionadas em uma biblioteca e, dessa maneira, facilitar seu uso em outros programas. Em alguns casos, a funo do ligador executada pelo prprio compilador. Por exemplo, quando compilamos o primeiro programa prog.c, o ligador foi chamado automaticamente para reunir o cdigo do programa aos cdigos de scanf, printf e de outras funes necessrias execuo independente do programa. Verificao e Validao Outro ponto que deve ser observado que os programas podem conter (e, em geral, contm) erros, que precisam ser identificados e corrigidos. Quase sempre a verificao realizada por meio de testes, executando o programa a ser testado com diferentes valores de entrada. Identificado um ou mais erros, o cdigo fonte corrigido e deve ser novamente verificado. O processo de compilao, ligao e teste se repete at que os resultados dos testes sejam satisfatrios e o programa seja considerado validado. Podemos descrever o ciclo atravs da Figura 1.4.

Editar

Compilar

Ligar

Testar

Figura 1.4: Ciclo de desenvolvimento.

Estruturas de Dados PUC-Rio

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.

Estruturas de Dados PUC-Rio

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

Estruturas de Dados PUC-Rio

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;

Estruturas de Dados PUC-Rio

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

Alguns compiladores exibem uma advertncia quando encontram o cdigo abaixo:


float x; ... x = 12.45;

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;

/* ERRO: b tem lixo */

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.

Estruturas de Dados PUC-Rio

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

resulta no valor 8, a atribuio:


a = 5

Estruturas de Dados PUC-Rio

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);

Salientamos a presena dos parnteses em torno de expr. Assim:


x *= y + 1;

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++;

Estruturas de Dados PUC-Rio

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.

Estruturas de Dados PUC-Rio

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.

Entrada e sada bsicas

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

Estruturas de Dados PUC-Rio

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);

tem como resultado a impresso da linha:


33 5.3

Ou:
printf ("Inteiro = %d Real = %g\n", 33, 5.3);

com sada:
Inteiro = 33 Real = 5.3

Estruturas de Dados PUC-Rio

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:

Estruturas de Dados PUC-Rio

2-10

%c %d %u %f,%e,%g %lf, %le, %lg %s

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.

Estruturas de Dados PUC-Rio

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).

3.1. Decises com if


if o comando de deciso bsico em C. Sua forma pode ser:
if (expr) { bloco de comandos 1 ... }

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

Podemos aninhar comandos if. Um exemplo simples ilustrado a seguir:


#include <stdio.h> int main (void) { int a, b; printf("Insira dois numeros inteiros:"); scanf("%d%d",&a,&b); if (a%2 == 0) if (b%2 == 0) printf("Voce inseriu dois numeros pares!\n"); return 0; }

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;

Estruturas de Dados PUC-Rio

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 no existe neste ponto do programa */

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; ...

Sua forma geral :


condio ? expresso1 : expresso2;

se a condio for verdadeira, a expresso1 avaliada; caso contrrio, avalia-se a expresso2. O comando:
maximo = a > b ? a : b ;

substitui a construo com if-else mostrada acima.

3.2. Construes com laos


muito comum, em programas computacionais, termos procedimentos iterativos, isto , procedimentos que devem ser executados em vrios passos. Como exemplo, vamos considerar o clculo do valor do fatorial de um nmero inteiro no negativo. Por definio:

n != n (n 1) (n 2)...3 2 1, onde 0 != 1

Estruturas de Dados PUC-Rio

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 ... }

Estruturas de Dados PUC-Rio

3-5

A ordem de avaliao desta construo ilustrada a seguir:


expr_inicial; while (expr_booleana) { bloco de comandos ... expr_de_incremento }

A seguir, ilustramos a utilizao do comando for no programa para clculo do fatorial.


/* Fatorial (versao 2) */ #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 */ for (i = 1; i <= n; i++) { f *= i; } printf(" Fatorial = %d \n", f); return 0;

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 {

bloco de comandos } while (expr_booleana);

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).

Estruturas de Dados PUC-Rio

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; }

A sada deste programa, se executado, ser:


0 1 2 3 4 fim

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.

Estruturas de Dados PUC-Rio

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 :

Estruturas de Dados PUC-Rio

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; }

Estruturas de Dados PUC-Rio

3-9

4. Funes
W. Celes e J. L. Rangel

4.1. Definio de funes


As funes dividem grandes tarefas de computao em tarefas menores. Os programas em C geralmente consistem de vrias pequenas funes em vez de poucas de maior tamanho. A criao de funes evita a repetio de cdigo, de modo que um procedimento que repetido deve ser transformado numa funo que, ento, ser chamada diversas vezes. Um programa bem estruturado deve ser pensado em termos de funes, e estas, por sua vez, podem (e devem, se possvel) esconder do corpo principal do programa detalhes ou particularidades de implementao. Em C, tudo feito atravs de funes. Os exemplos anteriores utilizam as funes da biblioteca padro para realizar entrada e sada. Neste captulo, discutiremos a codificao de nossas prprias funes. A forma geral para definir uma funo :
tipo_retornado { corpo da funo } nome_da_funo (lista de parmetros...)

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); }

Estruturas de Dados PUC-Rio

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.2. Pilha de execuo


Apresentada a forma bsica para a definio de funes, discutiremos agora, em detalhe, como funciona a comunicao entre a funo que chama e a funo que chamada. J mencionamos na introduo deste curso que as funes so independentes entre si. As variveis locais definidas dentro do corpo de uma funo (e isto inclui os parmetros das funes) no existem fora da funo. Cada vez que a funo executada, as variveis locais so criadas, e, quando a execuo da funo termina, estas variveis deixam de existir. A transferncia de dados entre funes feita atravs dos parmetros e do valor de retorno da funo chamada. Conforme mencionado, uma funo pode retornar um valor para a funo que a chamou e isto feito atravs do comando return. Quando uma funo tem um valor de retorno, a chamada da funo uma expresso cujo valor resultante o valor retornado pela funo. Por isso, foi vlido escrevermos na funo main acima a expresso r = fat(n); que chama a funo fat armazenando seu valor de retorno na varivel r. A comunicao atravs dos parmetros requer uma anlise mais detalhada. Para ilustrar a discusso, vamos considerar o exemplo abaixo, no qual a implementao da funo fat foi ligeiramente alterada:

Estruturas de Dados PUC-Rio

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

Figura 4.1: Esquema representativo da memria.

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

r main > main > n

fat > main >

n r n

5 5

4 - Declarao da varivel local: f

5 - Final do lao

6 - Retorno da funo: desempilha

f n fat > r n main >

1.0 5 5

f n fat > r n main >

120.0 0 5

main >

r n

120.0 5

Figura 4.2: Execuo do programa passo a passo.

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.

4.3. Ponteiro de variveis


A linguagem C permite o armazenamento e a manipulao de valores de endereos de memria. Para cada tipo existente, h um tipo ponteiro que pode armazenar endereos de memria onde existem valores do tipo correspondente armazenados. Por exemplo, quando escrevemos:
int a;

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;

/*varivel ponteiro p/ inteiro */

int *p;

p a

112 108 104

Figura 4.3: Efeito de declaraes de variveis na pilha de execuo.

Estruturas de Dados PUC-Rio

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

112 108 104

/* p recebe o endereo de a (diz-se p aponta para a) */

p = &a;
/* contedo de p recebe o valor 6 */

p a

104 5

112 108 104

*p = 6;

p a

104 6

112 108 104

Figura 4.4: Efeito de atribuio de variveis na pilha de execuo.

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

Figura 4.5: Representao grfica do valor de um ponteiro.

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.

Estruturas de Dados PUC-Rio

4-7

A seguir, apresentamos outros exemplos de uso de ponteiros. O cdigo abaixo:


int main ( void ) { int a; int *p; p = &a; *p = 2; printf(" %d ", a); return; }

imprime o valor 2. Agora, no exemplo abaixo:


int main ( void ) { int a, b, *p; a = 2; *p = 3; b = a + (*p); printf(" %d ", b); return 0; }

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.

Estruturas de Dados PUC-Rio

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.

Estruturas de Dados PUC-Rio

4-9

1 -Declarao das variveis: a, b

2 - Chamada da funo: passa endereos


120

main

b a >

7 5

112 108 104

troca main

py px >b a >

108 104 7 5

116 112 108 104

3 - Declarao da varivel local: temp


temp py troca main px > b a > 108 104 7 5 120 116 112 108 104

4 - temp recebe contedo de px


temp py px troca >b a main > 5 108 104 7 5 120 116 112 108 104

5 -Contedo de px recebe contedo de py


temp py px troca >b main > a 5 108 104 7 7 120 116 112 108 104

6 -Contedo de py recebe temp


temp py px troca >b main > a 5 108 104 5 7 120 116 112 108 104

Figura 4.6: Passo a passo da funo que troca dois valores.

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); }

4.5. Variveis estticas dentro de funes**


Podemos declarar variveis estticas dentro de funes. Neste caso, as variveis no so armazenadas na pilha, mas sim numa rea de memria esttica que existe enquanto o programa est sendo executado. Ao contrrio das variveis locais (ou automticas), que existem apenas enquanto a funo qual elas pertencem estiver sendo executada, as estticas, assim como as globais, continuam existindo mesmo antes ou depois de a funo ser executada. No entanto, uma varivel esttica declarada dentro de uma funo s visvel dentro dessa funo. Uma utilizao importante de variveis estticas dentro de funes quando se necessita recuperar o valor de uma varivel atribuda na ltima vez que a funo foi executada. Para exemplificar a utilizao de variveis estticas declaradas dentro de funes, consideremos uma funo que serve para imprimir nmeros reais. A caracterstica desta funo que ela imprime um nmero por vez, separando-os por espaos em branco e colocando, no mximo, cinco nmeros por linha. Com isto, do primeiro ao quinto nmero so impressos na primeira linha, do sexto ao dcimo na segunda, e assim por diante.
void imprime ( float a ) { static int n = 1; printf(" %f ", a); if ((n % 5) == 0) printf(" \n "); n++; }

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

4.6. Pr-processador e macros**


Um cdigo C, antes de ser compilado, passa por um pr-processador. O pr-processador de C reconhece determinadas diretivas e altera o cdigo para, ento, envi-lo ao compilador. Uma das diretivas reconhecidas pelo pr-processador, e j utilizada nos nossos exemplos, #include. Ela seguida por um nome de arquivo e o pr-processador a substitui pelo corpo do arquivo especificado. como se o texto do arquivo includo fizesse parte do cdigo fonte. Uma observao: quando o nome do arquivo a ser includo envolto por aspas ("arquivo"), o pr-processador procura primeiro o arquivo no diretrio atual e, caso no o encontre, o procura nos diretrios de include especificados para compilao. Se o arquivo colocado entre os sinais de menor e maior (<arquivo>), o pr-processador no procura o arquivo no diretrio atual. Outra diretiva de pr-processamento que muito utilizada e que ser agora discutida a diretiva de definio. Por exemplo, uma funo para calcular a rea de um crculo pode ser escrita da seguinte forma:
#define PI 3.14159

float area (float r) { float a = PI * r * r; return a; }

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

int main (void) { printf(" %d ", 4 * DIF(5,3)); return 0; }

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)

int main (void) { printf(" %d ", PROD(3+4, 2)); return 0; }

o resultado 11 e no 14. A macro corretamente definida seria:


#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.

Estruturas de Dados PUC-Rio

4-13

5. Vetores e alocao dinmica


W. Celes e J. L. Rangel

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

Figura 5.1: Espao de memria de um vetor de 10 elementos inteiros.

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)

Estruturas de Dados PUC-Rio

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

Uma possvel implementao apresentada a seguir.


/* Clculo da media e da varincia de 10 nmeros reais */ #include <stdio.h> int main ( void ) { float v[10]; float med, var; int i;

/* 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.

Estruturas de Dados PUC-Rio

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; }

A sada do programa 2 4 6, pois os elementos do vetor sero incrementados dentro da funo.

Estruturas de Dados PUC-Rio

5-4

5.2. Alocao dinmica


At aqui, na declarao de um vetor, foi preciso dimension-lo. Isto nos obrigava a saber, de antemo, quanto espao seria necessrio, isto , tnhamos que prever o nmero mximo de elementos no vetor durante a codificao. Este pr-dimensionamento do vetor um fator limitante. Por exemplo, se desenvolvermos um programa para calcular a mdia e a varincia das notas de uma prova, teremos que prever o nmero mximo de alunos. Uma soluo dimensionar o vetor com um nmero absurdamente alto para no termos limitaes quando da utilizao do programa. No entanto, isto levaria a um desperdcio de memria que inaceitvel em diversas aplicaes. Se, por outro lado, formos modestos no pr-dimensionamento do vetor, o uso do programa fica muito limitado, pois no conseguiramos tratar turmas com o nmero de alunos maior que o previsto. Felizmente, a linguagem C oferece meios de requisitarmos espaos de memria em tempo de execuo. Dizemos que podemos alocar memria dinamicamente. Com este recurso, nosso programa para o clculo da mdia e varincia discutido acima pode, em tempo de execuo, consultar o nmero de alunos da turma e ento fazer a alocao do vetor dinamicamente, sem desperdcio de memria.

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

Estruturas de Dados PUC-Rio

Cdigo do Programa

Variveis Globais e Estticas Memria Alocada Dinamicamente

Memria Livre

Pilha

Figura 5.2: Alocao esquemtica de memria.

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));

A figura abaixo ilustra de maneira esquemtica o que ocorre na memria:


1 - Declarao: int *v Abre-se espao na pilha para o ponteiro (varivel local) Cdigo do Programa Variveis Globais e Estticas 2 - Comando: v = (int *) malloc (10*sizeof(int)) Reserva espao de memria da rea livre e atribui endereo varivel Cdigo do Programa Variveis Globais e Estticas
40 bytes

504

Livre

Livre v 504

Figura 5.3: Alocao dinmica de memria.

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.

Estruturas de Dados PUC-Rio

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;

Estruturas de Dados PUC-Rio

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

Alguns caracteres de controle: 0 7 8 9 10 13 127


1

nul bel bs ht nl cr del

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

Estruturas de Dados PUC-Rio

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; }

Estruturas de Dados PUC-Rio

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);

6.2. Cadeia de caracteres (strings)


Cadeias de caracteres (strings), em C, so representadas por vetores do tipo char terminadas, obrigatoriamente, pelo caractere nulo ('\0'). Portanto, para armazenarmos uma cadeia de caracteres, devemos reservar uma posio adicional para o caractere de fim da cadeia. Todas as funes que manipulam cadeias de caracteres (e a biblioteca padro de C oferece vrias delas) recebem como parmetro um vetor de char, isto , um ponteiro para o primeiro elemento do vetor que representa a cadeia, e processam caractere por caractere, at encontrarem o caractere nulo, que sinaliza o final da cadeia. Por exemplo, o especificador de formato %s da funo printf permite imprimir uma cadeia de caracteres. A funo printf ento recebe um vetor de char e imprime elemento por elemento, at encontrar o caractere nulo. O cdigo abaixo ilustra a representao de uma cadeia de caracteres. Como queremos representar a palavra Rio, composta por 3 caracteres, declaramos um vetor com dimenso 4 (um elemento adicional para armazenarmos o caractere nulo no final da cadeia). O cdigo preenche os elementos do vetor, incluindo o caractere '\0', e imprime a palavra na tela.
int main ( void ) { char cidade[4]; cidade[0] = 'R'; cidade[1] = 'i'; cidade[2] = 'o'; cidade[3] = '\0'; printf("%s \n", cidade); return 0; }

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); ...

/* o branco no formato pula brancos da entrada */

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"); }

que teria funcionalidade anloga utilizao do especificador de formato %s.


void imprime (char* s) { printf("%s\n",s); }

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;

Estruturas de Dados PUC-Rio

6-6

O trecho de cdigo abaixo faz uso da funo acima.


#include <stdio.h> int comprimento (char* s); int main (void) { int tam; char cidade[] = "Rio de Janeiro"; tam = comprimento(cidade); printf("A string \"%s\" tem %d caracteres\n", cidade, tam); return 0; }

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);

Estruturas de Dados PUC-Rio

6-7

Uma possvel implementao desta funo mostrada a seguir:


void concatena (char*dest, char* orig) { int i = 0; /* indice usado na cadeia destino, inicializado com zero */ int j; /* indice usado na cadeia origem */ /* acha o final da cadeia destino */ i = 0; while (s[i] != '\0') i++; /* copia elementos da origem para o final do destino */ for (j=0; orig[j] != '\0'; j++) { dest[i] = orig[j]; i++; } /* fecha cadeia destino */ dest[i] = '\0'; }

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);

Uma possvel implementao, usando as funes da biblioteca padro, :


#include <stdlib.h> #include <string.h> char* duplica (char* s) { int n = strlen(s); char* d = (char*) malloc ((n+1)*sizeof(char)); strcpy(d,s); return d; }

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]); }

Estruturas de Dados PUC-Rio

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.

Estruturas de Dados PUC-Rio

6-10

Desta forma, tambm vlido escrever:


int main (void) { char *cidade; /* declara um ponteiro para char */ cidade = "Rio"; /* cidade recebe o endereco da cadeia printf ( "%s \n", cidade ); return 0; }

"Rio" */

Existe uma diferena sutil entre as duas declaraes abaixo:


char s1[] = "Rio de Janeiro"; char* s2 = "Rio de Janeiro";

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.

6.3. Vetor de cadeia de caracteres


Em muitas aplicaes, desejamos representar um vetor de cadeia de caracteres. Por exemplo, podemos considerar uma aplicao que armazene os nomes de todos os alunos de uma turma num vetor. Sabemos que uma cadeia de caracteres representada por um vetor do tipo char. Para representarmos um vetor onde cada elemento uma cadeia de caracteres, devemos ter um vetor cujos elementos so do tipo char*, isto , um vetor de ponteiros para char. Assim, criamos um conjunto (vetor) bidimensional de char. Assumindo que o nome de nenhum aluno ter mais do que 80 caracteres e que o nmero mximo de alunos numa turma 50, podemos declarar um vetor bidimensional para armazenar os nomes dos alunos
char alunos[50][81];

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]); }

Estruturas de Dados PUC-Rio

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]); }

Um programa que faz uso destas funes mostrado a seguir:


int main (void) { char* alunos[MAX]; int n = lenomes(alunos); imprimenomes(n,alunos); liberanomes(n,alunos); return 0; }

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.

7.1. O tipo estrutura


Em C, podemos definir um tipo de dado cujos campos so compostos de vrios valores de tipos mais simples. Para ilustrar, vamos considerar o desenvolvimento de programas que manipulam pontos no plano cartesiano. Cada ponto pode ser representado por suas coordenadas x e y, ambas dadas por valores reais. Sem um mecanismo para agrupar as duas componentes, teramos que representar cada ponto por duas variveis independentes.
float x; float y;

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;

podemos tambm declarar variveis do tipo ponteiro para estrutura:


struct ponto *pp;

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:

Estruturas de Dados PUC-Rio

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); }

Estruturas de Dados PUC-Rio

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; ...

7.2. Definio de "novos" tipos


A linguagem C permite criar nomes de tipos. Por exemplo, se escrevermos:
typedef float Real;

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];

Estruturas de Dados PUC-Rio

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;

Estruturas de Dados PUC-Rio

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.

7.3. Vetores de estruturas


J discutimos o uso de vetores para agrupar elementos dos tipos bsicos (vetores de inteiros, por exemplo)Nesta seo, vamos discutir o uso de vetores de estruturas, isto , vetores cujos elementos so estruturas. Para ilustrar a discusso, vamos considerar o clculo da rea de um polgono plano qualquer delimitado por uma seqncia de n pontos. A rea desse polgono pode ser calculada somando-se as reas dos trapzios formados pelos lados do polgono e o eixo x, conforme ilustra a Figura 7.1.
y pi yi pi+1

yi+1

xi

xi+1

Figura 7.1: Clculo da rea de um polgono.

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

Um exemplo de uso dessa funo mostrado no cdigo abaixo:


int main (void) { Ponto p[3] = {{1.0,1.0},{5.0,1.0},{4.0,3.0}}; printf("area = %f\n",area (3,p)); return 0; }

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.

7.4. Vetores de ponteiros para estruturas


Da mesma forma que podemos declarar vetores de estruturas, podemos tambm declarar vetores de ponteiros para estruturas. O uso de vetores de ponteiros til quando temos que tratar um conjunto elementos complexos. Para ilustrar o uso de estruturas complexas, consideremos um exemplo em que desejamos armazenar uma tabela com dados de alunos. Podemos organizar os dados dos alunos em um vetor. Para cada aluno, vamos supor que sejam necessrias as seguintes informaes: nome: cadeia com at 80 caracteres matricula: nmero inteiro endereo: cadeia com at 120 caracteres telefone: cadeia com at 20 caracteres Para estruturar esses dados, podemos definir um tipo que representa os dados de um aluno:
struct aluno { char nome[81]; int mat; char end[121]; char tel[21]; }; typedef struct aluno Aluno;

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; ...

Estruturas de Dados PUC-Rio

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

Estruturas de Dados PUC-Rio

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.5. Tipo unio**


Em C, uma unio uma localizao de memria que compartilhada por diferentes variveis, que podem ser de tipos diferentes. As unies so usadas quando queremos armazenar valores heterogneos num mesmo espao de memria. A definio de uma unio parecida com a de uma estrutura:
union exemplo { int i; char c; }

Estruturas de Dados PUC-Rio

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.

7.6. Tipo enumerao**


Uma enumerao um conjunto de constantes inteiras com nomes que especifica os valores legais que uma varivel daquele tipo pode ter. uma forma mais elegante de organizar valores constantes. Como exemplo, consideremos a criao de um tipo booleano. Variveis deste tipo podem receber os valores 0 (FALSE) ou 1 (TRUE). Poderamos definir duas constantes simblicas dissociadas e usar um inteiro para representar o tipo booleano:
#define FALSE #define TRUE 0 1

typedef int Bool;

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;

Estruturas de Dados PUC-Rio

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).

Estruturas de Dados PUC-Rio

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.

8.1. Alocao esttica versus dinmica


Antes de tratarmos das construes de matrizes, vamos recapitular alguns conceitos apresentados com vetores. A forma mais simples de declararmos um vetor de inteiros em C mostrada a seguir:
int v[10];

ou, se quisermos criar uma constante simblica para a dimenso:


#define N 10 int v[N];

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

O termo esttico aqui refere-se ao fato de no usarmos alocao dinmica.


8-1

Estruturas de Dados PUC-Rio

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).

8.2. Vetores bidimensionais Matrizes


A linguagem C permite a criao de vetores bidimensionais, declarados estaticamente. Por exemplo, para declararmos uma matriz de valores reais com 4 linhas e 3 colunas, fazemos:
float mat[4][3];

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

Figura 8.1: Alocao dos elementos de uma matriz.

Estruturas de Dados PUC-Rio

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}};

Ou podemos inicializar seqencialmente:


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

Estruturas de Dados PUC-Rio

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];

Com esta definio podemos declarar variveis e parmetros deste tipo:


Matrix4 m; ... void f (..., Matrix4 m, ...); /* declarao de varivel */ /* especificao de parmetro */

8.3. Matrizes dinmicas


As matrizes declaradas estaticamente sofrem das mesmas limitaes dos vetores: precisamos saber de antemo suas dimenses. O problema que encontramos que a linguagem C s permite alocarmos dinamicamente conjuntos unidimensionais. Para trabalharmos com matrizes alocadas dinamicamente, temos que criar abstraes conceituais com vetores para representar conjuntos bidimensionais. Nesta seo, discutiremos duas estratgias distintas para representar matrizes alocadas dinamicamente. Matriz representada por um vetor simples Conceitualmente, podemos representar uma matriz num vetor simples. Reservamos as primeiras posies do vetor para armazenar os elementos da primeira linha, seguidos dos elementos da segunda linha, e assim por diante. Como, de fato, trabalharemos com um vetor unidimensional, temos que criar uma disciplina para acessar os elementos da matriz, representada conceitualmente. A estratgia de endereamento para acessar os elementos a seguinte: se quisermos acessar o que seria o elemento mat[i][j] de uma matriz, devemos acessar o elemento v[i*n+j], onde n representa o nmero de colunas da matriz.
j=2 i=1 a e I b f j c g k d h l

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.

Estruturas de Dados PUC-Rio

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);

8.4. Representao de matrizes


Para exemplificar o uso de matrizes dinmicas, vamos discutir a escolha de um tipo para representar as matrizes e um conjunto de operaes implementadas sobre o tipo escolhido. Podemos considerar, por exemplo, a implementao de funes bsicas, sobre as quais podemos futuramente implementar funes mais complexas, tais como soma, multiplicao e inverso de matrizes. Vamos considerar a implementao das seguintes operaes bsicas: 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. A seguir, mostraremos a implementao dessas operaes usando as duas estratgias para alocar dinamicamente uma matriz, apresentadas na seo anterior. Matriz com vetor simples Usando a estratgia com um vetor simples, o tipo matriz pode ser representado por uma estrutura que guarda a dimenso da matriz e o vetor que armazena os elementos.
struct matriz { int lin; int col; float* v; }; typedef struct matriz Matriz;

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; };

Estruturas de Dados PUC-Rio

8-7

typedef struct matriz Matriz;

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.

Estruturas de Dados PUC-Rio

8-8

8.5. Representao de matrizes simtricas


Em uma matriz simtrica n por n, no h necessidade, no caso de ij, de armazenar ambos os elementos mat[i][j] e mat[j][i], porque os dois tm o mesmo valor. Portanto, basta guardar os valores dos elementos da diagonal e de metade dos elementos restantes por exemplo, os elementos abaixo da diagonal, para os quais i>j. Ou seja, podemos fazer uma economia de espao usado para alocar a matriz. Em vez de n2 valores, podemos armazenar apenas s elementos, sendo s dado por:

(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 */

typedef struct matsim MatSim;

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.

O tipo da matriz pode ser definido por:


struct matsim { int dim; float** v; }; typedef struct matsim MatSim;

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).

Estruturas de Dados PUC-Rio

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.

Estruturas de Dados PUC-Rio

8-11

9. Tipos Abstratos de Dados


R. Cerqueira, W. Celes e J.L. Rangel Neste captulo, discutiremos uma importante tcnica de programao baseada na definio de Tipos Abstratos de Dados (TAD). Veremos tambm como a linguagem C pode nos ajudar na implementao de um TAD, atravs de alguns de seus mecanismos bsicos de modularizao (diviso de um programa em vrios arquivos fontes).

9.1. Mdulos e Compilao em Separado


Como foi visto no captulo 1, um programa em C pode ser dividido em vrios arquivos fontes (arquivos com extenso .c ). Quando temos um arquivo com funes que representam apenas parte da implementao de um programa completo, denominamos esse arquivo de mdulo. Assim, a implementao de um programa pode ser composta por um ou mais mdulos. No caso de um programa composto por vrios mdulos, cada um desses mdulos deve ser compilado separadamente, gerando um arquivo objeto (geralmente um arquivo com extenso .o ou .obj) para cada mdulo. Aps a compilao de todos os mdulos, uma outra ferramenta, denominada ligador, usada para juntar todos os arquivos objeto em um nico arquivo executvel. Para programas pequenos, o uso de vrios mdulos pode no se justificar. Mas para programas de mdio e grande porte, a sua diviso em vrios mdulos uma tcnica fundamental, pois facilita a diviso de uma tarefa maior e mais complexa em tarefas menores e, provavelmente, mais fceis de implementar e de testar. Alm disso, um mdulo com funes C pode ser utilizado para compor vrios programas, e assim poupar muito tempo de programao. Para ilustrar o uso de mdulos em C, considere que temos um arquivo str.c que contm apenas a implementao das funes de manipulao de strings comprimento, copia e concatena vistas no captulo 6. Considere tambm que temos um arquivo prog1.c com o seguinte cdigo:
#include <stdio.h> int comprimento (char* str); void copia (char* dest, char* orig); void concatena (char* dest, char* orig); 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); copia(str, str1); concatena(str, str2); printf("Comprimento da concatenao: %d\n",comprimento(str)); return 0; }

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

copia(str, str1); concatena(str, str2); printf("Comprimento da concatenao: %d\n",comprimento(str)); return 0; }

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.2. Tipo Abstrato de Dados


Geralmente, um mdulo agrupa vrios tipos e funes com funcionalidades relacionadas, caracterizando assim uma finalidade bem definida. Por exemplo, na seo anterior vimos um mdulo com funes para manipulao de cadeias de caracteres. Nos casos em que um mdulo define um novo tipo de dado e o conjunto de operaes para manipular dados desse tipo, falamos que o mdulo representa um tipo abstrato de dados (TAD). Nesse contexto, abstrato significa esquecida a forma de implementao, ou seja, um TAD descrito pela finalidade do tipo e de suas operaes, e no pela forma como est implementado. Podemos, por exemplo, criar um TAD para representar matrizes alocadas dinamicamente. Para isso, criamos um tipo matriz e uma srie de funes que o manipulam. Podemos pensar, por exemplo, em funes que acessem e manipulem os valores dos elementos da matriz. Criando um tipo abstrato, podemos esconder a estratgia de implementao. Quem usa o tipo abstrato precisa apenas conhecer a funcionalidade que ele implementa, no a forma como ele implementado. Isto facilita a manuteno e o re-uso de cdigos. O uso de mdulos e TADs so tcnicas de programao muito importantes. Nos prximos captulos, vamos procurar dividir nossos exemplos e programas em mdulos e usar tipos abstratos de dados sempre que isso for possvel. Antes disso, vamos ver alguns exemplos completos de TADs. Exemplo 1: TAD Ponto Como nosso primeiro exemplo de TAD, vamos considerar a criao de um tipo de dado para representar um ponto no R2. Para isso, devemos definir um tipo abstrato, que denominaremos de Ponto, e o conjunto de funes que operam sobre esse tipo. Neste exemplo, vamos considerar as seguintes operaes: cria: operao que cria um ponto com coordenadas x e y; libera: operao que libera a memria alocada por um ponto; acessa: operao que devolve as coordenadas de um ponto; atribui: operao que atribui novos valores s coordenadas de um ponto; distancia: operao que calcula a distncia entre dois pontos. A interface desse mdulo pode ser dada pelo cdigo a seguir:

Estruturas de Dados PUC-Rio

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 */

Estruturas de Dados PUC-Rio

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.

Estruturas de Dados PUC-Rio

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

/* malloc, free, exit */ /* printf */

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 */

Estruturas de Dados PUC-Rio

9-8

int colunas (Matriz* mat) { return mat->col; }

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.

Estruturas de Dados PUC-Rio

9-9

10. Listas Encadeadas


W. Celes e J. L. Rangel Para representarmos um grupo de dados, j vimos que podemos usar um vetor em C. O vetor a forma mais primitiva de representar diversos elementos agrupados. Para simplificar a discusso dos conceitos que sero apresentados agora, vamos supor que temos que desenvolver uma aplicao que deve representar um grupo de valores inteiros. Para tanto, podemos declarar um vetor escolhendo um nmero mximo de elementos.
#define MAX 1000 int vet[MAX];

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. Lista encadeada


Numa lista encadeada, para cada novo elemento inserido na estrutura, alocamos um espao de memria para armazen-lo. Desta forma, o espao total de memria gasto pela estrutura proporcional ao nmero de elementos nela armazenado. No entanto, no podemos garantir que os elementos armazenados na lista ocuparo um espao de memria contguo, portanto no temos acesso direto aos elementos da lista. Para que seja possvel percorrer todos os elementos da lista, devemos explicitamente guardar o encadeamento dos elementos, o que feito armazenando-se, junto com a informao de cada elemento, um ponteiro para o prximo elemento da lista. A Figura 9.2 ilustra o arranjo da memria de uma lista encadeada. prim
Info1 Info2 Info3

10.1. ULL

Figura 9.2: Arranjo da memria de uma lista encadeada.

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

Figura 9. 3: Insero de um novo elemento no incio da lista.

A seguir, ilustramos um trecho de cdigo que cria uma lista inicialmente vazia e insere nela novos elementos.

Estruturas de Dados PUC-Rio

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

Figura 9.4: Remoo do primeiro elemento da lista.

prim

Info1

Info2

Info3

Figura 9.5: Remoo de um elemento no meio da lista.

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.

Estruturas de Dados PUC-Rio

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 */ } }

Estruturas de Dados PUC-Rio

10-6

Um programa que ilustra a utilizao dessas funes mostrado a seguir.


#include <stdio.h> int main (void) { Lista* l; l = inicializa(); l = insere(l, 23); l = insere(l, 45); l = insere(l, 56); l = insere(l, 78); imprime(l); l = retira(l, 78); imprime(l); l = retira(l, 45); imprime(l); libera(l); return 0; } /* /* /* /* /* /* /* declara uma lista no iniciada */ inicia lista vazia */ insere na lista o elemento 23 */ insere na lista o elemento 45 */ insere na lista o elemento 56 */ insere na lista o elemento 78 */ imprimir: 78 56 45 23 */

/* 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.

Estruturas de Dados PUC-Rio

10-7

prim
Info1 Info2 Info3

10.5. ULL
Novo

Figura 9.6: Insero de um elemento no meio da lista.

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.

Estruturas de Dados PUC-Rio

10-8

10.2. Implementaes recursivas


Uma lista pode ser definida de maneira recursiva. Podemos dizer que uma lista encadeada representada por: uma lista vazia; ou um elemento seguido de uma (sub-)lista. Neste caso, o segundo elemento da lista representa o primeiro elemento da sub-lista. Com base na definio recursiva, podemos implementar as funes de lista recursivamente. Por exemplo, a funo para imprimir os elementos da lista pode ser reescrita da forma ilustrada abaixo:
/* Funo imprime recursiva */ void imprime_rec (Lista* l) { if (vazia(l)) return; /* imprime primeiro elemento */ printf(info: %d\n,l->info); /* imprime sub-lista */ imprime_rec(l->prox); }

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.

Estruturas de Dados PUC-Rio

10-9

void libera_rec (Lista* l) { if (!vazia(l)) { libera_rec(l->prox); free(l); } }

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);

10.3. Listas genricas


Um n de uma lista encadeada contm basicamente duas informaes: o encadeamento e a informao armazenada. Assim, a estrutura de um n para representar uma lista de nmeros inteiros dada por:
struct lista { int info; struct lista *prox; }; typedef struct lista Lista;

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;

Estruturas de Dados PUC-Rio

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

Estruturas de Dados PUC-Rio

10-11

Devemos definir um tipo para cada objeto a ser representado:


struct retangulo { float b; float h; }; typedef struct retangulo Retangulo; struct triangulo { float b; float h; }; typedef struct triangulo Triangulo; struct circulo { float r; }; typedef struct circulo Circulo;

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.

Estruturas de Dados PUC-Rio

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

enum { RET, TRI, CIR };

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; }

Estruturas de Dados PUC-Rio

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.

10.4. Listas circulares


Algumas aplicaes necessitam representar conjuntos cclicos. Por exemplo, as arestas que delimitam uma face podem ser agrupadas por uma estrutura circular. Para esses casos, podemos usar listas circulares. Numa lista circular, o ltimo elemento tem como prximo o primeiro elemento da lista, formando um ciclo. A rigor, neste caso, no faz sentido falarmos em primeiro ou ltimo
1

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

Figura 9.7: Arranjo da memria de uma lista circular.

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.5. Listas duplamente encadeadas**


A estrutura de lista encadeada vista nas sees anteriores caracteriza-se por formar um encadeamento simples entre os elementos: cada elemento armazena um ponteiro para o prximo elemento da lista. Desta forma, no temos como percorrer eficientemente os elementos em ordem inversa, isto , do final para o incio da lista. O encadeamento simples tambm dificulta a retirada de um elemento da lista. Mesmo se tivermos o ponteiro do elemento que desejamos retirar, temos que percorrer a lista, elemento por elemento, para encontrarmos o elemento anterior, pois, dado um determinado elemento, no temos como acessar diretamente seu elemento anterior. Para solucionar esses problemas, podemos formar o que chamamos de listas duplamente encadeadas. Nelas, cada elemento tem um ponteiro para o prximo elemento e um ponteiro para o elemento anterior. Desta forma, dado um elemento, podemos acessar ambos os elementos adjacentes: o prximo e o anterior. Se tivermos um ponteiro para o ltimo elemento da lista, podemos percorrer a lista em ordem inversa, bastando acessar continuamente o elemento anterior, at alcanar o primeiro elemento da lista, que no tem elemento anterior (o ponteiro do elemento anterior vale NULL).

Estruturas de Dados PUC-Rio

10-16

A Figura 9.8 esquematiza a estruturao de uma lista duplamente encadeada.

prim
Info1 Info2 Info3

Figura 9.8: Arranjo da memria de uma lista duplamente encadeada.

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.

Estruturas de Dados PUC-Rio

10-17

prim

Novo

Info1

Info2

Info3

Figura 9.9: Insero de um novo elemento no incio da lista.

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.

Estruturas de Dados PUC-Rio

10-18

Uma implementao da funo para retirar um elemento mostrada a seguir:


/* funo retira: retira elemento da lista */ Lista2* retira (Lista2* l, int v) { Lista2* p = busca(l,v); if (p == NULL) return l; /* no achou o elemento: retorna lista inalterada */ /* retira elemento do encadeamento */ if (l == p) l = p->prox; else p->ant->prox = p->prox; if (p->prox != NULL) p->prox->ant = p->ant; free(p); return l; }

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.

Estruturas de Dados PUC-Rio

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

Figura 10.1: Funcionamento da pilha.

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.

Estruturas de Dados PUC-Rio

10-1

11.1. Interface do tipo pilha


Neste captulo, consideraremos duas implementaes de pilha: usando vetor e usando lista encadeada. Para simplificar a exposio, consideraremos uma pilha que armazena valores reais. Independente da estratgia de implementao, podemos definir a interface do tipo abstrato que representa uma estrutura de pilha. A interface composta pelas operaes que estaro disponibilizadas para manipular e acessar as informaes da pilha. Neste exemplo, vamos considerar a implementao de cinco operaes: criar uma estrutura de pilha; inserir um elemento no topo (push); remover o elemento do topo (pop); verificar se a pilha est vazia; liberar a estrutura de pilha. O arquivo pilha.h, que representa a interface do tipo, pode conter o seguinte cdigo:
typedef struct pilha Pilha; Pilha* cria (void); void push (Pilha* p, float v); float pop (Pilha* p); int vazia (Pilha* p); void libera (Pilha* p);

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.

11.2. Implementao de pilha com vetor


Em aplicaes computacionais que precisam de uma estrutura de pilha, comum sabermos de antemo o nmero mximo de elementos que podem estar armazenados simultaneamente na pilha, isto , a estrutura da pilha tem um limite conhecido. Nestes casos, a implementao da pilha pode ser feita usando um vetor. A implementao com vetor bastante simples. Devemos ter um vetor (vet) para armazenar os elementos da pilha. Os elementos inseridos ocupam as primeiras posies do vetor. Desta forma, se temos n elementos armazenados na pilha, o elemento vet[n-1] representa o elemento do topo. A estrutura que representa o tipo pilha deve, portanto, ser composta pelo vetor e pelo nmero de elementos armazenados.
#define MAX 50

struct pilha { int n; float vet[MAX]; };

Estruturas de Dados PUC-Rio

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); }

11.3. Implementao de pilha com lista


Quando o nmero mximo de elementos que sero armazenados na pilha no conhecido, devemos implementar a pilha usando uma estrutura de dados dinmica, no caso, empregando uma lista encadeada. Os elementos so armazenados na lista e a pilha pode ser representada simplesmente por um ponteiro para o primeiro n da lista.
Estruturas de Dados PUC-Rio 10-3

O n da lista para armazenar valores reais pode ser dado por:


struct no { float info; struct no* prox; }; typedef struct no No;

A estrutura da pilha ento simplesmente:


struct pilha { No* prim; };

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; }

As funes que manipulam a pilha fazem uso dessas funes de lista:


void push (Pilha* p, float v) { p->prim = ins_ini(p->prim,v); } float pop (Pilha* p) { float v; if (vazia(p)) { printf("Pilha vazia.\n"); exit(1); /* aborta programa */ } v = p->prim->info; p->prim = ret_ini(p->prim); return v; }
Estruturas de Dados PUC-Rio 10-4

A pilha estar vazia se a lista estiver vazia:


int vazia (Pilha* p) { return (p->prim==NULL); }

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); }

11.4. Exemplo de uso: calculadora ps-fixada


Um bom exemplo de aplicao de pilha o funcionamento das calculadoras da HP (Hewlett-Packard). Elas trabalham com expresses ps-fixadas, ento para avaliarmos uma expresso como ( 1 - 2 ) * ( 4 + 5 ) podemos digitar 1 2 4 5 + *. O funcionamento dessas calculadoras muito simples. Cada operando empilhado numa pilha de valores. Quando se encontra um operador, desempilha-se o nmero apropriado de operandos (dois para operadores binrios e um para operadores unrios), realiza-se a operao devida e empilha-se o resultado. Deste modo, na expresso acima, so empilhados os valores 1 e 2. Quando aparece o operador -, 1 e 2 so desempilhados e o resultado da operao, no caso -1 (= 1 - 2), colocado no topo da pilha. A seguir, 4 e 5 so empilhados. O operador seguinte, +, desempilha o 4 e o 5 e empilha o resultado da soma, 9 . Nesta hora, esto na pilha os dois resultados parciais, -1 na base e 9 no topo. O operador * , ento, desempilha os dois e coloca -9 (= -1 * 9) no topo da pilha.
Estruturas de Dados PUC-Rio 10-5

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); }

Estruturas de Dados PUC-Rio

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;

/* empilha resultado */ push(c->p,v); /* imprime topo da pilha */ printf(c->f,v); }

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); }

Um programa cliente que faa uso da calculadora mostrado abaixo:


/* Programa para ler expresso e chamar funes da calculadora */ #include <stdio.h> #include "calc.h" int main (void) { char c; float v; Calc* calc; /* cria calculadora com preciso de impresso de duas casas decimais */ calc = cria_calc("%.2f\n"); do { /* le proximo caractere nao branco */ scanf(" %c",&c); /* verifica se e' operador valido */ if (c=='+' || c=='-' || c=='*' || c=='/') { operador(calc,c); } /* devolve caractere lido e tenta ler nmero */ else { ungetc(c,stdin); if (scanf("%f",&v) == 1) operando(calc,v);
Estruturas de Dados PUC-Rio 10-7

} } while (c!='q'); libera_calc(calc); return 0; }

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

digitado pelo usurio digitado pelo usurio

Exerccio: Estenda a funcionalidade da calculadora incluindo novos operadores unrios e binrios (sugesto: ~ como menos unrio, # como raiz quadrada, ^ como exponenciao).

Estruturas de Dados PUC-Rio

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.

12.1. Interface do tipo fila


Antes de discutirmos as duas estratgias de implementao, podemos definir a interface disponibilizada pela estrutura, isto , definir quais operaes sero implementadas para manipular a fila. Mais uma vez, para simplificar a exposio, consideraremos uma estrutura que armazena valores reais. Independente da estratgia de implementao, a interface do tipo abstrato que representa uma estrutura de fila pode ser composta pelas seguintes operaes: criar uma estrutura de fila; inserir um elemento no fim; retirar o elemento do incio; verificar se a fila est vazia; liberar a fila. O arquivo fila.h, que representa a interface do tipo, pode conter o seguinte cdigo:
typedef struct fila Fila; Fila* cria (void); void insere (Fila* f, float v); float retira (Fila* f); int vazia (Fila* f); void libera (Fila* f);

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

Estruturas de Dados PUC-Rio

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.

12.2. Implementao de fila com vetor


Como no caso da pilha, nossa primeira implementao de fila ser feita usando um vetor para armazenar os elementos. Para isso, devemos fixar o nmero mximo N de elementos na fila. Podemos observar que o processo de insero e remoo em extremidades opostas far com que a fila ande no vetor. Por exemplo, se inserirmos os elementos 1.4, 2.2, 3.5, 4.0 e depois retirarmos dois elementos, a fila no estar mais nas posies iniciais do vetor. A Figura 11.1 ilustra a configurao da fila aps a insero dos primeiros quatro elementos e a Figura 11.2 aps a remoo de dois elementos.
0 1 2 3 4 5

1.4 ini

2.2

3.5

4.0 fim

Figura 11.1: Fila aps insero de quatro novos elementos.

3.5 ini

4.0 fim

Figura 11.2: Fila aps retirar dois elementos.

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

Figura 11.3: Fila com incremento circular.

Estruturas de Dados PUC-Rio

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.

Estruturas de Dados PUC-Rio

11-3

A estrutura de fila pode ento ser dada por:


#define N 100 struct fila { int ini, fim; float vet[N]; };

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); }

Estruturas de Dados PUC-Rio

11-4

Finalmente, a funo para liberar a memria alocada pela fila pode ser:
void libera (Fila* f) { free(f); }

12.3. Implementao de fila com lista


Vamos agora ver como implementar uma fila atravs de uma lista encadeada, que ser, como nos exemplos anteriores, uma lista simplesmente encadeada, em que cada n guarda um ponteiro para o prximo n da lista. Como teremos que inserir e retirar elementos das extremidades opostas da lista, que representaro o incio e o fim da fila, teremos que usar dois ponteiros, ini e fim , que apontam respectivamente para o primeiro e para o ltimo elemento da fila. Essa situao ilustrada na figura abaixo: ini fim

Info1

Info2

Info3

Figura 11.4: Estrutura de fila com lista encadeada.

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 estrutura da fila agrupa os ponteiros para o incio e o fim da lista:


struct fila { No* ini; No* fim; };

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; }

A fila estar vazia se a lista estiver vazia:


int vazia (Fila* f) { return (f->ini==NULL); }

Estruturas de Dados PUC-Rio

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); }

Um exemplo simples de utilizao da estrutura de fila apresentado a seguir:


/* Mdulo para ilustrar utilizao da fila */ #include <stdio.h> #include "fila.h" int main (void) { Fila* f = cria(); insere(f,20.0); insere(f,20.8); insere(f,21.2); insere(f,24.3); printf("Primeiro elemento: %f\n", retira(f)); printf("Segundo elemento: %f\n", retira(f)); printf("Configuracao da fila:\n"); imprime(f); libera(f); return 0; }

12.4. Fila dupla


A estrutura de dados que chamamos de fila dupla consiste numa fila na qual possvel inserir novos elementos em ambas as extremidades, no incio e no fim. Conseqentemente, permite-se tambm retirar elementos de ambos os extremos. como se, dentro de uma mesma estrutura de fila, tivssemos duas filas, com os elementos dispostos em ordem inversa uma da outra.
Estruturas de Dados PUC-Rio 11-7

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.

12.5. Implementao de fila dupla com lista


A implementao de uma fila dupla com lista encadeada merece uma discusso mais detalhada. A dificuldade que encontramos reside na implementao da funo para retirar um elemento do final da lista. Todas as outras funes j foram discutidas e poderiam ser facilmente implementadas usando uma lista simplesmente encadeada. No entanto, na lista simplesmente encadeada, a funo para retirar do fim no pode ser implementada de forma eficiente, pois, dado o ponteiro para o ltimo elemento da lista, no temos como acessar o anterior, que passaria a ser o ento ltimo elemento. Para solucionar esse problema, temos que lanar mo da estrutura de lista duplamente encadeada (veja a seo 9.5). Nessa lista, cada n guarda, alm da referncia para o prximo elemento, uma referncia para o elemento anterior: dado o ponteiro de um n, podemos acessar ambos os elementos adjacentes. Este arranjo resolve o problema de acessarmos o elemento anterior ao ltimo. Devemos salientar que o uso de uma lista duplamente encadeada para implementar a fila bastante simples, pois s manipulamos os elementos das extremidades da lista.

Estruturas de Dados PUC-Rio

11-8

O arranjo de memria para implementarmos a fila dupla com lista ilustrado na figura abaixo: ini fim

Info1

Info2

Info3

Figura 11.5: Arranjo da estrutura de fila dupla com lista.

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;

A estrutura da fila dupla agrupa os ponteiros para o incio e o fim da lista:


struct fila2 { No2* ini; No2* fim; };

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; }

Estruturas de Dados PUC-Rio

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.

Figura 13.1: Um exemplo de rvore de diretrio.

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.

13.1. rvores binrias


Um exemplo de utilizao de rvores binrias est na avaliao de expresses. Como trabalhamos com operadores que esperam um ou dois operandos, os ns da rvore para representar uma expresso tm no mximo dois filhos. Nessa rvore, os ns folhas representam operandos e os ns internos operadores. Uma rvore que representa, por exemplo a expresso (3+6)*(4-1)+5 ilustrada na Figura 13.3. +

* + 3 6 4 1

Figura 13.3: rvore da expresso: (3+6) * (4-1) + 5.

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.

Estruturas de Dados PUC-Rio

12-2

raiz
vazia

sae

sad

Figura 13.4: Representao esquemtica da definio da estrutura de rvore binria.

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

Figura 13.5: Exemplo de rvore binria

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

Figura 13.6: Duas rvores binrias distintas.

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:

Estruturas de Dados PUC-Rio

12-4

Arv* inicializa(void) { return NULL; }

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:

Estruturas de Dados PUC-Rio

12-5

int vazia(Arv* a) { return a==NULL; }

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); } }

/* mostra raiz */ /* mostra sae */ /* mostra sad */

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; }

/* libera sae */ /* libera sad */ /* libera raiz */

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()) ) );

Podemos acrescentar alguns ns, com:


a->esq->esq = cria('x', cria('y',inicializa(),inicializa()), cria('z',inicializa(),inicializa()) );

E podemos liberar alguns outros, com:


a->dir->esq = libera(a->dir->esq);

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:

Estruturas de Dados PUC-Rio

12-7

if (c==a->info) return 1; else if (busca(a->esq,c)) return 1; else return busca(a->dir,c);

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

13.2. rvores genricas


Nesta seo, vamos discutir as estruturas conhecidas como rvores genricas. Como vimos, numa rvore binria o nmero de filhos dos ns limitado em no mximo dois. No caso da rvore genrica, esta restrio no existe. Cada n pode ter um nmero arbitrrio de filhos. Essa estrutura deve ser usada, por exemplo, para representar uma rvore de diretrios. Como veremos, as funes para manipularem uma rvore genrica tambm sero implementadas de forma recursiva, e sero baseadas na seguinte definio: uma rvore genrica composta por: um n raiz; e zero ou mais sub-rvores. Estritamente, segundo essa definio, uma rvore no pode ser vazia, e a rvore vazia no sequer mencionada na definio. Assim, uma folha de uma rvore no um n com sub-rvores vazias, como no caso da rvore binria, mas um n com zero subrvores. Em qualquer definio recursiva deve haver uma condio de contorno, que permita a definio de estruturas finitas, e, no nosso caso, a definio de uma rvore se encerra nas folhas, que so identificadas como sendo ns com zero sub-rvores. Como as funes que implementaremos nesta seo sero baseadas nessa definio, no ser considerado o caso de rvores vazias. Esta pequena restrio simplifica as implementaes recursivas e, em geral, no limita a utilizao da estrutura em aplicaes reais. Uma rvore de diretrio, por exemplo, nunca vazia, pois sempre existe o diretrio base o diretrio raiz. Como as sub-rvores de um determinado n formam um conjunto linear e so dispostas numa determinada ordem, faz sentido falarmos em primeira sub-rvore (sa1), segunda sub-rvore (sa2), etc. Um exemplo de uma rvore genrica ilustrado na Figura 13.7.

Figura 13.7: Exemplo de rvore genrica.

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.

Estruturas de Dados PUC-Rio

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.

Estruturas de Dados PUC-Rio

12-10

Figura 13.8: rvore com no mximo trs filhos por n.

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.

Estruturas de Dados PUC-Rio

12-11

Figura 13.9: Exemplo usando lista de filhos.

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);

Estruturas de Dados PUC-Rio

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); }

Exerccio: Escreva uma funo com o prottipo


ArvGen* copia(ArvGen*a);

para criar dinamicamente uma cpia da rvore. Exerccio: Escreva uma funo com o prottipo
int igual(ArvGen*a, ArvGen*b);

para testar se duas rvores so iguais.

Estruturas de Dados PUC-Rio

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.

14.1. Funes para abrir e fechar arquivos


A funo bsica para abrir um arquivo fopen:
FILE* fopen (char* nome_arquivo, char* modo);

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

Indica modo binrio.

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.

14.2. Arquivos em modo texto


Nesta seo, vamos descrever as principais funes para manipular arquivos em modo texto. Tambm discutiremos algumas estratgias para organizao de dados em arquivos. Funes para ler dados A principal funo de C para leitura de dados em arquivos em modo texto a funo fscanf, similar funo scanf que temos usado para capturar valores entrados via o teclado. No caso da fscanf , os dados so capturados de um arquivo previamente aberto para leitura. A cada leitura, os dados correspondentes so transferidos para a memria e o ponteiro do arquivo avana, passando a apontar para o prximo dado do arquivo (que pode ser capturado numa leitura subseqente). O prottipo da funo fscanf :
int fscanf (FILE* fp, char* formato, ...);

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, ...);

A funo fputc escreve um caractere no arquivo. O prottipo :


int fputc (int c, FILE* fp);

O valor de retorno dessa funo o prprio caractere escrito, ou EOF se ocorrer um erro na escrita.

14.3. Estruturao de dados em arquivos textos


Existem diferentes formas para estruturarmos os dados em arquivos em modo texto, e diferentes formas de capturarmos as informaes contidas neles. A forma de estruturar e

Estruturas de Dados PUC-Rio

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;

/* contador do nmero de linhas */

/* 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:

Estruturas de Dados PUC-Rio

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;

/* /* /* /*

armazena armazena ponteiro ponteiro

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);

Estruturas de Dados PUC-Rio

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

c 5.1 Figura 14.1: Exemplo de formatao por linha.

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 */

Estruturas de Dados PUC-Rio

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; } } } ...

retngulo: base = v1, altura = v2 */

de formato do arquivo */

tringulo: base = v1, altura = v2 */

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.

Estruturas de Dados PUC-Rio

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

Figura 14.2: Formato com uso de palavras chaves.

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 */ } }

Estruturas de Dados PUC-Rio

13-10

14.4. Arquivos em modo binrio


Arquivos em modo binrio servem para salvarmos (e depois recuperarmos) o contedo da memria principal diretamente no disco. A memria escrita copiando-se o contedo de cada byte da memria para o arquivo. Uma das grandes vantagens de se usar arquivos binrios que podemos salvar (e recuperar) uma grande quantidade de dados de forma bastante eficiente. Neste curso, vamos apenas apresentar as duas funes bsicas para manipulao de arquivos binrios. Funo para salvar e recuperar Para escrever (salvar) dados em arquivos binrios, usamos a funo fwrite . O prottipo dessa funo pode ser simplificado por1:
int fwrite (void* p, int tam, int nelem, FILE* fp);

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

A funo para recuperar os dados salvos pode ser:


void carrega (char* arquivo, int n, Ponto* vet) { FILE* fp = fopen(arquivo,"rb"); if (fp==NULL) { printf("Erro na abertura do arquivo.\n"); exit(1); } fread(vet,sizeof(Ponto),n,fp); fclose(fp); }

Estruturas de Dados PUC-Rio

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.

15.1. Ordenao bolha


O algortimo de ordenao bolha, ou bubble sort, recebeu este nome pela imagem pitoresca usada para descrev-lo: os elementos maiores so mais leves, e sobem como bolhas at suas posies corretas. A idia fundamental fazer uma srie de comparaes entre os elementos do vetor. Quando dois elementos esto fora de ordem,
Estruturas de Dados PUC-Rio 14-1

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

Seguimos os passos indicados:


25 25 25 25 25 25 25 25 48 48 37 37 37 37 37 37 37 37 48 12 12 12 12 12 12 12 12 48 48 48 48 48 57 57 57 57 57 57 57 57 86 86 86 86 86 86 33 33 33 33 33 33 33 33 86 86 92 92 92 92 92 92 92 92 25x48 48x37 48x12 48x57 57x86 86x33 86x92 final troca troca troca da primeira passada

Neste ponto, o maior elemento, 92, j est na sua posio final.


25 25 25 25 25 25 25 37 37 12 12 12 12 12 12 12 37 37 37 37 37 48 48 48 48 48 48 48 57 57 57 57 57 33 33 33 33 33 33 33 57 57 86 86 86 86 86 86 86 92 92 92 92 92 92 92 25x37 37x12 troca 37x48 48x57 57x33 troca 57x86 final da segunda passada

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

Idem para 57.


12 12 12 12 12 25 25 25 25 25 37 37 37 33 33 33 33 33 37 37 48 48 48 48 48 57 57 57 57 57 86 86 86 86 86 92 92 92 92 92 12x25 25x37 37x33 troca 37x48 final da quarta passada

Idem para 48.


12 12 12 12 25 25 25 25 33 33 33 33 37 37 37 37 48 48 48 48 57 57 57 57 86 86 86 86 92 92 92 92 12x25 25x33 33x37 final da quinta passada

Estruturas de Dados PUC-Rio

14-2

Idem para 37.


12 25 33 37 48 57 86 92 12 25 33 37 48 57 86 92 12 25 33 37 48 57 86 92 12x25 25x33 final da sexta passada

Idem para 33.


12 25 33 37 48 57 86 92 12 25 33 37 48 57 86 92 12x25 final da stima passada

Idem para 25 e, consequentemente, 12.


12 25 33 37 48 57 86 92 final da ordenao

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:

Estruturas de Dados PUC-Rio

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.

Estruturas de Dados PUC-Rio

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; }

Consideremos agora um vetor de ponteiros para a estrutura Aluno:


struct aluno { char nome[81]; char mat[8]; char turma; char email[41]; };

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);

A funo de ordenao por sua vez, recebe dois ponteiros genricos:


int compara (void* a, void* b);

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:

Estruturas de Dados PUC-Rio

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.

Estruturas de Dados PUC-Rio

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:

Estruturas de Dados PUC-Rio

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; }

Podemos, ento, chamar a funo para ordenar um vetor v de n nmeros reais:


... buble_gen(n,v,sizeof(float),compara_reais); ...

15.2. Ordenao Rpida


Assim como o algoritmo anterior, o algoritmo ordenao rpida, quick sort, que iremos discutir agora, procura resolver o problema da ordenao por partes. No entanto, enquanto o algoritmo de ordenao bolha coloca em sua posio (no final do vetor) o maior elemento, a ordenao rpida faz isso com um elemento arbitrrio x, chamado de piv. Por exemplo, podemos escolher como piv o primeiro elemento do vetor, e posicionar esse elemento em sua correta posio numa primeira passada. Suponha que este elemento, x , deva ocupar a posio i do vetor, de acordo com a ordenao, ou seja, que essa seja a sua posio definitiva no vetor. Sem ordenar o vetor completamente, este fato pode ser reconhecido quando todos os elementos v[0], v[i-1] so menores que x, e todos os elementos v[i+1], , v[n-1] so maiores que x . Supondo que x j est na sua posio correta, com ndice i , h dois problemas menores para serem resolvidos: ordenar os (sub-) vetores formados por v[0], v[i1] e por v[i+1], , v[n-1]. Esses sub-problemas so resolvidos (recursivamente) de forma semelhante, cada vez com vetores menores, e o processo continua at que os vetores que devem ser ordenados tenham zero ou um elementos, caso em que sua ordenao j est concluda. A grande vantagem desse algoritmo que ele pode ser muito eficiente. O melhor caso ocorre quando o elemento piv representa o valor mediano do conjunto dos elementos do vetor. Se isto acontece, aps o posicionamento do piv em sua posio, restar dois sub-vetores para serem ordenados, ambos com o nmero de elementos reduzido a metade, em relao ao vetor original. Pode-se mostrar que, neste melhor caso, o esforo computacional do algoritmo proporcional a n log(n), e dizemos que o algoritmo O(n log(n)). Um desempenho muito superior ao O(n2) apresentado pelo algoritmo de ordenao bolha. Infelizmente, no temos como garantir que o piv seja o mediano. No pior caso, o piv pode sempre ser, por exemplo, o maior elemento, e recamos no algoritmo de ordenao bolha. No entanto, mostra-se que o algoritmo quicksort ainda apresenta, no caso mdio, um desempenho O(n log(n)). A verso do quicksort que vamos apresentar aqui usa x=v[0] como primeiro elemento a ser colocado em sua posio correta. O processo compara os elementos v[1], v[2], at encontrar um elemento v[a]>x. Ento, a partir do final do vetor, compara os elementos v[n-1] , v[n-2] , at encontrar um elemento v[b]<=x .
Estruturas de Dados PUC-Rio 14-9

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

Estruturas de Dados PUC-Rio

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

Trocamos os elementos e atualizamos os ndices:


(2-7) 37 33 57 86 48 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.

Estruturas de Dados PUC-Rio

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)

enquanto que para deslocar o ndice b para a esquerda, fizemos apenas:


while (v[b] > 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 :

Estruturas de Dados PUC-Rio

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; }

Estruturas de Dados PUC-Rio

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); }

Estruturas de Dados PUC-Rio

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.

16.1. Busca em Vetor


Nesta seo, apresentaremos os algoritmos de busca em vetor. Dado um vetor vet com n elementos, desejamos saber se um determinado elemento elem est ou no presente no vetor. Busca linear A forma mais simples de fazermos uma busca num vetor consiste em percorrermos o vetor, elemento a elemento, verificando se o elemento de interesse igual a um dos elementos do vetor. Esse algoritmo pode ser implementado conforme ilustrado pelo cdigo a seguir, considerando-se um vetor de nmeros inteiros. A funo apresentada tem como valor de retorno o ndice do vetor no qual foi encontrado o elemento; se o elemento no for encontrado, o valor de retorno 1.
int busca (int n, int* vet, int elem) { int i; for (i=0; i<n; i++) { if (elem == vet[i]) return i; /* elemento encontrado */ } /* percorreu todo o vetor e no encontrou elemento */ return -1; }

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; }

/* elemento encontrado */ /* interrompe busca */

/* percorreu todo o vetor e no encontrou elemento */ 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.

Estruturas de Dados PUC-Rio

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).

Estruturas de Dados PUC-Rio

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; }

Estruturas de Dados PUC-Rio

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.2. rvore binria de busca


Como vimos, o algoritmo de busca binria apresentado na seo anterior apresenta bom desempenho computacional e deve ser usado quando temos os dados ordenados armazenados num vetor. No entanto, se precisarmos inserir e remover elementos da estrutura, e ao mesmo tempo dar suporte a eficientes funes de busca, a estrutura de vetor (e, conseqentemente, o uso do algoritmo de busca binria) no se torna adequada. Para inserirmos um novo elemento num vetor ordenado, temos que rearrumar os elementos no vetor, para abrir espao para a insero do novo elemento. Situao anloga ocorre quando removemos um elemento do vetor. Precisamos portanto de uma estrutura dinmica que d suporte a operaes de busca. Um dos resultados que apresentamos anteriormente foi o da relao entre o nmero de ns de uma rvore binria e sua altura. A cada nvel, o nmero (potencial) de ns vai dobrando, de maneira que uma rvore binria de altura h pode ter um nmero de ns dado por: 1 + 2 + 22 + + 2h-1 + 2h = 2h+1-1 Assim, dizemos que uma rvore binria de altura h pode ter no mximo O(2h) ns, ou, pelo outro lado, que uma rvore binria com n ns pode ter uma altura mnima de O(log n). Essa relao entre o nmero de ns e a altura mnima da rvore importante porque se as condies forem favorveis, podemos alcanar qualquer um dos n ns de uma rvore binria a partir da raiz em, no mximo, O(log n) passos. Se tivssemos os n ns em uma lista linear, o nmero mximo de passos seria O(n), e, para os valores de n encontrados na prtica, log n muito menor do que n. A altura de uma rvore , certamente, uma medida do tempo necessrio para encontrar um dado n. No entanto, importante observar que para acessarmos qualquer n de maneira eficiente necessrio termos rvores binrias balanceadas, em que os ns internos tm todos, ou quase todos, o mximo nmero de filhos, no caso 2. Lembramos que o nmero mnimo de ns de uma rvore binria de altura h h+1, de forma que a altura mxima de uma rvore com n ns O(n). Esse caso extremo corresponde rvore degenerada, em que todos os ns tm apenas 1 filho, com exceo da (nica) folha. As rvores binrias que sero consideradas nesta seo tm uma propriedade fundamental: o valor associado raiz sempre maior que o valor associado a qualquer n da sub-rvore esquerda (sae), e sempre menor que o valor associado a qualquer n da sub-rvore direita (sad). Essa propriedade garante que, quando a rvore percorrida em ordem simtrica (sae - raiz - sad), os valores so encontrados em ordem crescente. Uma variao possvel permite que haja repetio de valores na rvore: o valor associado raiz sempre maior que o valor associado a qualquer n da sae, e sempre menor ou igual ao valor associado a qualquer n da sad. Nesse caso, como a repetio de valores permitida, quando a rvore percorrida em ordem simtrica, os valores so encontrados em ordem no decrescente.

Estruturas de Dados PUC-Rio

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

O tipo da rvore binria pode ento ser dado por:


struct arv { int info; struct arv* esq; struct arv* dir; }; typedef struct arv Arv;

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; }

Estruturas de Dados PUC-Rio

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; }

Estruturas de Dados PUC-Rio

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

Figura 16.2: Retirada de um elemento com um nico filho.

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.

Estruturas de Dados PUC-Rio

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.

Estruturas de Dados PUC-Rio

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.

Estruturas de Dados PUC-Rio

16-12

17. Tabelas de disperso


W. Celes e J. L. Rangel

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.

17.1. Idia central


A idia central por trs de uma tabela de disperso identificar, na chave de busca, quais as partes significativas. Na PUC-Rio, por exemplo, alm do dgito de controle, alguns outros dgitos do nmero de matrcula tm significados especiais, conforme ilustra a Figura 17.1. 9711234-4
indicadores seqenciais perodo de ingresso ano de ingresso

Figura 17.1: Significado dos dgitos do nmero da matrcula.

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.

17.2. Funo de disperso


A funo de disperso (funo de hash) mapeia uma chave de busca num ndice da tabela. Por exemplo, no caso exemplificado acima, adotamos como funo de hash a utilizao dos dois ltimos dgitos do nmero de matrcula. A implementao dessa funo recebe como parmetro de entrada a chave de busca e retorna um ndice da tabela. No caso d a chave de busca ser um inteiro representando o nmero de matrcula, essa funo pode ser dada por.
int hash (int mat) { return (mat%100); }

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

Estruturas de Dados PUC-Rio

17-3

75%. Uma taxa de 50% em geral traz bons resultados. Uma taxa menor que 25% pode representar um gasto excessivo de memria.

17.3. Tratamento de coliso


Existem diversas estratgias para tratarmos as eventuais colises que surgem quando duas ou mais chaves de busca so mapeadas para um mesmo ndice da tabela de hash. Nesta seo, vamos apresentar algumas dessas estratgias comumente usadas. Para cada uma das estratgias, vamos apresentar as duas principais funes de manipulao de tabelas de disperso: a funo que busca um elemento na tabela e a funo que insere ou modifica um elemento. Nessas implementaes, vamos considerar a existncia da funo de disperso que mapeia o nmero de matrcula num ndice da tabela, vista na seo anterior. Em todas as estratgias, a tabela de disperso em si representada por um vetor de ponteiros para a estrutura que representa a informao a ser armazenada, no caso Aluno. Podemos definir um tipo que representa a tabela por:
#define N 100 typedef Aluno* Hash[N];

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

Figura 17.2: Tratamento de colises usando prxima 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

Estruturas de Dados PUC-Rio

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.

Estruturas de Dados PUC-Rio

17-6

h(x)

Figura 17.3: Tratamento de colises com lista encadeada.

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:

Estruturas de Dados PUC-Rio

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.4. Exemplo: Nmero de Ocorrncias de Palavras


Para exemplificar o uso de tabelas de disperso, vamos considerar o desenvolvimento de um programa para exibir quantas vezes cada palavra foi utilizada em um dado texto. A sada do programa ser uma lista de palavras, em ordem decrescente do nmero de vezes que cada palavra ocorre no texto de entrada. Para simplificar, no consideraremos caracteres acentuados. Projeto: Dividir para conquistar A melhor estratgia para desenvolvermos programas dividirmos um problema grande em diversos problemas menores. Uma aplicao deve ser construda atravs de mdulos independentes. Cada mdulo projetado para a realizao de tarefas especficas. Um segundo mdulo, que cliente, no precisa conhecer detalhes de como o primeiro foi implementado; o cliente precisa apenas saber a funcionalidade oferecida pelo mdulo que oferece os servios. Dentro de cada mdulo, a realizao da tarefa dividida entre vrias pequenas funes. Mais uma vez, vale a mesma regra de encapsulamento: funes clientes no precisam conhecer detalhes de implementao das funes que oferecem os servios. Dessa forma, aumentamos o potencial de re-uso do cdigo e facilitamos o entendimento e a manuteno do programa. O programa para contar o uso das palavras um programa relativamente simples, que no precisa ser subdividido em mdulos para ser construdo. Aqui, vamos projetar o programa identificando as diversas funes necessrias para a construo do programa como um todo. Cada funo tem sua finalidade especfica e o programa principal (a funo main) far uso dessas funes.

Estruturas de Dados PUC-Rio

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.

Estruturas de Dados PUC-Rio

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;

/* primeira letra j foi capturada */

/* 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.

Estruturas de Dados PUC-Rio

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

Estruturas de Dados PUC-Rio

17-11

funo recebe como parmetros de entrada o nmero de elementos e a tabela de disperso.


Palavra** cria_vetor (int n, Hash tab) { int i, j=0; Palavra* p; Palavra** vet = (Palavra**) malloc(n*sizeof(Palavra*)); /* percorre tabela preenchendo vetor */ for (i=0; i<NTAB; i++) { for (p=tab[i]; p!=NULL; p=p->prox) vet[j++] = p; } return vet; }

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

Estruturas de Dados PUC-Rio

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>

/* funes auxiliares mostradas acima */

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.5. Uso de callbacks **


No programa da seo anterior, precisamos implementar duas funes que percorrem os elementos da tabela de disperso: uma para contar o nmero de elementos e outra para preencher o vetor. Em todas as estruturas de dados, muito comum necessitarmos de funes que percorrem os elementos, executando uma ao especfica para cada elemento. Como um exemplo adicional, podemos imaginar uma funo para imprimir os elementos na ordem em que eles aparecem na tabela de disperso. Ainda usando o exemplo da seo anterior, poderamos ter:
void imprime_tabela (Hash tab) { int i; Palavra* p; for (i=0; i<NTAB; i++) { for (p=tab[i]; p!=NULL; p=p->prox) printf("%s = %d\n",p->pal,p->n); } }

Estruturas de Dados PUC-Rio

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:

Estruturas de Dados PUC-Rio

17-14

void conta_elemento (Palavra* p) { Total++; }

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; }

Estruturas de Dados PUC-Rio

17-15

Você também pode gostar