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

Memria

Armazenamento
secundrio

Dispositivos de
entrada/sada

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

CM
Compilador

PM
Programa
Objeto

PM
Programa
Objeto

Sada

Execuo
Dados de
Entrada

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.

Estruturas de Dados PUC-Rio

2-1

A linguagem oferece ainda dois tipos bsicos para a representao de nmeros reais (ponto
flutuante): float e double. A tabela abaixo compara estes dois tipos.
Tipo
float
double

Tamanho
4 bytes
8 bytes

Representatividade
-38
38
10 a 10
-308
308
10
a 10

Declarao de variveis
Para armazenarmos um dado (valor) na memria do computador, devemos reservar o
espao correspondente ao tipo do dado a ser armazenado. A declarao de uma varivel
reserva um espao na memria para armazenar um dado do tipo da varivel e associa o
nome da varivel a este espao de memria.
int a;
int b;
float c;

/* declara uma varivel do tipo int */


/* declara outra varivel do tipo int */
/* declara uma varivel do tipo float */

a = 5;
b = 10;
c = 5.3;

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

2.3.

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

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

%c
%d
%u
%f
%e
%g
%s

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

Estruturas de Dados PUC-Rio

Real = 5.3

2-9

Isto , alm dos especificadores de formato, podemos incluir textos no formato, que so
mapeados diretamente para a sada. Assim, a sada formada pela cadeia de caracteres do
formato onde os especificadores so substitudos pelos valores correspondentes.
Existem alguns caracteres de escape que so freqentemente utilizados nos formatos de
sada. So eles:
\n
\t
\r
\"
\\

caractere de nova linha


caractere de tabulao
caractere de retrocesso
o caractere "
o caractere \

Ainda, se desejarmos ter como sada um caractere %, devemos, dentro do formato, escrever
%%.
possvel tambm especificarmos o tamanho dos campos:
%4d

3 3
4

%7.2f

5 .

3 0
2

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

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

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

/* obs: existe ; no prottipo */

int main (void)


{
. . .
}
void fat (int n)
{
. . .
}

/* obs: nao existe ; na definio */

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

main >

2 - Declarao das variveis: n, r

main >

4 - Declarao da varivel local: f

f
n
fat >
r
n
main >

1.0
5
5

5 - Final do lao

f
n
fat >
r
n
main >

120.0
0
5

3 - Chamada da funo: cpia do parmetro

fat >
main >

n
r

5
-

6 - Retorno da funo: desempilha

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 recebe o endereo de a
(diz-se p aponta para a) */

p = &a;

p
a

112
108
104

p
a

104
5

112
108
104

p
a

104
6

112
108
104

/* contedo de p recebe o valor 6 */

*p = 6;

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

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

3 - Declarao da varivel local: temp

troca
main

temp
py

108

px
>
b
a
>

104

116
112

7
5

108
104

120

5 -Contedo de px recebe contedo de py


temp
py
px
troca
>b
main

>

5
108
104

120

7
7

108
104

116
112

py
px
>b
a
>

main

108
104
7
5

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

6 -Contedo de py recebe temp


temp
py
px
troca
>b
main

>

5
108
104

120

5
7

108
104

116
112

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]

Estruturas de Dados PUC-Rio

est ERRADO (invaso de memria)

5-1

Para exemplificar o uso de vetores, vamos considerar um programa que l 10 nmeros


reais, fornecidos via teclado, e calcula a mdia e a varincia destes nmeros. A mdia e a
varincia so dadas por:
x
m= ,
N

(x m )
v=

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

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

/* faz ndice variar de 0 a 9 */


/* l cada elemento do vetor */
/* inicializa mdia com zero */
/* acumula soma dos elementos */
/* calcula a mdia */

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

Estruturas de Dados PUC-Rio

5-5

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)

2 - Comando: v = (int *) malloc (10*sizeof(int))


Reserva espao de memria da rea livre
e atribui endereo varivel

Cdigo do
Programa

Cdigo do
Programa

Variveis
Globais e Estticas

Variveis
Globais e Estticas
40 bytes

Livre

504

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

)
3
=
G
Q
[
e
o
y

sp
*
4
>
H
R
\
f
p
z

!
+
5
?
I
S
]
g
q
{

"
,
6
@
J
T
^
h
r
|

#
7
A
K
U
_
i
S
}

$
.
8
B
L
V
`
j
t
~

%
/
9
C
M
W
a
k
u

&
0
:
D
N
X
b
l
v

'
1
;
E
O
Y
c
m
w

Alguns caracteres de controle:


0
7
8
9
10
13
127

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

Estruturas de Dados PUC-Rio

6-1

Em C, a diferena entre caracteres e inteiros feita apenas atravs da maneira pela qual so
tratados. Por exemplo, podemos imprimir o mesmo valor de duas formas diferentes usando
formatos diferentes. Vamos analisar o fragmento de cdigo abaixo:
char c = 97;
printf("%d %c\n",c,c);

Considerando a codificao de caracteres via tabela ASCII, a varivel c, que foi


inicializada com o valor 97, representa o caractere a. A funo printf imprime o
contedo da varivel c usando dois formatos distintos: com o especificador de formato
para inteiro, %d, ser impresso o valor do cdigo numrico, 97; com o formato de caractere,
%c, ser impresso o caractere associado ao cdigo, a letra a.
Conforme mencionamos, devemos evitar o uso explcito de cdigos de caracteres. Para
tanto, a linguagem C permite a escrita de constantes caracteres. Uma constante caractere
escrita envolvendo o caractere com aspas simples. Assim, a expresso 'a' representa uma
constante caractere e resulta no valor numrico associado ao caractere a. Podemos, ento,
reescrever o fragmento de cdigo acima sem particularizar a tabela ASCII.
char c = 'a';
printf("%d %c\n", c, c);

Alm de agregar portabilidade e clareza ao cdigo, o uso de constantes caracteres nos livra
de conhecermos os cdigos associados a cada caractere.
Independente da tabela de cdigos numricos utilizada, garante-se que os dgitos so
codificados em seqncia. Deste modo, se o dgito zero tem cdigo 48, o dgito um tem
obrigatoriamente cdigo 49, e assim por diante. As letras minsculas e as letras maisculas
tambm formam dois grupos de cdigos seqenciais. O exemplo a seguir tira proveito desta
seqncia dos cdigos de caracteres.
Exemplo. Suponhamos que queremos escrever uma funo para testar se um caractere c
um dgito (um dos caracteres entre '0' e '9'). Esta funo pode ter o prottipo:
int digito(char c);

e ter como resultado 1 (verdadeiro) se c for um dgito, e 0 (falso) se no for.


A implementao desta funo pode ser dada por:
int digito(char c)
{
if ((c>='0')&&(c<='9'))
return 1;
else
return 0;
}

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
2

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

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

pi
yi

xi

yi+1

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.

Estruturas de Dados PUC-Rio

7-8

Podemos tambm prever uma funo para remover os dados de um aluno da tabela. Vamos
considerar que a posio da tabela a ser liberada ser passada para a funo:
void remove (int i)
{
if (tab[i] != NULL)
{
free(tab[i]);
tab[i] = NULL;
}
}

Para consultarmos os dados, vamos considerar uma funo que imprime os dados
armazenados numa determinada posio do vetor:
void imprime (int i)
{
if (tab[i] != NULL)
{
printf("Nome: %s\n, tab[i]->nome);
printf("Matrcula: %d\n, tab[i]->mat);
printf("Endereo: %s\n, tab[i]->end);
printf("Telefone: %s\n, tab[i]->tel);
}
}

Por fim, podemos implementar uma funo que imprima os dados de todos os alunos da
tabela:
void imprime_tudo (void)
{
int i;
for (i=0; i<MAX; i++)
imprime(i);
}

Exerccio. Faa um programa que utilize as funes da tabela de alunos escritas acima.
Exerccio. Re-escreva as funes acima sem usar uma varivel global.
Sugesto: Crie um tipo Tabela e faa as funes receberem este tipo como primeiro
parmetro.

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

Estruturas de Dados PUC-Rio

8-1

criado dinamicamente dentro da funo, podemos acess-lo depois da funo ser finalizada,
pois a rea de memria ocupada por ele permanece vlida, isto , o vetor no est alocado
na pilha de execuo. Usamos esta propriedade quando escrevemos a funo que duplica
uma cadeia de caracteres (string): a funo duplica aloca um vetor de char
dinamicamente, preenche seus valores e retorna o ponteiro, para que a funo que chama
possa acessar a nova cadeia de caracteres.
A linguagem C oferece ainda um mecanismo para re-alocarmos um vetor dinamicamente.
Em tempo de execuo, podemos verificar que a dimenso inicialmente escolhida para um
vetor tornou-se insuficiente (ou excessivamente grande), necessitando um redimensionamento. A funo realloc da biblioteca padro nos permite re-alocar um vetor,
preservando o contedo dos elementos, que permanecem vlidos aps a re-alocao (no
fragmento de cdigo abaixo, m representa a nova dimenso do vetor).
v = (int*) realloc(v, m*sizeof(int));

Vale salientar que, sempre que possvel, optamos por trabalhar com vetores criados
estaticamente. Eles tendem a ser mais eficientes, j que os vetores alocados dinamicamente
tm uma indireo a mais (primeiro acessa-se o valor do endereo armazenado na varivel
ponteiro para ento acessar o elemento do vetor).

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

Estruturas de Dados PUC-Rio

8-3

vez que sabemos de antemo as dimenses a serem usadas. Nestes casos, vale a pena
definirmos um tipo prprio, pois nos livramos das construes sintticas confusas
explicitadas acima. Por exemplo, podemos definir o tipo Matrix4.
typedef float Matrix4[4][4];

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

g h

I j

Figura 8.3: Matriz com vetor de ponteiros.

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"

Estruturas de Dados PUC-Rio

/* malloc, free, exit */


/* printf */
/* sqrt */

9-4

Como s precisamos guardar as coordenadas de um ponto, podemos definir a estrutura


ponto da seguinte forma:
struct ponto {
float x;
float y;
};

A funo que cria um ponto dinamicamente deve alocar a estrutura que representa o
ponto e inicializar os seus campos:
Ponto* cria (float x, float y) {
Ponto* p = (Ponto*) malloc(sizeof(Ponto));
if (p == NULL) {
printf("Memria insuficiente!\n");
exit(1);
}
p->x = x;
p->y = y;
return p;
}

Para esse TAD, a funo que libera um ponto deve apenas liberar a estrutura que foi
criada dinamicamente atravs da funo cria:
void libera (Ponto* p) {
free(p);
}

As funes para acessar e atribuir valores s coordenadas de um ponto so de fcil


implementao, como pode ser visto a seguir.
void acessa (Ponto* p, float* x, float* y) {
*x = p->x;
*y = p->y;
}
void atribui (Ponto* p, float x, float y) {
p->x = x;
p->y = y;
}

J a operao para calcular a distncia entre dois pontos pode ser implementada da
seguinte forma:
float distancia (Ponto* p1, Ponto* p2) {
float dx = p2->x p1->x;
float dy = p2->y p1->y;
return sqrt(dx*dx + dy*dy);
}

Exerccio: Escreva um programa que faa uso do TAD ponto definido acima.
Exerccio: Acrescente novas operaes ao TAD ponto, tais como soma e subtrao de
pontos.

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"

/* malloc, free, exit */


/* printf */

struct matriz {
int lin;
int col;
float* v;
};
Matriz* cria (int m, int n) {
Matriz* mat = (Matriz*) malloc(sizeof(Matriz));
if (mat == NULL) {
printf("Memria insuficiente!\n");
exit(1);
}
mat->lin = m;
mat->col = n;
mat->v = (float*) malloc(m*n*sizeof(float));
return mat;
}
void libera (Matriz* mat){
free(mat->v);
free(mat);
}
float acessa (Matriz* mat, int i, int j) {
int k;
/* ndice do elemento no vetor */
if (i<0 || i>=mat->lin || j<0 || j>=mat->col) {
printf("Acesso invlido!\n");
exit(1);
}
k = i*mat->col + j;
return mat->v[k];
}
void atribui (Matriz* mat, int i, int j, float v) {
int k;
/* ndice do elemento no vetor */
if (i<0 || i>=mat->lin || j<0 || j>=mat->col) {
printf("Atribuio invlida!\n");
exit(1);
}
Estruturas de Dados PUC-Rio

9-7

k = i*mat->col + j;
mat->v[k] = v;
}
int linhas (Matriz* mat) {
return mat->lin;
}
int colunas (Matriz* mat) {
return mat->col;
}

Arquivo matriz2.c:
#include <stdlib.h>
#include <stdio.h>
#include "matriz.h"

/* malloc, free, exit */


/* printf */

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

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

Info1

10.2.
ULL

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 a
Uma possvel implementao da funo para retirar um elemento da lista mostrada
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

Estruturas de Dados PUC-Rio

t=

b*h
2

c = p r2

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)

topo

b
a

push (c)

topo

c
b
a

pop ()
retorna-se c

topo
b
a

topo

push (d)

d
b
a

pop ()
retorna-se d

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

2.2

3.5

4.0

ini

fim

Figura 11.1: Fila aps insero de quatro novos elementos.

3.5

4.0

ini

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

21.2

24.3

98

99

20.0

20.8

fim

ini

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

Info1

fim

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

Info1

fim

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

...

sub-rvores

Figura 13.2: Estrutura de rvore.

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

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

c
d

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

/* sub-rvore com 'd'

a2= cria('b',inicializa(),a1);

/* sub-rvore com 'b'

a3= cria('e',inicializa(),inicializa());

/* sub-rvore com 'e'

a4= cria('f',inicializa(),inicializa());

/* sub-rvore com 'f'

a5= cria('c',a3,a4);

/* sub-rvore com 'c'

a = cria('a',a2,a5 );

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

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

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

Idem para 57.


12
12
12
12
12

25
25
25
25
25

37
37
37
33
33

Idem para 48.


12
12
12
12

25
25
25
25

33
33
33
33

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
8

2
1

4
3

Figura 16.1: Exemplo de rvore binria de busca.

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
8

2
Retira n 4

4
3

4
3

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

4
8

2
troca 6 com 4

4
3

4
8

2
retira n 6

6
3

6
3

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