Escolar Documentos
Profissional Documentos
Cultura Documentos
Introdução À Estrutura de Dados PDF
Introdução À Estrutura de Dados PDF
Estruturas de Dados
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
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-1
CPU
Central de
processamento
Memria
Armazenamento
secundrio
Dispositivos de
entrada/sada
1-2
numrico (por exemplo, pode-se associar ao caractere 'A' o cdigo 65, ao caractere 'B' o
cdigo 66, e assim por diante). Se todos os caracteres tiverem cdigos associados (inclusive
os caracteres de pontuao e de formatao), podemos armazenar um texto na memria do
computador como uma seqncia de cdigos numricos.
Um computador s pode executar programas em linguagens de mquina. Cada programa
executvel uma seqncia de instrues que o processador central interpreta, executando
as operaes correspondentes. Esta seqncia de instrues tambm representada como
uma seqncia de cdigos numricos. Os programas ficam armazenados em disco e, para
serem executados pelo computador, devem ser carregados (transferidos) para a memria
principal. Uma vez na memria, o computador executa a seqncia de operaes
correspondente.
IM
Interpretador
Sada
Dados de
Entrada
1-3
Compilao
PC
Programa
Fonte
CM
Compilador
PM
Programa
Objeto
PM
Programa
Objeto
Sada
Execuo
Dados de
Entrada
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
1-5
(so visveis em todas as funes que se seguem sua definio). Alm do mais, devido
s variveis externas (ou globais) existirem permanentemente (pelo menos enquanto o
programa estiver sendo executado), elas retm seus valores mesmo quando as funes que
as acessam deixam de existir. Embora seja possvel definir variveis globais em qualquer
parte do ambiente global (entre quaisquer funes), prtica comum defini-las no incio do
arquivo-fonte.
Como regra geral, por razes de clareza e estruturao adequada do cdigo, devemos evitar
o uso indisciplinado de variveis globais e resolver os problemas fazendo uso de variveis
locais sempre que possvel. No prximo captulo, discutiremos variveis com mais detalhe.
Se no houver erro de compilao no nosso cdigo, este comando gera o executvel com o
nome prog (prog.exe, no Windows). Podemos ento executar o programa:
> prog
Digite a temperatura em Celsius: 10
A temperatura em Fahrenheit vale: 50.000000
>
Podemos utilizar qualquer editor de texto para escrever os programas fontes, exceto editores que incluem
caracteres de formatao (como o Word do Windows, por exemplo).
1-6
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;
}
1-7
A opo c do compilador gcc indica que no queremos criar um executvel, apenas gerar
o arquivo objeto (com extenso .o ou .obj). Depois, invocamos gcc para fazer a
ligao dos objetos, gerando o executvel.
Editar
Compilar
Ligar
Testar
1-8
Este ciclo pode ser realizado usando programas (editor, compilador, ligador) separados ou
empregando um ambiente integrado de desenvolvimento (integrated development
environment, ou IDE). IDE um programa que oferece janelas para a edio de programas
e facilidades para abrir, fechar e salvar arquivos e para compilar, ligar e executar
programas. Se um IDE estiver disponvel, possvel criar e testar um programa, tudo em
um mesmo ambiente, e todo o ciclo mencionado acima acontece de maneira mais
confortvel dentro de um mesmo ambiente, de preferncia com uma interface amigvel.
1-9
2. Expresses
W. Celes e J. L. Rangel
Em C, uma expresso uma combinao de variveis, constantes e operadores que pode ser
avaliada computacionalmente, resultando em um valor. O valor resultante chamado de
valor da expresso.
2.1.
Variveis
Podemos dizer que uma varivel representa um espao na memria do computador para
armazenar determinado tipo de dado. Na linguagem C, todas as variveis devem ser
explicitamente declaradas. Na declarao de uma varivel, obrigatoriamente, devem ser
especificados seu tipo e seu nome: o nome da varivel serve de referncia ao dado
armazenado no espao de memria da varivel e o tipo da varivel determina a natureza do
dado que ser armazenado.
Tipos bsicos
A linguagem C oferece alguns tipos bsicos. Para armazenar valores inteiros, existem trs
tipos bsicos: char, short int, long int. Estes tipos diferem entre si pelo espao de
memria que ocupam e conseqentemente pelo intervalo de valores que podem representar.
O tipo char, por exemplo, ocupa 1 byte de memria (8 bits), podendo representar 28
(=256) valores distintos. Todos estes tipos podem ainda ser modificados para representarem
apenas valores positivos, o que feito precedendo o tipo com o modificador sem sinal
unsigned. A tabela abaixo compara os tipos para valores inteiros e suas
representatividades.
Tipo
char
unsigned char
short int
unsigned short int
long int
unsigned long int
Tamanho
1 byte
1 byte
2 bytes
2 bytes
4 bytes
4 bytes
Representatividade
-128 a 127
0 a 255
-32 768 a 32 767
0 a 65 535
-2 147 483 648 a 2 147 483 647
4 294 967295
Os tipos short int e long int podem ser referenciados simplesmente com short e
long, respectivamente. O tipo int puro mapeado para o tipo inteiro natural da mquina,
que pode ser short ou long. A maioria das mquinas que usamos hoje funcionam com
processadores de 32 bits e o tipo int mapeado para o inteiro de 4 bytes (long).1
O tipo char geralmente usado apenas para representar cdigos de caracteres, como
veremos nos captulos subseqentes.
Um contra-exemplo o compilador TurboC, que foi desenvolvido para o sistema operacional DOS mas
ainda pode ser utilizado no Windows. No TurboC, o tipo int mapeado para 2 bytes.
2-1
A linguagem oferece ainda dois tipos bsicos para a representao de nmeros reais (ponto
flutuante): float e double. A tabela abaixo compara estes dois tipos.
Tipo
float
double
Tamanho
4 bytes
8 bytes
Representatividade
-38
38
10 a 10
-308
308
10
a 10
Declarao de variveis
Para armazenarmos um dado (valor) na memria do computador, devemos reservar o
espao correspondente ao tipo do dado a ser armazenado. A declarao de uma varivel
reserva um espao na memria para armazenar um dado do tipo da varivel e associa o
nome da varivel a este espao de memria.
int a;
int b;
float c;
a = 5;
b = 10;
c = 5.3;
/* 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;
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;
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;
Valores constantes
Em nossos cdigos, usamos tambm valores constantes. Quando escrevemos a atribuio:
a = b + 123;
2-2
pois o cdigo, a rigor, armazena um valor double (12.45) numa varivel do tipo float.
Desde que a constante seja representvel dentro de um float, no precisamos nos
preocupar com este tipo de advertncia.
Variveis com valores indefinidos
Um dos erros comuns em programas de computador o uso de variveis cujos valores
ainda esto indefinidos. Por exemplo, o trecho de cdigo abaixo est errado, pois o valor
armazenado na varivel b est indefinido e tentamos us-lo na atribuio a c. comum
dizermos que b tem lixo.
int a, b, c;
a = 2;
c = a + b;
Alguns destes erros so bvios (como o ilustrado acima) e o compilador capaz de nos
reportar uma advertncia; no entanto, muitas vezes o uso de uma varivel no definida fica
difcil de ser identificado no cdigo. Repetimos que um erro comum em programas e uma
razo para alguns programas funcionarem na parte da manh e no funcionarem na parte da
tarde (ou funcionarem durante o desenvolvimento e no funcionarem quando entregamos
para nosso cliente!). Todos os erros em computao tm lgica. A razo de o programa
poder funcionar uma vez e no funcionar outra que, apesar de indefinido, o valor da
varivel existe. No nosso caso acima, pode acontecer que o valor armazenado na memria
ocupada por b seja 0, fazendo com que o programa funcione. Por outro lado, pode
acontecer de o valor ser 293423 e o programa no funcionar.
2-3
2.2.
Operadores
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
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;
so equivalentes a:
var = var op (expr);
equivale a
x = x * (y + 1)
e no a
x = x * y + 1;
2-5
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;
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:
&&
||
!
/* verdadeiro */
/* falso */
armazena o valor 4 na varivel a, pois um float ocupa 4 bytes de memria. Este operador
pode tambm ser aplicado a uma varivel, retornando o nmero de bytes do tipo associado
varivel.
Converso de tipo
Em C, como na maioria das linguagens, existem converses automticas de valores na
avaliao de uma expresso. Assim, na expresso 3/1.5, o valor da constante 3 (tipo int)
promovido (convertido) para double antes de a expresso ser avaliada, pois o segundo
operando do tipo double (1.5) e a operao feita na preciso do tipo mais
representativo.
2-7
Quando, numa atribuio, o tipo do valor atribudo diferente do tipo da varivel, tambm
h uma converso automtica de tipo. Por exemplo, se escrevermos:
int a = 3.5;
o valor 3.5 convertido para inteiro (isto , passa a valer 3) antes de a atribuio ser
efetuada. Como resultado, como era de se esperar, o valor atribudo varivel 3 (inteiro).
Alguns compiladores exibem advertncias quando a converso de tipo pode significar uma
perda de preciso ( o caso da converso real para inteiro, por exemplo).
O programador pode explicitamente requisitar uma converso de tipo atravs do uso do
operador de molde de tipo (operador cast). Por exemplo, so vlidos (e isentos de qualquer
advertncia por parte dos compiladores) os comandos abaixo.
int a, b;
a = (int) 3.5;
b = (int) 3.5 % 2;
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
2-8
cdigo. Este assunto ser tratado em detalhes na seo sobre funes. Por ora, basta saber
que preciso escrever:
#include <stdio.h>
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);
Ou:
printf ("Inteiro = %d
com sada:
Inteiro = 33
Real = 5.3
2-9
Isto , alm dos especificadores de formato, podemos incluir textos no formato, que so
mapeados diretamente para a sada. Assim, a sada formada pela cadeia de caracteres do
formato onde os especificadores so substitudos pelos valores correspondentes.
Existem alguns caracteres de escape que so freqentemente utilizados nos formatos de
sada. So eles:
\n
\t
\r
\"
\\
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:
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);
obriga que os valores (inteiros) fornecidos sejam separados pelo caractere dois pontos (:).
Um espao em branco dentro do formato faz com que sejam "pulados" eventuais brancos
da entrada. Os especificadores %d, %f, %e e %g automaticamente pulam os brancos que
precederem os valores numricos a serem capturados. A funo scanf retorna o nmero de
campos lidos com sucesso.
2-11
3.
Controle de fluxo
W. Celes e J. L. Rangel
ou
if ( expr ) {
bloco de comandos 1
...
}
else {
bloco de comandos 2
...
}
A indentao (recuo de linha) dos comandos fundamental para uma maior clareza do
cdigo. O estilo de indentao varia a gosto do programador. Alm da forma ilustrada
acima, outro estilo bastante utilizado por programadores C :
if ( expr )
{
bloco de comandos 1
...
}
else
{
bloco de comandos 2
...
}
Estruturas de Dados PUC-Rio
3-1
Primeiramente, notamos que no foi necessrio criar blocos ( {...} ) porque a cada if est
associado apenas um comando. Ao primeiro, associamos o segundo comando if, e ao
segundo if associamos o comando que chama a funo printf. Assim, o segundo if s
ser avaliado se o primeiro valor fornecido for par, e a mensagem s ser impressa se o
segundo valor fornecido tambm for par. Outra construo para este mesmo exemplo
simples pode ser:
int main (void)
{
int a, b;
printf("Digite dois numeros inteiros:");
scanf("%d%d",&a,&b);
if ((a%2 == 0) && (b%2 == 0))
printf ( "Voce digitou dois numeros pares!\n");
return 0;
}
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);
3-3
Estruturas de bloco
Observamos que uma funo C composta por estruturas de blocos. Cada chave aberta e
fechada em C representa um bloco. As declaraes de variveis s podem ocorrer no incio
do corpo da funo ou no incio de um bloco, isto , devem seguir uma chave aberta. Uma
varivel declarada dentro de um bloco vlida apenas dentro do bloco. Aps o trmino do
bloco, a varivel deixa de existir. Por exemplo:
...
if ( n > 0 )
{
int i;
...
}
...
A varivel i, definida dentro do bloco do if, s existe dentro deste bloco. uma boa
prtica de programao declarar as varveis o mais prximo possvel dos seus usos.
Operador condicional
C possui tambm um chamado operador condicional. Trata-se de um operador que
substitui construes do tipo:
...
if ( a > b )
maximo = a;
else
maximo = b;
...
expresso2;
n != n (n 1) (n 2)...3 2 1, onde 0 != 1
3-4
3-5
/* 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);
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;
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.
3-7
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;
}
3.3. Seleo
Alm da construo else-if, C prov um comando (switch) para selecionar um dentre
um conjunto de possveis casos. Sua forma geral :
3-8
switch ( expr )
{
case op1:
...
/* comandos executados se expr == op1
break;
case op2:
...
/* comandos executados se expr == op2
break;
case op3:
...
/* comandos executados se expr == op3
break;
default:
...
/* executados se expr for diferente de
break;
}
*/
*/
*/
todos
*/
opi deve ser um nmero inteiro ou uma constante caractere. Se expr resultar no valor opi,
os comandos que se seguem ao caso opi so executados, at que se encontre um break. Se
o comando break for omitido, a execuo do caso continua com os comandos do caso
seguinte. O caso default (nenhum dos outros) pode aparecer em qualquer posio, mas
3-9
4. Funes
W. Celes e J. L. Rangel
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);
4-2
Uma funo pode ter um valor de retorno associado. Para ilustrar a discusso, vamos reescrever o cdigo acima, fazendo com que a funo fat retorne o valor do fatorial. A
funo main fica ento responsvel pela impresso do valor.
/* programa que le um numero e imprime seu fatorial (verso 2) */
#include <stdio.h>
int fat (int n);
int main (void)
{
int n, r;
scanf("%d", &n);
r = fat(n);
printf("Fatorial = %d\n", r);
return 0;
}
/* funcao para calcular o valor do fatorial */
int fat (int n)
{
int i;
int f = 1;
for (i = 1; i <= n; i++)
f *= i;
return f;
}
4-3
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
4-4
c
b
a
'x'
43.5
7
Podemos, ento, analisar passo a passo a evoluo do programa mostrado acima, ilustrando
o funcionamento da pilha de execuo.
1 - Incio do programa: pilha vazia
main >
main >
f
n
fat >
r
n
main >
1.0
5
5
5 - Final do lao
f
n
fat >
r
n
main >
120.0
0
5
fat >
main >
n
r
5
-
main >
r
n
120.0
5
Isto ilustra por que o valor da varivel passada nunca ser alterado dentro da funo. A
seguir, discutiremos uma forma para podermos alterar valores por passagem de parmetros,
o que ser realizado passando o endereo de memria onde a varivel est armazenada.
Vale salientar que existe outra forma de fazermos comunicao entre funes, que consiste
no uso de variveis globais. Se uma determinada varivel global visvel em duas funes,
ambas as funes podem acessar e/ou alterar o valor desta varivel diretamente. No
Estruturas de Dados PUC-Rio
4-5
declaramos uma varivel com nome a que pode armazenar valores inteiros.
Automaticamente, reserva-se um espao na memria suficiente para armazenar valores
inteiros (geralmente 4 bytes).
Da mesma forma que declaramos variveis para armazenar inteiros, podemos declarar
variveis que, em vez de servirem para armazenar valores inteiros, servem para armazenar
valores de endereos de memria onde h variveis inteiras armazenadas. C no reserva
uma palavra especial para a declarao de ponteiros; usamos a mesma palavra do tipo com
os nomes das variveis precedidas pelo caractere *. Assim, podemos escrever:
int *p;
Neste caso, declaramos uma varivel com nome p que pode armazenar endereos de
memria onde existe um inteiro armazenado. Para atribuir e acessar endereos de memria,
a linguagem oferece dois operadores unrios ainda no discutidos. O operador unrio &
(endereo de), aplicado a variveis, resulta no endereo da posio da memria reservada
para a varivel. O operador unrio * (contedo de), aplicado a variveis do tipo ponteiro,
acessa o contedo do endereo de memria armazenado pela varivel ponteiro. Para
exemplificar, vamos ilustrar esquematicamente, atravs de um exemplo simples, o que
ocorre na pilha de execuo. Consideremos o trecho de cdigo mostrado na figura abaixo.
/*varivel inteiro */
int a;
int *p;
p
a
112
108
104
4-6
/* 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
*p = 6;
p
a
4-7
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;
}
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;
}
A Figura 4.6 ilustra a execuo deste programa mostrando o uso da memria. Assim,
conseguimos o efeito desejado. Agora fica explicado por que passamos o endereo das
variveis para a funo scanf, pois, caso contrrio, a funo no conseguiria devolver os
valores lidos.
4-9
main
b
a
>
7
5
112
108
104
troca
troca
main
temp
py
108
px
>
b
a
>
104
116
112
7
5
108
104
120
>
5
108
104
120
7
7
108
104
116
112
py
px
>b
a
>
main
108
104
7
5
116
112
108
104
5
108
104
7
5
120
116
112
108
104
>
5
108
104
120
5
7
108
104
116
112
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
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-11
3.14159
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)
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
a - b
o resultado impresso 17 e no 8, como poderia ser esperado. A razo simples, pois para
o compilador (fazendo a substituio da macro) est escrito:
printf(" %d ", 4 * 5 - 3);
(a * b)
((a) * (b))
Conclumos, portanto, que, como regra bsica para a definio de macros, devemos
envolver cada parmetro, e a macro como um todo, com parnteses.
4-13
5.1. Vetores
A forma mais simples de estruturarmos um conjunto de dados por meio de vetores. Como
a maioria das linguagens de programao, C permite a definio de vetores. Definimos um
vetor em C da seguinte forma:
int v[10];
A declarao acima diz que v um vetor de inteiros dimensionado com 10 elementos, isto
, reservamos um espao de memria contnuo para armazenar 10 valores inteiros. Assim,
se cada int ocupa 4 bytes, a declarao acima reserva um espao de memria de 40 bytes,
como ilustra a figura abaixo.
144
104
Mas:
v[10]
5-1
(x m )
v=
/* 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
Devemos observar que passamos para a funo scanf o endereo de cada elemento do
vetor (&v[i]), pois desejamos que os valores capturados sejam armazenados nos
elementos do vetor. Se v[i] representa o (i+1)-simo elemento do vetor, &v[i] representa
o endereo de memria onde esse elemento est armazenado.
Na verdade, existe uma associao forte entre vetores e ponteiros, pois se existe a
declarao:
int v[10];
a varivel v, que representa o vetor, uma constante que armazena o endereo inicial do
vetor, isto , v, sem indexao, aponta para o primeiro elemento do vetor.
5-2
ou simplesmente:
int v[] = { 5, 10, 15, 20, 25 };
Neste ltimo caso, a linguagem dimensiona o vetor pelo nmero de elementos inicializados.
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
Observamos ainda que, como passado para a funo o endereo do primeiro elemento do
vetor (e no os elementos propriamente ditos), podemos alterar os valores dos elementos do
vetor dentro da funo. O exemplo abaixo ilustra:
/* Incrementa elementos de um vetor */
#include <stdio.h>
void incr_vetor ( int n, int *v )
{
int i;
for (i = 0; i < n; i++)
v[i]++;
}
int main ( void )
{
int a[ ] = {1, 3, 5};
incr_vetor(3, a);
printf("%d %d %d \n", a[0], a[1], a[2]);
return 0;
}
5-4
Uso da memria
Informalmente, podemos dizer que existem trs maneiras de reservarmos espao de
memria para o armazenamento de informaes. A primeira delas atravs do uso de
variveis globais (e estticas). O espao reservado para uma varivel global existe enquanto
o programa estiver sendo executado. A segunda maneira atravs do uso de variveis
locais. Neste caso, como j discutimos, o espao existe apenas enquanto a funo que
declarou a varivel est sendo executada, sendo liberado para outros usos quando a
execuo da funo termina. Por este motivo, a funo que chama no pode fazer referncia
ao espao local da funo chamada. As variveis globais ou locais podem ser simples ou
vetores. Para os vetores, precisamos informar o nmero mximo de elementos, caso
contrrio o compilador no saberia o tamanho do espao a ser reservado.
A terceira maneira de reservarmos memria requisitando ao sistema, em tempo de
execuo, um espao de um determinado tamanho. Este espao alocado dinamicamente
permanece reservado at que explicitamente seja liberado pelo programa. Por isso,
podemos alocar dinamicamente um espao de memria numa funo e acess-lo em outra.
A partir do momento que liberarmos o espao, ele estar disponibilizado para outros usos e
no podemos mais acess-lo. Se o programa no liberar um espao alocado, este ser
automaticamente liberado quando a execuo do programa terminar.
Apresentamos abaixo um esquema didtico que ilustra de maneira fictcia a distribuio do
uso da memria pelo sistema operacional1.
A rigor, a alocao dos recursos bem mais complexa e varia para cada sistema operacional.
5-5
Cdigo do
Programa
Variveis
Globais e Estticas
Memria Alocada
Dinamicamente
Memria Livre
Pilha
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));
Cdigo do
Programa
Cdigo do
Programa
Variveis
Globais e Estticas
Variveis
Globais e Estticas
40 bytes
Livre
504
Livre
v
504
Se, porventura, no houver espao livre suficiente para realizar a alocao, a funo retorna
um endereo nulo (representado pelo smbolo NULL, definido em stdlib.h). Podemos cercar
o erro na alocao do programa verificando o valor de retorno da funo malloc. Por
exemplo, podemos imprimir uma mensagem e abortar o programa com a funo exit,
tambm definida na stdlib.
5-7
v = (int*) malloc(10*sizeof(int));
if (v==NULL)
{
printf("Memoria insuficiente.\n");
exit(1); /* aborta o programa e retorna 1 para o sist. operacional */
}
Para liberar um espao de memria alocado dinamicamente, usamos a funo free. Esta
funo recebe como parmetro o ponteiro da memria a ser liberada. Assim, para liberar o
vetor v, fazemos:
free (v);
S podemos passar para a funo free um endereo de memria que tenha sido alocado
dinamicamente. Devemos lembrar ainda que no podemos acessar o espao na memria
depois que o liberamos.
Para exemplificar o uso da alocao dinmica, alteraremos o programa para o clculo da
mdia e da varincia mostrado anteriormente. Agora, o programa l o nmero de valores
que sero fornecidos, aloca um vetor dinamicamente e faz os clculos. Somente a funo
principal precisa ser alterada, pois as funes para calcular a mdia e a varincia
anteriormente apresentadas independem do fato de o vetor ter sido alocado esttica ou
dinamicamente.
/* Clculo da mdia e da varincia de n reais */
#include <stdio.h>
#include <stdlib.h>
...
int main ( void )
{
int i, n;
float *v;
float med, var;
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
nul
bel
bs
ht
nl
cr
del
null: nulo
bell: campainha
backspace: voltar e apagar um caractere
tab ou tabulao horizontal
newline ou line feed: mudana de linha
carriage return: volta ao incio da linha
delete: apagar um caractere
Alguns alfabetos precisam de maior representatividade. O alfabeto chins, por exemplo, tem mais de 256
caracteres, no sendo suficiente o tipo char (alguns compiladores oferecem o tipo wchar, para estes casos).
6-1
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);
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);
6-2
Exerccio. Escreva uma funo para determinar se um caractere uma letra, com prottipo:
int letra(char c);
Exerccio. Escreva uma funo para converter um caractere para maiscula. Se o caractere
dado representar uma letra minscula, devemos ter como valor de retorno a letra maiscula
correspondente. Se o caractere dado no for uma letra minscula, devemos ter como valor
de retorno o mesmo caractere, sem alterao. O prottipo desta funo pode ser dado por:
char maiuscula(char c);
Se o caractere '\0' no fosse colocado, a funo printf executaria de forma errada, pois
no conseguiria identificar o final da cadeia.
Como as cadeias de caracteres so vetores, podemos reescrever o cdigo acima
inicializando os valores dos elementos do vetor na declarao:
int main ( void )
Estruturas de Dados PUC-Rio
6-3
{
char cidade[ ] = {'R', 'i', 'o', '\0'};
printf("%s \n", cidade);
return 0;
}
s1[] = "";
s2[] = "Rio de Janeiro";
s3[81];
s4[81] = "Rio";
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);
...
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 */
6-6
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';
}
6-7
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]);
}
}
6-9
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]);
}
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'
6-10
"Rio" */
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
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);
A funo para liberar os nomes alocados na tabela pode ser implementada por:
void liberanomes (int n, char** alunos)
{
int i;
for (i=0; i<n; i++)
free(alunos[i]);
}
6-12
Uma funo para imprimir os nomes dos alunos pode ser dada por:
void imprimenomes (int n, char** alunos)
{
int i;
for (i=0; i<n; i++)
printf("%s\n", alunos[i]);
}
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.
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;
7-1
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;
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);
}
Podemos ainda pensar numa funo para ler a hora do evento. Observamos que, neste caso,
obrigatoriamente devemos passar o ponteiro da estrutura, caso contrrio no seria possvel
passar ao programa principal os dados lidos:
void captura (struct ponto* pp)
{
printf("Digite as coordenadas do ponto(x y): ");
scanf("%f %f", &p->x, &p->y);
}
7-3
Com estas funes, nossa funo main ficaria como mostrado abaixo.
int main (void)
{
struct ponto p;
captura(&p);
imprime(&p);
return 0;
}
podemos usar o nome Real como um mnemnico para o tipo float. O uso de typedef
muito til para abreviarmos nomes de tipos e para tratarmos tipos complexos. Alguns
exemplos vlidos de typedef:
typedef unsigned char UChar;
typedef int* PInt;
typedef float Vetor[4];
7-4
Neste fragmento de cdigo, definimos UChar como sendo o tipo char sem sinal, PInt
como um tipo ponteiro para int, e Vetor como um tipo que representa um vetor de quatro
elementos. A partir dessas definies, podemos declarar variveis usando estes
mnemnicos:
Vetor v;
...
v[0] = 3;
...
Em geral, definimos nomes de tipos para as estruturas com as quais nossos programas
trabalham. Por exemplo, podemos escrever:
struct ponto {
float x;
float y;
};
typedef struct ponto Ponto;
Neste caso, Ponto passa a representar nossa estrutura de ponto. Tambm podemos definir
um nome para o tipo ponteiro para a estrutura.
typedef struct ponto *PPonto;
Podemos ainda definir mais de um nome num mesmo typedef. Os dois typedef
anteriores poderiam ser escritos por:
typedef struct ponto Ponto, *PPonto;
7-5
pi+1
pi
yi
xi
yi+1
xi+1
Na figura, ressaltamos a rea do trapzio definido pela aresta que vai do ponto pi ao ponto
pi+1. A rea desse trapzio dada por: a = ( xi +1 xi )( yi +1 + yi ) / 2 . Somando-se as reas
(algumas delas negativas) dos trapzios definidos por todas as arestas chega-se a rea do
polgono (as reas externas ao polgono so anuladas). Se a seqncia de pontos que define
o polgono for dada em sentido anti-horrio, chega-se a uma rea de valor negativo.
Neste caso, a rea do polgono o valor absoluto do resultado da soma.
Um vetor de estruturas pode ser usado para definir um polgono. O polgono passa a ser
representado por um seqncia de pontos. Podemos, ento, escrever uma funo para
calcular a rea de um polgono, dados o nmero de pontos e o vetor de pontos que o
representa. Uma implementao dessa funo mostrada abaixo.
float area (int n, Ponto* p)
{
int i, j;
float a = 0;
for (i=0; i<n; i++) {
j = (i+1) % n;
/* prximo ndice (incremento circular) */
a += (p[j].x-p[i].x)*(p[i].y + p[j].y)/2;
}
if (a < 0)
return -a;
else
return a;
}
Estruturas de Dados PUC-Rio
7-6
Exerccio: Altere o programa acima para capturar do teclado o nmero de pontos que
delimitam o polgono. O programa deve, ento, alocar dinamicamente o vetor de pontos,
capturar as coordenadas dos pontos e, chamando a funo area, exibir o valor da rea.
Vamos montar a tabela de alunos usando um vetor global com um nmero mximo de
alunos. Uma primeira opo declarar um vetor de estruturas:
#define MAX 100
Aluno tab[MAX];
Desta forma, podemos armazenar nos elementos do vetor os dados dos alunos que
queremos organizar. Seria vlido, por exemplo, uma atribuio do tipo:
...
tab[i].mat = 9912222;
...
7-7
No entanto, o uso de vetores de estruturas tem, neste caso, uma grande desvantagem. O tipo
1
Aluno definido acima ocupa pelo menos 227 (=81+4+121+21) bytes . A declarao de
um vetor desta estrutura representa um desperdcio significativo de memria, pois
provavelmente estaremos armazenando de fato um nmero de alunos bem inferior ao
mximo estimado. Para contornar este problema, podemos trabalhar com um vetor de
ponteiros.
typedef struct aluno *PAluno;
#define MAX 100
PAluno tab[MAX];
Assim, cada elemento do vetor ocupa apenas o espao necessrio para armazenar um
ponteiro. Quando precisarmos alocar os dados de um aluno numa determinada posio do
vetor, alocamos dinamicamente a estrutura Aluno e guardamos seu endereo no vetor de
ponteiros.
Considerando o vetor de ponteiros declarado acima como uma varivel global, podemos
ilustrar a implementao de algumas funcionalidades para manipular nossa tabela de
alunos. Inicialmente, vamos considerar uma funo de inicializao. Uma posio do vetor
estar vazia, isto , disponvel para armazenar informaes de um novo aluno, se o valor do
seu elemento for o ponteiro nulo. Portanto, numa funo de inicializao, podemos atribuir
NULL a todos os elementos da tabela, significando que temos, a princpio, uma tabela vazia.
void inicializa (void)
{
int i;
for (i=0; i<MAX; i++)
tab[i] = NULL;
}
Uma segunda funcionalidade que podemos prever armazena os dados de um novo aluno
numa posio do vetor. Vamos considerar que os dados sero fornecidos via teclado e que
uma posio onde os dados sero armazenados ser passada para a funo. Se a posio da
tabela estiver vazia, devemos alocar uma nova estrutura; caso contrrio, atualizamos a
estrutura j apontada pelo ponteiro.
void preenche (int i)
{
if (tab[i]==NULL)
tab[i] = (PAluno)malloc(sizeof(Aluno));
Provavelmente o tipo ocupar mais espao, pois os dados tm que estar alinhados para serem armazenados
na memria.
7-8
Podemos tambm prever uma funo para remover os dados de um aluno da tabela. Vamos
considerar que a posio da tabela a ser liberada ser passada para a funo:
void remove (int i)
{
if (tab[i] != NULL)
{
free(tab[i]);
tab[i] = NULL;
}
}
Para consultarmos os dados, vamos considerar uma funo que imprime os dados
armazenados numa determinada posio do vetor:
void imprime (int i)
{
if (tab[i] != NULL)
{
printf("Nome: %s\n, tab[i]->nome);
printf("Matrcula: %d\n, tab[i]->mat);
printf("Endereo: %s\n, tab[i]->end);
printf("Telefone: %s\n, tab[i]->tel);
}
}
Por fim, podemos implementar uma funo que imprima os dados de todos os alunos da
tabela:
void imprime_tudo (void)
{
int i;
for (i=0; i<MAX; i++)
imprime(i);
}
Exerccio. Faa um programa que utilize as funes da tabela de alunos escritas acima.
Exerccio. Re-escreva as funes acima sem usar uma varivel global.
Sugesto: Crie um tipo Tabela e faa as funes receberem este tipo como primeiro
parmetro.
7-9
Anlogo estrutura, este fragmento de cdigo no declara nenhuma varivel, apenas define
o tipo unio. Aps uma definio, podemos declarar variveis do tipo unio:
union exemplo v;
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.
0
1
7-10
Com isto, definimos as constantes FALSE e TRUE. Por default, o primeiro smbolo
representa o valor 0, o seguinte o valor 1, e assim por diante. Poderamos explicitar os
valores dos smbolos numa enumerao, como por exemplo:
enum bool {
TRUE = 1,
FALSE = 0,
};
No exemplo do tipo booleano, a numerao default coincide com a desejada (desde que o
smbolo FALSE preceda o smbolo TRUE dentro da lista da enumerao).
A declarao de uma varivel do tipo criado pode ser dada por:
Bool resultado;
onde resultado representa uma varivel que pode receber apenas os valores FALSE (0)
ou TRUE (1).
7-11
8. Matrizes
W. Celes e J. L. Rangel
J discutimos em captulos anteriores a construo de conjuntos unidimensionais atravs do
uso de vetores. A linguagem C tambm permite a construo de conjuntos bi ou
multidimensionais. Neste captulo, discutiremos em detalhe a manipulao de matrizes,
representadas por conjuntos bidimensionais de valores numricos. As construes
apresentadas aqui podem ser estendidas para conjuntos de dimenses maiores.
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).
5.0
20.0
35.0
50.0
10.0
25.0
40.0
55.0
15.0
30.0
45.0
60.0
152
60.0
55.0
50.0
45.0
40.0
35.0
30.0
25.0
20.0
15.0
10.0
5.0
104
8-2
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};
Uma segunda opo declarar o parmetro como matriz, podendo omitir o nmero de
linhas2:
void f (..., float mat[][3], ...);
De qualquer forma, o acesso aos elementos da matriz dentro da funo feito da forma
usual, com indexao dupla.
Na prxima seo, examinaremos formas de trabalhar com matrizes alocadas
dinamicamente. No entanto, vale salientar que recomendamos, sempre que possvel, o uso
de matrizes alocadas estaticamente. Em diversas aplicaes, as matrizes tm dimenses
fixas e no justificam a criao de estratgias para trabalhar com alocao dinmica. Em
aplicaes da rea de Computao Grfica, por exemplo, comum trabalharmos com
matrizes de 4 por 4 para representar transformaes geomtricas e projees. Nestes casos,
muito mais simples definirmos as matrizes estaticamente (float mat[4][4];), uma
2
Isto tambm vale para vetores. Um prottipo de uma funo que recebe um vetor como parmetro pode ser
dado por: void f (..., float v[], ...);.
8-3
vez que sabemos de antemo as dimenses a serem usadas. Nestes casos, vale a pena
definirmos um tipo prprio, pois nos livramos das construes sintticas confusas
explicitadas acima. Por exemplo, podemos definir o tipo Matrix4.
typedef float Matrix4[4][4];
/* declarao de varivel */
/* especificao de parmetro */
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.
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
A alocao da matriz agora mais elaborada. Primeiro, temos que alocar o vetor de
ponteiros. Em seguida, alocamos cada uma das linhas da matriz, atribuindo seus endereos
aos elementos do vetor de ponteiros criado. O fragmento de cdigo abaixo ilustra esta
codificao:
int i;
float **mat;
/* matriz representada por um vetor de ponteiros */
...
mat = (float**) malloc(m*sizeof(float*));
for (i=0; i<m; i++)
m[i] = (float*) malloc(n*sizeof(float));
A grande vantagem desta estratgia que o acesso aos elementos feito da mesma forma
que quando temos uma matriz criada estaticamente, pois, se mat representa uma matriz
Estruturas de Dados PUC-Rio
8-5
alocada segundo esta estratgia, mat[i] representa o ponteiro para o primeiro elemento da
linha i, e, conseqentemente, mat[i][j] acessa o elemento da coluna j da linha i.
A liberao do espao de memria ocupado pela matriz tambm exige a construo de um
lao, pois temos que liberar cada linha antes de liberar o vetor de ponteiros:
...
for (i=0; i<m; i++)
free(mat[i]);
free(mat);
A funo que cria a matriz dinamicamente deve alocar a estrutura que representa a matriz e
alocar o vetor dos elementos:
Matriz* cria (int m, int n)
{
Matriz* mat = (Matriz*) malloc(sizeof(Matriz));
mat->lin = m;
mat->col = n;
mat->v = (float*) malloc(m*n*sizeof(float));
Estruturas de Dados PUC-Rio
8-6
return mat;
}
Poderamos ainda incluir na criao uma inicializao dos elementos da matriz, por
exemplo atribuindo-lhes valores iguais a zero.
A funo que libera a memria deve liberar o vetor de elementos e ento liberar a estrutura
que representa a matriz:
void libera (Matriz* mat)
{
free(mat->v);
free(mat);
}
A funo de acesso e atribuio pode fazer um teste adicional para garantir que no haja
invaso de memria. Se a aplicao que usa o mdulo tentar acessar um elemento fora das
dimenses da matriz, podemos reportar um erro e abortar o programa. A implementao
destas funes pode ser dada por:
float acessa (Matriz* mat, int i, int j)
{
int k;
/* ndice do elemento no vetor */
if (i<0 || i>=mat->lin || j<0 || j>=mat->col) {
printf("Acesso invlido!\n);
exit(1);
}
k = i*mat->col + j;
return mat->v[k];
}
void atribui (Matriz* mat, int i, int j, float v)
{
int k;
/* ndice do elemento no vetor */
if (i<0 || i>=mat->lin || j<0 || j>=mat->col) {
printf("Atribuio invlida!\n);
exit(1);
}
k = i*mat->col + j;
mat->v[k] = v;
}
8-7
As funes para criar uma nova matriz e para liberar uma matriz previamente criada podem
ser dadas por:
Matriz* cria (int m, int n)
{
int i;
Matriz mat = (Matriz*) malloc(sizeof(Matriz));
mat->lin = m;
mat->col = n;
mat->v = (float**) malloc(m*sizeof(float*));
for (i=0; i<m; i++)
mat->v[i] = (float*) malloc(n*sizeof(float));
return mat;
}
void libera (Matriz* mat)
{
int i;
for (i=0; i<mat->lin; i++)
free(mat->v[i]);
free(mat->v);
free(mat);
}
As funes para acessar e atribuir podem ser implementadas conforme ilustrado abaixo:
float acessa (Matriz* mat, int i, int j)
{
if (i<0 || i>=mat->lin || j<0 || j>=mat->col) {
printf("Acesso invlido!\n);
exit(1);
}
return mat->v[i][j];
}
void atribui (Matriz* mat, int i, int j, float v)
{
if (i<0 || i>=mat->lin || j<0 || j>=mat->col) {
printf("Atribuio invlida!\n);
exit(1);
}
mat->v[i][j] = v;
}
Exerccio: Escreva um programa que faa uso das operaes de matriz definidas acima.
Note que a estratgia de implementao no deve alterar o uso das operaes.
Exerccio: Implemente uma funo que, dada uma matriz, crie dinamicamente a matriz
transposta correspondente, fazendo uso das operaes bsicas discutidas acima.
Exerccio: Implemente uma funo que determine se uma matriz ou no simtrica
quadrada, tambm fazendo uso das operaes bsicas.
8-8
(n 2 n) n (n + 1)
=
2
2
Podemos tambm determinar s como sendo a soma de uma progresso aritmtica, pois
temos que armazenar um elemento da primeira linha, dois elementos da segunda, trs da
terceira, e assim por diante.
s =n+
s = 1 + 2 + ... + n =
n (n + 1)
2
A implementao deste tipo abstrato tambm pode ser feita com um vetor simples ou um
vetor de ponteiros. A seguir, discutimos a implementao das operaes para criar uma
matriz e para acessar os elementos, agora para um tipo que representa uma matriz simtrica.
Matriz simtrica com vetor simples
Usando um vetor simples para armazenar os elementos da matriz, dimensionamos o vetor
com apenas s elementos. A estrutura que representa a matriz pode ser dada por:
struct matsim {
int dim;
float* v;
};
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];
}
Para criar a matriz, basta alocarmos um nmero varivel de elementos para cada linha. O
cdigo abaixo ilustra uma possvel implementao:
MatSim* cria (int n)
{
int i;
MatSim* mat = (MatSim*) malloc(sizeof(MatSim));
mat->dim = n;
mat->v = (float**) malloc(n*sizeof(float*));
for (i=0; i<n; i++)
mat->v[i] = (float*) malloc((i+1)*sizeof(float));
return mat;
}
O acesso aos elementos natural, desde que tenhamos o cuidado de no acessar elementos
que no estejam explicitamente alocados (isto , elementos com i<j).
8-10
Finalmente, observamos que exatamente as mesmas tcnicas poderiam ser usadas para
representar uma matriz triangular, isto , uma matriz cujos elementos acima (ou abaixo)
da diagonal so todos nulos. Neste caso, a principal diferena seria na funo acessa, que
teria como resultado o valor zero em um dos lados da diagonal, em vez acessar o valor
simtrico.
Exerccio: Escreva um cdigo para representar uma matriz triangular inferior.
Exerccio: Escreva um cdigo para representar uma matriz triangular superior.
8-11
9-1
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;
}
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);
<stdlib.h>
<stdio.h>
<math.h>
"ponto.h"
9-4
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);
}
J a operao para calcular a distncia entre dois pontos pode ser implementada da
seguinte forma:
float distancia (Ponto* p1, Ponto* p2) {
float dx = p2->x p1->x;
float dy = p2->y p1->y;
return sqrt(dx*dx + dy*dy);
}
Exerccio: Escreva um programa que faa uso do TAD ponto definido acima.
Exerccio: Acrescente novas operaes ao TAD ponto, tais como soma e subtrao de
pontos.
9-5
Exerccio: Acrescente novas operaes ao TAD ponto, de tal forma que seja possvel
obter uma representao do ponto em coordenadas polares.
Exerccio: Implemente um novo TAD para representar pontos no R3.
Exemplo 2: TAD Matriz
Como foi discutido anteriormente, a implementao de um TAD fica escondida
dentro de seu mdulo. Assim, podemos experimentar diferentes maneiras de
implementar um mesmo TAD, sem que isso afete os seus clientes. Para ilustrar essa
independncia de implementao, vamos considerar a criao de um tipo abstrato de
dados para representar matrizes de valores reais alocadas dinamicamente, com
dimenses m por n fornecidas em tempo de execuo. Para tanto, devemos definir um
tipo abstrato, que denominaremos de Matriz , e o conjunto de funes que operam
sobre esse tipo. Neste exemplo, vamos considerar as seguintes operaes:
cria: operao que cria uma matriz de dimenso m por n;
libera: operao que libera a memria alocada para a matriz;
acessa: operao que acessa o elemento da linha i e da coluna j da matriz;
atribui: operao que atribui o elemento da linha i e da coluna j da matriz;
linhas: operao que devolve o nmero de linhas da matriz;
colunas: operao que devolve o nmero de colunas da matriz.
A interface do mdulo pode ser dada pelo cdigo abaixo:
Arquivo matriz.h:
/* TAD: matriz m por n */
/* Tipo exportado */
typedef struct matriz Matriz;
/* Funes exportadas */
/* Funo cria
** Aloca e retorna uma matriz de dimenso m por n
*/
Matriz* cria (int m, int n);
/* Funo libera
** Libera a memria de uma matriz previamente criada.
*/
void libera (Matriz* mat);
/* Funo acessa
** Retorna o valor do elemento da linha i e coluna j da matriz
*/
float acessa (Matriz* mat, int i, int j);
/* Funo atribui
** Atribui o valor dado ao elemento da linha i e coluna j da matriz
*/
void atribui (Matriz* mat, int i, int j, float v);
/* Funo linhas
** Retorna o nmero de linhas da matriz
*/
int linhas (Matriz* mat);
Estruturas de Dados PUC-Rio
9-6
/* Funo colunas
** Retorna o nmero de colunas da matriz
*/
int colunas (Matriz* mat);
struct matriz {
int lin;
int col;
float* v;
};
Matriz* cria (int m, int n) {
Matriz* mat = (Matriz*) malloc(sizeof(Matriz));
if (mat == NULL) {
printf("Memria insuficiente!\n");
exit(1);
}
mat->lin = m;
mat->col = n;
mat->v = (float*) malloc(m*n*sizeof(float));
return mat;
}
void libera (Matriz* mat){
free(mat->v);
free(mat);
}
float acessa (Matriz* mat, int i, int j) {
int k;
/* ndice do elemento no vetor */
if (i<0 || i>=mat->lin || j<0 || j>=mat->col) {
printf("Acesso invlido!\n");
exit(1);
}
k = i*mat->col + j;
return mat->v[k];
}
void atribui (Matriz* mat, int i, int j, float v) {
int k;
/* ndice do elemento no vetor */
if (i<0 || i>=mat->lin || j<0 || j>=mat->col) {
printf("Atribuio invlida!\n");
exit(1);
}
Estruturas de Dados PUC-Rio
9-7
k = i*mat->col + j;
mat->v[k] = v;
}
int linhas (Matriz* mat) {
return mat->lin;
}
int colunas (Matriz* mat) {
return mat->col;
}
Arquivo matriz2.c:
#include <stdlib.h>
#include <stdio.h>
#include "matriz.h"
struct matriz {
int lin;
int col;
float** v;
};
Matriz* cria (int m, int n) {
int i;
Matriz* mat = (Matriz*) malloc(sizeof(Matriz));
if (mat == NULL) {
printf("Memria insuficiente!\n");
exit(1);
}
mat->lin = m;
mat->col = n;
mat->v = (float**) malloc(m*sizeof(float*));
for (i=0; i<m; i++)
mat->v[i] = (float*) malloc(n*sizeof(float));
return mat;
}
void libera (Matriz* mat) {
int i;
for (i=0; i<mat->lin; i++)
free(mat->v[i]);
free(mat->v);
free(mat);
}
float acessa (Matriz* mat, int i, int j) {
if (i<0 || i>=mat->lin || j<0 || j>=mat->col) {
printf("Acesso invlido!\n");
exit(1);
}
return mat->v[i][j];
}
void atribui (Matriz* mat, int i, int j, float v) {
if (i<0 || i>=mat->lin || j<0 || j>=mat->col) {
printf("Atribuio invlida!\n");
exit(1);
}
mat->v[i][j] = v;
}
int linhas (Matriz* mat) {
return mat->lin;
}
9-8
Exerccio: Escreva um programa que faa uso do TAD matriz definido acima. Teste o
seu programa com as duas implementaes vistas.
Exerccio: Usando apenas as operaes definidas pelo TAD matriz, implemente uma
funo que determine se uma matriz ou no quadrada simtrica.
Exerccio: Usando apenas as operaes definidas pelo TAD matriz, implemente uma
funo que, dada uma matriz, crie dinamicamente a matriz transposta correspondente.
Exerccio: Defina um TAD para implementar matrizes quadradas simtricas, de acordo
com a representao sugerida no captulo 8.
9-9
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
Info2
Info3
10.1.
ULL
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
A seguir, ilustramos um trecho de cdigo que cria uma lista inicialmente vazia e insere
nela novos elementos.
10-3
/*
/*
/*
/*
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);
}
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
10.3.
ULL
Info1
Info2
Info3
prim
Info1
Info2
Info3
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.
10-5
elemento da lista */
int v) {
ponteiro para elemento anterior */
ponteiro para percorrer a lista*/
10-6
/*
/*
/*
/*
/*
/*
/*
/* imprimir: 56 45 23 */
/* imprimir: 56 23 */
Mais uma vez, observe que no podemos deixar de atualizar a varivel que representa a
lista a cada insero e a cada remoo de um elemento. Esquecer de atribuir o valor de
retorno varivel que representa a lista pode gerar erros graves. Se, por exemplo, a
funo retirar o primeiro elemento da lista, a varivel que representa a lista, se no fosse
atualizada, estaria apontando para um n j liberado. Como alternativa, poderamos
fazer com que as funes insere e retira recebessem o endereo da varivel que
representa a lista. Nesse caso, os parmetros das funes seriam do tipo ponteiro para
lista (Lista** l) e seu contedo poderia ser acessado/atualizado de dentro da funo
usando o operador contedo (*l).
Manuteno da lista ordenada
A funo de insero vista acima armazena os elementos na lista na ordem inversa
ordem de insero, pois um novo elemento sempre inserido no incio da lista. Se
quisermos manter os elementos na lista numa determinada ordem, temos que encontrar
a posio correta para inserir o novo elemento. Essa funo no eficiente, pois temos
que percorrer a lista, elemento por elemento, para acharmos a posio de insero. Se a
ordem de armazenamento dos elementos dentro da lista no for relevante, optamos por
fazer inseres no incio, pois o custo computacional disso independe do nmero de
elementos na lista.
No entanto, se desejarmos manter os elementos em ordem, cada novo elemento deve ser
inserido na ordem correta. Para exemplificar, vamos considerar que queremos manter
nossa lista de nmeros inteiros em ordem crescente. A funo de insero, neste caso,
tem a mesma assinatura da funo de insero mostrada, mas percorre os elementos da
lista a fim de encontrar a posio correta para a insero do novo. Com isto, temos que
saber inserir um elemento no meio da lista. A Figura 9.6 ilustra a insero de um
elemento no meio da lista.
10-7
prim
Info1
Info2
Info3
10.5.
ULL
Novo
Devemos notar que essa funo, analogamente ao observado para a funo de remoo,
tambm funciona se o elemento tiver que ser inserido no final da lista.
10-8
fcil alterarmos o cdigo acima para obtermos a impresso dos elementos da lista em
ordem inversa: basta invertermos a ordem das chamadas s funes printf e
imprime_rec.
A funo para retirar um elemento da lista tambm pode ser escrita de forma recursiva.
Neste caso, s retiramos um elemento se ele for o primeiro da lista (ou da sub-lista). Se
o elemento que queremos retirar no for o primeiro, chamamos a funo recursivamente
para retirar o elemento da sub-lista.
/* Funo retira recursiva */
Lista* retira_rec (Lista* l, int v)
{
if (vazia(l))
return l;
/* lista vazia: retorna valor original */
/* verifica se elemento a ser retirado o primeiro */
if (l->info == v) {
Lista* t = l;
/* temporrio para poder liberar */
l = l->prox;
free(t);
}
else {
/* retira de sub-lista */
l->prox = retira_rec(l->prox,v);
}
return l;
}
A funo para liberar uma lista tambm pode ser escrita recursivamente, de forma
bastante simples. Nessa funo, se a lista no for vazia, liberamos primeiro a sub-lista e
depois liberamos a lista.
10-9
Exerccio: Implemente uma funo que verifique se duas listas encadeadas so iguais.
Duas listas so consideradas iguais se tm a mesma seqncia de elementos. O
prottipo da funo deve ser dado por:
int igual (Lista* l1, Lista* l2);
Exerccio: Implemente uma funo que crie uma cpia de uma lista encadeada. O
prottipo da funo deve ser dado por:
Lista* copia (Lista* l);
Esta mesma composio pode ser escrita de forma mais clara se definirmos um tipo
adicional que represente a informao. Podemos definir um tipo Retangulo e us-lo
para representar a informao armazenada na lista.
struct retangulo {
float b;
float h;
};
typedef struct retangulo Retangulo;
10-10
struct lista {
Retangulo info;
struct lista *prox;
};
typedef struct lista Lista;
Aqui, a informao volta a ser representada por um nico campo (info), que uma
estrutura. Se p fosse um ponteiro para um n da lista, o valor da base do retngulo
armazenado nesse n seria acessado por: p->info.b.
Ainda mais interessante termos o campo da informao representado por um ponteiro
para a estrutura, em vez da estrutura em si.
struct retangulo {
float b;
float h;
};
typedef struct retangulo Retangulo;
struct lista {
Retangulo *info;
struct lista *prox;
};
typedef struct lista Lista;
Neste caso, para criarmos um n, temos que fazer duas alocaes dinmicas: uma para
criar a estrutura do retngulo e outra para criar a estrutura do n. O cdigo abaixo ilustra
uma funo para a criao de um n.
Lista* cria (void)
{
Retangulo* r = (Retangulo*) malloc(sizeof(Retangulo));
Lista* p = (Lista*) malloc(sizeof(Lista));
p->info = r;
p->prox = NULL;
return p;
}
Naturalmente, o valor da base associado a um n p seria agora acessado por: p->info>b . A vantagem dessa representao (utilizando ponteiros) que, independente da
informao armazenada na lista, a estrutura do n sempre composta por um ponteiro
para a informao e um ponteiro para o prximo n da lista.
A representao da informao por um ponteiro nos permite construir listas
heterogneas, isto , listas em que as informaes armazenadas diferem de n para n.
Diversas aplicaes precisam construir listas heterogneas, pois necessitam agrupar
elementos afins mas no necessariamente iguais. Como exemplo, vamos considerar uma
aplicao que necessite manipular listas de objetos geomtricos planos para clculos de
reas. Para simplificar, vamos considerar que os objetos podem ser apenas retngulos,
tringulos ou crculos. Sabemos que as reas desses objetos so dadas por:
r = b*h
t=
b*h
2
c = p r2
10-11
A funo para a criao de um n da lista pode ser definida por trs variaes, uma para
cada tipo de objeto que pode ser armazenado.
10-12
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
};
10-14
A funo para o clculo da rea mostrada acima pode ser subdivida em funes
especficas para o clculo das reas de cada objeto geomtrico, resultando em um
cdigo mais estruturado.
/* funo para clculo da rea de um retngulo */
float ret_area (Retangulo* r)
{
return r->b * r->h;
}
/* funo para clculo da rea de um tringulo */
float tri_area (Triangulo* t)
{
return (t->b * t->h) / 2;
}
/* funo para clculo da rea de um crculo */
float cir_area (Circulo* c)
{
return PI * c->r * c->r;
}
/* funo para clculo da rea do n (verso 2) */
float area (ListaGen* p)
{
float a;
switch (p->tipo) {
case RET:
a = ret_area(p->info);
break;
case TRI:
a = tri_area(p->info);
break;
case CIR:
a = cir_area(p->info);
break;
}
return a;
}
Neste caso, a converso de ponteiro genrico para ponteiro especfico feita quando
chamamos uma das funes de clculo da rea: passa-se um ponteiro genrico que
atribudo, atravs da converso implcita de tipo, para um ponteiro especfico1.
Devemos salientar que, quando trabalhamos com converso de ponteiros genricos,
temos que garantir que o ponteiro armazene o endereo onde de fato existe o tipo
especfico correspondente. O compilador no tem como checar se a converso vlida;
a verificao do tipo passa a ser responsabilidade do programador.
Este cdigo no vlido em C++. A linguagem C++ no tem converso implcita de um ponteiro
genrico para um ponteiro especfico. Para compilar em C++, devemos fazer a converso explicitamente.
Por exemplo:
a = ret_area((Retangulo*)p->info);
Estruturas de Dados PUC-Rio
10-15
elemento. A lista pode ser representada por um ponteiro para um elemento inicial
qualquer da lista. A Figura 9.7 ilustra o arranjo da memria para a representao de uma
lista circular.
ini
Info1
Info2
Info3
Para percorrer os elementos de uma lista circular, visitamos todos os elementos a partir
do ponteiro do elemento inicial at alcanarmos novamente esse mesmo elemento. O
cdigo abaixo exemplifica essa forma de percorrer os elementos. Neste caso, para
simplificar, consideramos uma lista que armazena valores inteiros. Devemos salientar
que o caso em que a lista vazia ainda deve ser tratado (se a lista vazia, o ponteiro
para um elemento inicial vale NULL).
void imprime_circular (Lista* l)
{
Lista* p = l;
/* faz p apontar para o n inicial */
/* testa se lista no vazia */
if (p) {
{
/* percorre os elementos at alcanar novamente o incio */
do {
printf("%d\n", p->info);
/* imprime informao do n */
p = p->prox;
/* avana para o prximo n */
} while (p != l);
}
Exerccio: Escreva as funes para inserir e retirar um elemento de uma lista circular.
10-16
prim
Info1
Info2
Info3
Nessa funo, o novo elemento encadeado no incio da lista. Assim, ele tem como
prximo elemento o antigo primeiro elemento da lista e como anterior o valor NULL. A
seguir, a funo testa se a lista no era vazia, pois, neste caso, o elemento anterior do
ento primeiro elemento passa a ser o novo elemento. De qualquer forma, o novo
elemento passa a ser o primeiro da lista, e deve ser retornado como valor da lista
atualizada. A Figura 9.9 ilustra a operao de insero de um novo elemento no incio
da lista.
10-17
prim
Novo
Info1
Info2
Info3
Funo de busca
A funo de busca recebe a informao referente ao elemento que queremos buscar e
tem como valor de retorno o ponteiro do n da lista que representa o elemento. Caso o
elemento no seja encontrado na lista, o valor retornado NULL.
/* funo busca: busca um elemento na lista */
Lista2* busca (Lista2* l, int v)
{
Lista2* p;
for (p=l; p!=NULL; p=p->prox)
if (p->info == v)
return p;
return NULL;
/* no achou o elemento */
}
isto , o anterior passa a apontar para o prximo e o prximo passa a apontar para o
anterior. Quando p apontar para um elemento no meio da lista, as duas atribuies
acima so suficientes para efetivamente acertar o encadeamento da lista. No entanto, se
p for um elemento no extremo da lista, devemos considerar as condies de contorno.
Se p for o primeiro, no podemos escrever p->ant->prox, pois p->ant NULL; alm
disso, temos que atualizar o valor da lista, pois o primeiro elemento ser removido.
10-18
Exerccio: Escreva as funes para inserir e retirar um elemento de uma lista circular
duplamente encadeada.
10-19
11. Pilhas
W. Celes e J. L. Rangel
Uma das estruturas de dados mais simples a pilha. Possivelmente por essa razo, a
estrutura de dados mais utilizada em programao, sendo inclusive implementada
diretamente pelo hardware da maioria das mquinas modernas. A idia fundamental da
pilha que todo o acesso a seus elementos feito atravs do seu topo. Assim, quando
um elemento novo introduzido na pilha, passa a ser o elemento do topo, e o nico
elemento que pode ser removido da pilha o do topo. Isto faz com que os elementos da
pilha sejam retirados na ordem inversa ordem em que foram introduzidos: o primeiro
que sai o ltimo que entrou (a sigla LIFO last in, first out usada para descrever
esta estratgia).
Para entendermos o funcionamento de uma estrutura de pilha, podemos fazer uma
analogia com uma pilha de pratos. Se quisermos adicionar um prato na pilha, o
colocamos no topo. Para pegar um prato da pilha, retiramos o do topo. Assim, temos
que retirar o prato do topo para ter acesso ao prximo prato. A estrutura de pilha
funciona de maneira anloga. Cada novo elemento inserido no topo e s temos acesso
ao elemento do topo da pilha.
Existem duas operaes bsicas que devem ser implementadas numa estrutura de pilha:
a operao para empilhar um novo elemento, inserindo-o no topo, e a operao para
desempilhar um elemento, removendo-o do topo. comum nos referirmos a essas duas
operaes pelos termos em ingls push (empilhar) e pop (desempilhar). A Figura 10.1
ilustra o funcionamento conceitual de uma pilha.
push (a)
push (b)
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
10-1
50
struct pilha {
int n;
float vet[MAX];
};
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);
}
10-3
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;
}
10-4
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);
}
10-5
A funo cria recebe como parmetro de entrada uma cadeia de caracteres com o
formato que ser utilizado pela calculadora para imprimir os valores. Essa funo cria
uma calculadora inicialmente sem operandos na pilha.
Calc* cria_calc (char* formato)
{
Calc* c = (Calc*) malloc(sizeof(Calc));
strcpy(c->f,formato);
c->p = cria();
/* cria pilha vazia */
return c;
}
A funo operando coloca no topo da pilha o valor passado como parmetro. A funo
operador retira os dois valores do topo da pilha (s consideraremos operadores
binrios), efetua a operao correspondente e coloca o resultado no topo da pilha. As
operaes vlidas so: '+' para somar, '-' para subtrair, '*' para multiplicar e '/'
para dividir. Se no existirem operandos na pilha, consideraremos que seus valores so
zero. Tanto a funo operando quanto a funo operador imprimem, utilizando o
formato especificado na funo cria, o novo valor do topo da pilha.
void operando (Calc* c, float v)
{
/* empilha operando */
push(c->p,v);
/* imprime topo da pilha */
printf(c->f,v);
}
10-6
*/
=
=
=
=
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);
}
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
10-8
12. Filas
W. Celes e J. L. Rangel
A funo cria aloca dinamicamente a estrutura da fila, inicializa seus campos e retorna
seu ponteiro; a funo insere adiciona um novo elemento no final da fila e a funo
11-1
1.4
2.2
3.5
4.0
ini
fim
3.5
4.0
ini
fim
Com essa estratgia, fcil observar que, em um dado instante, a parte ocupada do
vetor pode chegar ltima posio. Para reaproveitar as primeiras posies livres do
vetor sem implementarmos uma re-arrumao trabalhosa dos elementos, podemos
incrementar as posies do vetor de forma circular: se o ltimo elemento da fila
ocupa a ltima posio do vetor, inserimos os novos elementos a partir do incio do
vetor. Desta forma, em um dado momento, poderamos ter quatro elementos, 20.0,
20.8, 21.2 e 24.3, distribudos dois no fim do vetor e dois no incio.
0
21.2
24.3
98
99
20.0
20.8
fim
ini
11-2
Podemos definir uma funo auxiliar responsvel por incrementar o valor de um ndice.
Essa funo recebe o valor do ndice atual e fornece com valor de retorno o ndice
incrementado, usando o incremento circular. Uma possvel implementao dessa funo
:
int incr (int i)
{
if (i == N-1)
return 0;
else
return i+1;
}
Essa mesma funo pode ser implementada de uma forma mais compacta, usando o
operador mdulo:
int incr(int i)
{
return (i+1)%N;
}
Com o uso do operador mdulo, muitas vezes optamos inclusive por dispensar a funo
auxiliar e escrever diretamente o incremento circular:
...
i=(i+1)%N;
...
Podemos declarar o tipo fila como sendo uma estrutura com trs componentes: um vetor
vet de tamanho N, um ndice ini para o incio da fila e um ndice fim para o fim da
fila.
Conforme ilustrado nas figuras acima, usamos as seguintes convenes para a
identificao da fila:
ini marca a posio do prximo elemento a ser retirado da fila;
fim marca a posio (vazia), onde ser inserido o prximo elemento.
Desta forma, a fila vazia se caracteriza por ter ini == fim e a fila cheia (quando no
possvel inserir mais elementos) se caracteriza por ter f i m e ini em posies
consecutivas (circularmente): incr(fim) == ini. Note que, com essas convenes,
a posio indicada por fim permanece sempre vazia, de forma que o nmero mximo
de elementos na fila N-1. Isto necessrio porque a insero de mais um elemento
faria ini == fim, e haveria uma ambigidade entre fila cheia e fila vazia. Outra
estratgia possvel consiste em armazenar uma informao adicional, n, que indicaria
explicitamente o nmero de elementos armazenados na fila. Assim, a fila estaria vazia
se n == 0 e cheia se n == N-1. Nos exemplos que se seguem, optamos por no
armazenar n explicitamente.
11-3
A funo para criar a fila aloca dinamicamente essa estrutura e inicializa a fila como
sendo vazia, isto , com os ndices ini e fim iguais entre si (no caso, usamos o valor
zero).
Fila* cria (void)
{
Fila* f = (Fila*) malloc(sizeof(Fila));
f->ini = f->fim = 0;
/* inicializa fila vazia */
return f;
}
Para inserir um elemento na fila, usamos a prxima posio livre do vetor, indicada por
fim. Devemos ainda assegurar que h espao para a insero do novo elemento, tendo
em vista que trata-se de um vetor com capacidade limitada. Consideraremos que a
funo auxiliar que faz o incremento circular est disponvel.
void insere (Fila* f, float v)
{
if (incr(f->fim) == f->ini) {
/* fila cheia: capacidade esgotada
*/
printf("Capacidade da fila estourou.\n");
exit(1);
/* aborta programa */
}
/* insere elemento na prxima posio livre */
f->vet[f->fim] = v;
f->fim = incr(f->fim);
}
A funo para retirar o elemento do incio da fila fornece o valor do elemento retirado
como retorno. Podemos tambm verificar se a fila est ou no vazia.
float retira (Fila* f)
{
float v;
if (vazia(f)) {
printf("Fila vazia.\n");
exit(1);
/* aborta programa */
}
/* retira elemento do incio */
v = f->vet[f->ini];
f->ini = incr(f->ini);
return v;
}
A funo que verifica se a fila est vazia pode ser dada por:
int vazia (Fila* f)
{
return (f->ini == f->fim);
}
11-4
Finalmente, a funo para liberar a memria alocada pela fila pode ser:
void libera (Fila* f)
{
free(f);
}
Info1
fim
Info2
Info3
A funo cria aloca a estrutura da fila e inicializa a lista como sendo vazia.
Fila* cria (void)
{
Fila* f = (Fila*) malloc(sizeof(Fila));
f->ini = f->fim = NULL;
return f;
}
Cada novo elemento inserido no fim da lista e, sempre que solicitado, retiramos o
elemento do incio da lista. Desta forma, precisamos de duas funes auxiliares de lista:
Estruturas de Dados PUC-Rio
11-5
para inserir no fim e para remover do incio. A funo para inserir no fim ainda no foi
discutida, mas simples, uma vez que temos explicitamente armazenado o ponteiro
para o ltimo elemento. Essa funo deve ter como valor de retorno o novo fim da
lista. A funo para retirar do incio idntica funo usada na implementao de
pilha.
/* funo auxiliar: insere no fim */
No* ins_fim (No* fim, float v)
{
No* p = (No*) malloc(sizeof(No));
p->info = v;
p->prox = NULL;
if (fim != NULL) /* verifica se lista no estava vazia */
fim->prox = p;
return p;
}
/* funo auxiliar: retira do incio */
No* ret_ini (No* ini)
{
No* p = ini->prox;
free(ini);
return p;
}
As funes que manipulam a fila fazem uso dessas funes de lista. Devemos salientar
que a funo de insero deve atualizar ambos os ponteiros, ini e fim, quando da
insero do primeiro elemento. Analogamente, a funo para retirar deve atualizar
ambos se a fila tornar-se vazia aps a remoo do elemento:
void insere (Fila* f, float v)
{
f->fim = ins_fim(f->fim,v);
if (f->ini==NULL) /* fila antes vazia? */
f->ini = f->fim;
}
float retira (Fila* f)
{
float v;
if (vazia(f)) {
printf("Fila vazia.\n");
exit(1);
/* aborta programa */
}
v = f->ini->info;
f->ini = ret_ini(f->ini);
if (f->ini == NULL)
/* fila ficou vazia? */
f->fim = NULL;
return v;
}
11-6
Por fim, a funo que libera a fila deve antes liberar todos os elementos da lista.
void libera (Fila* f)
{
No* q = f->ini;
while (q!=NULL) {
No* t = q->prox;
free(q);
q = t;
}
free(f);
}
Analogamente pilha, para testar o cdigo, pode ser til implementarmos uma funo
que imprima os valores armazenados na fila. Os cdigos abaixo ilustram a
implementao dessa funo nas duas verses de fila (vetor e lista). A ordem de
impresso adotada do incio para o fim.
/* imprime: verso com vetor */
void imprime (Fila* f)
{
int i;
for (i=f->ini; i!=f->fim; i=incr(i))
printf("%f\n",f->vet[i]);
}
/* imprime: verso com lista */
void imprime (Fila* f)
{
No* q;
for (q=f->ini; q!=NULL; q=q->prox)
printf("%f\n",q->info);
}
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);
11-8
O arranjo de memria para implementarmos a fila dupla com lista ilustrado na figura
abaixo:
ini
Info1
fim
Info2
Info3
O n da lista duplamente encadeada para armazenar valores reais pode ser dado por:
struct no2 {
float info;
struct no2* ant;
struct no2* prox;
};
typedef struct no2 No2;
11-9
Uma possvel implementao das funes para remover o elemento do incio ou do fim
mostrada a seguir. Essas funes tambm retornam, respectivamente, o novo n
inicial e final.
/* funo auxiliar: retira do incio */
No2* ret2_ini (No2* ini) {
No2* p = ini->prox;
if (p != NULL)
/* verifica se lista no ficou vazia */
p->ant = NULL;
free(ini);
return p;
}
/* funo auxiliar: retira do fim */
No2* ret2_fim (No2* fim) {
No2* p = fim->ant;
if (p != NULL)
/* verifica se lista no ficou vazia */
p->prox = NULL;
free(fim);
return p;
}
As funes que manipulam a fila fazem uso dessas funes de lista, atualizando os
ponteiros ini e fim quando necessrio.
void insere_ini (Fila2* f, float v) {
f->ini = ins2_ini(f->ini,v);
if (f->fim==NULL) /* fila antes vazia? */
f->fim = f->ini;
}
void insere_fim (Fila2* f, float v) {
f->fim = ins2_fim(f->fim,v);
if (f->ini==NULL) /* fila antes vazia? */
f->ini = f->fim;
}
float retira_ini (Fila2* f) {
float v;
if (vazia(f)) {
printf("Fila vazia.\n");
exit(1);
/* aborta programa */
}
v = f->ini->info;
f->ini = ret2_ini(f->ini);
if (f->ini == NULL)
/* fila ficou vazia? */
f->fim = NULL;
return v;
}
float retira_fim (Fila2* f) {
float v;
if (vazia(f)) {
printf("Fila vazia.\n");
exit(1);
/* aborta programa */
}
v = f->fim->info;
f->fim = ret2_fim(f->fim);
if (f->fim == NULL)
/* fila ficou vazia? */
f->ini = NULL;
return v;
}
11-10
13. rvores
W. Celes e J. L. Rangel
Nos captulos anteriores examinamos as estruturas de dados que podem ser chamadas
de unidimensionais ou lineares, como vetores e listas. A importncia dessas estruturas
inegvel, mas elas no so adequadas para representarmos dados que devem ser
dispostos de maneira hierrquica. Por exemplo, os arquivos (documentos) que criamos
num computador so armazenados dentro de uma estrutura hierrquica de diretrios
(pastas). Existe um diretrio base dentro do qual podemos armazenar diversos subdiretrios e arquivos. Por sua vez, dentro dos sub-diretrios, podemos armazenar outros
sub-diretrios e arquivos, e assim por diante, recursivamente. A Figura 13.1 mostra uma
imagem de uma rvore de diretrio no Windows 2000.
Neste captulo, vamos introduzir rvores, que so estruturas de dados adequadas para a
representao de hierarquias. A forma mais natural para definirmos uma estrutura de
rvore usando recursividade. Uma rvore composta por um conjunto de ns. Existe
um n r , denominado raiz, que contm zero ou mais sub-rvores, cujas razes so
ligadas diretamente a r. Esses ns razes das sub-rvores so ditos filhos do n pai, r.
Ns com filhos so comumente chamados de ns internos e ns que no tm filhos so
chamados de folhas, ou ns externos. tradicional desenhar as rvores com a raiz para
cima e folhas para baixo, ao contrrio do que seria de se esperar. A Figura 13.2
exemplifica a estrutura de uma rvore.
n raiz
...
sub-rvores
12-1
+
3
Numa rvore binria, cada n tem zero, um ou dois filhos. De maneira recursiva,
podemos definir uma rvore binria como sendo:
uma rvore vazia; ou
um n raiz tendo duas sub-rvores, identificadas como a sub-rvore da
direita (sad) e a sub-rvore da esquerda (sae).
A Figura 13.4 ilustra a definio de rvore binria. Essa definio recursiva ser usada
na construo de algoritmos, e na verificao (informal) da correo e do desempenho
dos mesmos.
12-2
raiz
vazia
sae
sad
a
b
c
d
Para descrever rvores binrias, podemos usar a seguinte notao textual: a rvore vazia
representada por <>, e rvores no vazias por <raiz sae sad>. Com essa notao,
a
rvore
da
Figura
13.5
representada
por:
<a<b<><d<><>>><c<e<><>><f<><>>>>.
Pela definio, uma sub-rvore de uma rvore binria sempre especificada como
sendo a sae ou a sad de uma rvore maior, e qualquer das duas sub-rvores pode ser
vazia. Assim, as duas rvores da Figura 13.6 so distintas.
a
b
a
b
Isto tambm pode ser visto pelas representaes textuais das duas rvores, que so,
respectivamente: <a <b<><>> <> > e <a <> <b<><>> >.
Estruturas de Dados PUC-Rio
12-3
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;
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;
}
a1= cria('d',inicializa(),inicializa());
a2= cria('b',inicializa(),a1);
a3= cria('e',inicializa(),inicializa());
a4= cria('f',inicializa(),inicializa());
a5= cria('c',a3,a4);
a = cria('a',a2,a5 );
Alternativamente, a rvore poderia ser criada com uma nica atribuio, seguindo a sua
estrutura, recursivamente:
Arv* a = cria('a',
cria('b',
inicializa(),
cria('d', inicializa(), inicializa())
),
cria('c',
cria('e', inicializa(), inicializa()),
cria('f', inicializa(), inicializa())
)
);
Para tratar a rvore vazia de forma diferente das outras, importante ter uma operao
que diz se uma rvore ou no vazia. Podemos ter:
12-5
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())
)
);
Uma outra funo que podemos considerar percorre a rvore buscando a ocorrncia de
um determinado caractere c em um de seus ns. Essa funo tem como retorno um
valor booleano (um ou zero) indicando a ocorrncia ou no do caractere na rvore.
int busca (Arv* a, char c){
if (vazia(a))
return 0;
/* rvore vazia: no encontrou */
else
return a->info==c || busca(a->esq,c) || busca(a->dir,c);
}
Note que esta forma de programar busca, em C, usando o operador lgico || (ou)
faz com que a busca seja interrompida assim que o elemento encontrado. Isto acontece
porque se c==a->info for verdadeiro, as duas outras expresses no chegam a ser
avaliadas. Analogamente, se o caractere for encontrado na sub-rvore da esquerda, a
busca no prossegue na sub-rvore da direita.
Podemos dizer que a expresso:
return c==a->info || busca(a->esq,c) || busca(a->dir,c);
equivalente a:
12-7
if (c==a->info)
return 1;
else if (busca(a->esq,c))
return 1;
else
return busca(a->dir,c);
inicializa (void);
cria (char c, Arv* e, Arv* d);
vazia (Arv* a);
imprime (Arv* a);
libera (Arv* a);
busca (Arv* a, char c);
/* mostra sae */
/* mostra sad */
/* mostra raiz */
/* libera sae */
/* libera sad */
/* libera raiz */
12-8
Nesse exemplo, podemos notar que o a rvore com raiz no n a tem 3 sub-rvores, ou,
equivalentemente, o n a tem 3 filhos. Os ns b e g tem dois filhos cada um; os ns c e
i tem um filho cada, e os ns d, e, h e j so folhas, e tem zero filhos.
12-9
De forma semelhante ao que foi feito no caso das rvores binrias, podemos representar
essas rvores atravs de notao textual, seguindo o padro: <raiz sa1 sa2 ...
san>. Com esta notao, a rvore da Figura 13.7 seria representada por:
a = <a <b <c <d>> <e>> <f> <g <h> <i <j>>>>
<d>>
<b <c <d>> <e>>
<j>>
<g <h> <i <j>>>
= <a <b <c <d>> <e>> <f> <g <h> <i <j>>>>
Representao em C
Dependendo da aplicao, podemos usar vrias estruturas para representar rvores,
levando em considerao o nmero de filhos que cada n pode apresentar. Se
soubermos, por exemplo, que numa aplicao o nmero mximo de filhos que um n
pode apresentar 3, podemos montar uma estrutura com 3 campos para apontadores
para os ns filhos, digamos, f1 , f2 e f3 . Os campos no utilizados podem ser
preenchidos com o valor nulo NULL, sendo sempre utilizados os campos em ordem.
Assim, se o n n tem 2 filhos, os campos f1 e f2 seriam utilizados, nessa ordem, para
apontar para eles, ficando f3 vazio. Prevendo um nmero mximo de filhos igual a 3, e
considerando a implementao de rvores para armazenar valores de caracteres simples,
a declarao do tipo que representa o n da rvore poderia ser:
struct arv3 {
char val;
struct no *f1, *f2, *f3;
};
A Figura 13.8 indica a representao da rvore da Figura 13.7 com esta organizao.
Como se pode ver no exemplo, em cada um dos ns que tem menos de trs filhos, o
espao correspondente aos filhos inexistentes desperdiado. Alm disso, se no existe
um limite superior no nmero de filhos, esta tcnica pode no ser aplicvel. O mesmo
acontece se existe um limite no nmero de ns, mas esse limite ser raramente
alcanado, pois estaramos tendo um grande desperdcio de espao de memria com os
campos no utilizados.
12-10
Uma soluo que leva a um aproveitamento melhor do espao utiliza uma lista de
filhos: um n aponta apenas para seu primeiro (prim) filho, e cada um de seus filhos,
exceto o ltimo, aponta para o prximo (prox) irmo. A declarao de um n pode ser:
struct arvgen {
char info;
struct arvgen *prim;
struct arvgen *prox;
};
A Figura 13.9 mostra o mesmo exemplo representado de acordo com esta estrutura.
Uma das vantagens dessa representao que podemos percorrer os filhos de um n de
forma sistemtica, de maneira anloga ao que fizemos para percorrer os ns de uma lista
simples.
12-11
12-12
(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 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);
}
12-14
14. Arquivos
W. Celes e J. L. Rangel
13-1
FILE um tipo definido pela biblioteca padro que representa uma abstrao do
arquivo. Quando abrimos um arquivo, a funo tem como valor de retorno um ponteiro
para o tipo FILE , e todas as operaes subseqentes nesse arquivo recebero este
endereo como parmetro de entrada. Se o arquivo no puder ser aberto, a funo tem
como retorno o valor NULL.
Devemos passar o nome do arquivo a ser aberto. O nome do arquivo pode ser relativo, e
o sistema procura o arquivo a partir do diretrio corrente (diretrio de trabalho do
programa), ou pode ser absoluto, onde especificamos o nome completo do arquivo,
incluindo os diretrios, desde o diretrio raiz.
Existem diferentes modos de abertura de um arquivo. Podemos abrir um arquivo para
leitura ou para escrita, e devemos especificar se o arquivo ser aberto em modo texto ou
em modo binrio. O parmetro modo da funo fopen uma cadeia de caracteres onde
espera-se a ocorrncia de caracteres que identificam o modo de abertura. Os caracteres
interpretados no modo so:
r
read-only
Indica modo apenas para leitura, no pode ser alterado.
w
write
Indica modo para escrita.
a
append
Indica modo para escrita ao final do existente.
t
text
Indica modo texto.
Estruturas de Dados PUC-Rio
13-2
binary
Se o arquivo j existe e solicitamos a sua abertura para escrita com modo w, o arquivo
apagado e um novo, inicialmente vazio, criado. Quando solicitamos com modo a, o
mesmo preservado e novos contedos podem ser escritos no seu fim. Com ambos os
modos, se o arquivo no existe, um novo criado.
Os modos b e t podem ser combinados com os demais. Maiores detalhes e outros
modos de abertura de arquivos podem ser encontrados nos manuais da linguagem C.
Em geral, quando abrimos um arquivo, testamos o sucesso da abertura antes de
qualquer outra operao, por exemplo:
...
FILE* fp;
fp = fopen("entrada.txt","rt");
if (fp == NULL) {
printf("Erro na abertura do arquivo!\n");
exit(1);
}
...
O valor de retorno dessa funo zero, se o arquivo for fechado com sucesso, ou a
constante EOF (definida pela biblioteca), que indica a ocorrncia de um erro.
Conforme pode ser observado, o primeiro parmetro deve ser o ponteiro do arquivo do
qual os dados sero lidos. Os demais parmetros so os j discutidos para a funo
scanf: o formato e a lista de endereos de variveis que armazenaro os valores lidos.
Como a funo scanf, a funo fscanf tambm tem como valor de retorno o nmero
de dados lidos com sucesso.
Estruturas de Dados PUC-Rio
13-3
Uma outra funo de leitura muito usada em modo texto a funo fgetc que, dado o
ponteiro do arquivo, captura o prximo caractere do arquivo. O prottipo dessa funo
:
int fgetc (FILE* fp);
Apesar do tipo do valor de retorno ser int, o valor retornado o caractere lido. Se o
fim do arquivo for alcanado, a constante EOF (end of file) retornada.
Uma outra funo muito utilizada para ler linhas de um arquivo a funo fgets. Essa
funo recebe como parmetros trs valores: a cadeia de caracteres que armazenar o
contedo lido do arquivo, o nmero mximo de caracteres que deve ser lido e o ponteiro
do arquivo. O prottipo da funo :
char* fgets (char* s, int n, FILE* fp);
O valor de retorno dessa funo o prprio caractere escrito, ou EOF se ocorrer um erro
na escrita.
13-4
13-5
/* funo toupper */
/*
/*
/*
/*
armazena
armazena
ponteiro
ponteiro
13-6
/* funo strstr */
/*
/*
/*
/*
/*
/*
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
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, ...);
13-8
...
}
else {
/* interpretar
...
}
break;
case 't':
if (n!=3) {
/* tratar erro
...
}
else {
/* interpretar
...
}
break;
case 'c':
if (n!=2) {
/* tratar erro
...
}
else {
/* interpretar
...
}
break;
default:
/* tratar erro de
...
break;
de formato do arquivo */
de formato do arquivo */
crculo: raio = v1 */
formato do arquivo */
}
}
}
...
4.5
10.4
1.3
6.0
9.6
Para interpretar esse formato, devemos ler cada uma das linhas e tentar ler trs valores
reais de cada linha (aceitando o caso de apenas dois valores serem lidos com sucesso).
Exerccio: Faa um programa que interprete o formato de pontos 3D descrito acima,
armazenando-os num vetor.
13-9
x n yn
13-10
Para exemplificar a utilizao dessas funes, vamos considerar que uma aplicao tem
um conjunto de pontos armazenados num vetor. O tipo que define o ponto pode ser:
struct ponto {
float x, y, z;
};
typedef struct ponto Ponto;
Uma funo para salvar o contedo de um vetor de pontos pode receber como
parmetros o nome do arquivo, o nmero de pontos no vetor, e o ponteiro para o vetor.
Uma possvel implementao dessa funo ilustrada abaixo:
void salva (char* arquivo, int n, Ponto* vet)
{
FILE* fp = fopen(arquivo,"wb");
if (fp==NULL) {
printf("Erro na abertura do arquivo.\n");
exit(1);
}
fwrite(vet,sizeof(Ponto),n,fp);
fclose(fp);
}
A rigor, os tipos int so substitudos pelo tipo size_t , definido pela biblioteca padro, sendo, em
geral, sinnimo para um inteiro sem sinal (unsigned int).
Estruturas de Dados PUC-Rio
13-11
13-12
15. Ordenao
W. Celes e J. L. Rangel
14-1
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
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
25
25
25
25
25
37
37
37
33
33
25
25
25
25
33
33
33
33
14-2
12x25
25x33
final da sexta passada
12x25
final da stima passada
final da ordenao
Uma funo cliente para testar esse algoritmo pode ser dada por:
/* Testa algoritmo de ordenao bolha */
#include <stdio.h>
int main (void)
{
int i;
int v[8] = {25,48,37,12,57,86,33,92};
bolha(8,v);
printf("Vetor ordenado: ");
for (i=0; i<8; i++)
printf("%d ",v[i]);
printf("\n);
return 0;
}
Para evitar que o processo continue mesmo depois de o vetor estar ordenado, podemos
interromper o processo quando houver uma passagem inteira sem trocas, usando uma
variante do algoritmo apresentado acima:
14-3
A varivel troca guarda o valor 0 (falso) quando uma passada do vetor (no for
interno) se faz sem nenhuma troca.
O esforo computacional despendido pela ordenao de um vetor por este procedimento
fcil de se determinar, pelo nmero de comparaes, que serve tambm para estimar o
nmero mximo de trocas que podem ser realizadas. Na primeira passada, fazemos n-1
comparaes; na segunda, n-2; na terceira n-3; e assim por diante. Logo, o tempo total
gasto pelo algoritmo proporcional a (n-1) + (n-2) + ... + 2 + 1. A soma
desses termos proporcional ao quadrado de n. Dizemos que o algoritmo de ordem
quadrtica e representamos isso escrevendo O(n2).
Implementao recursiva
Analisando a forma com que a ordenao bolha funciona, verificamos que o algoritmo
procura resolver o problema da ordenao por partes. Inicialmente, o algoritmo coloca
em sua posio (no final do vetor) o maior elemento, e o problema restante
semelhante ao inicial, s que com um vetor com menos elementos, formado pelos
elementos v[0],,v[n-2].
Baseado nessa observao, fcil implementar um algoritmo de ordenao bolha
recursivamente. Embora no seja a forma mais adequada de implementarmos esse
algoritmo, o estudo dessa recurso nos ajudar a entender a idia por trs do prximo
algoritmo de ordenao que veremos mais adiante.
O algoritmo recursivo de ordenao bolha posiciona o elemento de maior valor e
chama, recursivamente, o algoritmo para ordenar o vetor restante, com n-1 elementos.
14-4
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;
}
}
14-5
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);
14-6
14-7
Uma varivel ponteiro para armazenar o endereo dessa funo declarada como:
int(*cmp)(void*,void*);
Esse cdigo genrico pode ser usado para ordenar vetores com qualquer informao.
Para exemplificar, vamos us-lo para ordenar um vetor de nmeros reais. Para isso,
temos que escrever o cdigo da funo que faz a comparao, agora especializada para
nmero reais:
14-8
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
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
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
14-10
restando os vetores
(2-2) 33
e
(4-7) 57 86 48 92
14-11
/* Ordenao rpida */
void rapida (int n, int* v)
{
if (n <= 1)
return;
else {
int x = v[0];
int a = 1;
int b = n-1;
do {
while (a < n && v[a] <= x) a++;
while (v[b] > x) b--;
if (a < b) {
/* faz troca */
int temp = v[a];
v[a] = v[b];
v[b] = temp;
a++; b--;
}
} while (a <= b);
/* troca piv */
v[0] = v[b];
v[b] = x;
/* ordena sub-vetores restantes */
rapida(b,v);
rapida(n-a,&v[a]);
}
}
Devemos observar que para deslocar o ndice a para a direita, fizemos o teste:
while (a < n && v[a] <= x)
O teste adicional no deslocamento para a direita necessrio porque o piv pode ser o
elemento de maior valor, nunca ocorrendo a situao v[a]<=x, o que nos faria acessar
posies alm dos limites do vetor. No deslocamento para a esquerda, um teste
adicional tipo b>=0 no necessrio, pois, na nossa implementao, v[0] o piv,
impedindo que b assuma valores negativos (teremos, pelo menos, x>=v[0]).
Algoritmo genrico da biblioteca padro
O quicksort o algoritmo de ordenao mais utilizado no desenvolvimento de
aplicaes. Mesmo quando temos os dados organizados em listas encadeadas, e
precisamos coloc-los de forma ordenada, em geral, optamos por criar um vetor
temporrio com ponteiros para os ns da lista, fazer a ordenao usando quicksort e reencadear os ns montando a lista ordenada.
Devido a sua grande utilidade, a biblioteca padro de C disponibiliza, via a interface
stdlib.h , uma funo que ordena vetores usando esse algoritmo. A funo
disponibilizada pela biblioteca independe do tipo de informao armazenada no vetor.
A implementao dessa funo genrica segue os princpios discutidos na
implementao do algoritmo de ordenao bolha genrico. O prottipo da funo
disponibilizada pela biblioteca :
14-12
void qsort (void *v, int n, int tam, int (*cmp)(const void*, const
void*));
O parmetro cmp recebido pela funo qsort um ponteiro para uma funo
com esse prottipo. Assim, para usarmos a funo de ordenao da biblioteca
temos que escrever uma funo que receba dois ponteiros genricos, void*, os
quais representam ponteiros para os dois elementos que se deseja comparar. O
modificador de tipo const aparece no prottipo apenas para garantir que essa
funo no modificar os valores dos elementos (devem ser tratados como
valores constantes). Essa funo deve ter como valor de retorno 1 , 0 , ou 1 ,
dependendo se o primeiro elemento for menor, igual, ou maior que o segundo,
respectivamente.
Para ilustrar a utilizao da funo qsort vamos considerar alguns exemplos. O cdigo
a seguir ilustra a utilizao da funo para ordenar valores reais. Neste caso, os dois
ponteiros genricos passados para a funo de comparao representam ponteiros para
float.
/* Ilustra uso do algoritmo qsort */
#include <stdio.h>
#include <stdlib.h>
/* funo de comparao de reais */
int comp_reais (const void* p1, const void* p2)
{
/* converte ponteiros genricos para ponteiros de float */
float *f1 = (float*)p1;
float *f2 = (float*)p2;
/* dados os ponteiros de float, faz a comparao */
if (*f1 < *f2) return 1;
else if (*f1 > * f2) return 1;
else return 0;
}
14-13
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;
Numa segunda situao, podemos considerar que temos um vetor de ponteiros para a
estrutura aluno (por exemplo, Aluno* vet[N];). Agora, cada elemento do vetor um
ponteiro para o tipo Aluno e a funo de comparao tem que tratar uma indireo a
mais. Aqui, os dois ponteiros genricos passados para a funo de comparao
representam ponteiros de ponteiros para Aluno.
/* Funo de comparao: elemento do tipo Aluno* */
int comp_alunos (const void* p1, const void* p2)
/* converte ponteiros genricos para ponteiros de ponteiros de Aluno
*/
Aluno **pa1 = (Aluno**)p1;
Aluno **pa2 = (Aluno**)p2;
/* acessa ponteiro de Aluno */
Aluno *a1 = *p1;
Aluno *a2 = *p2;
/* dados os ponteiros de Aluno, faz a comparao */
return strcmp(a1->nome,a2->nome);
}
14-14
16. Busca
W. Celes e J. L. Rangel
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
/* elemento encontrado */
/* interrompe busca */
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;
}
Tamanho do problema
n
n/2
n/4
...
1
16-3
16-4
Algoritmo genrico
A biblioteca padro de C disponibiliza, via a interface stdlib.h, uma funo que faz a
busca binria de um elemento num vetor. A funo disponibilizada pela biblioteca
independe do tipo de informao armazenada no vetor. A implementao dessa funo
genrica segue os mesmos princpios discutidos no captulo anterior. O prottipo da
funo de busca binria da biblioteca :
void* bsearch (void* info, void *v, int n, int tam,
int (*cmp)(const void*, const void*)
);
Se o elemento for encontrado no vetor, a funo tem como valor de retorno o endereo
do elemento no vetor; caso o elemento no seja encontrado, o valor de retorno NULL.
Anlogo a funo qsort, apresentada no captulo anterior, os parmetros de entrada
dessa funo so:
info: o ponteiro para a informao que se deseja buscar no vetor representa a
chave de busca;
v : o ponteiro para o primeiro elemento do vetor onde a busca ser feita. Os
elementos do vetor tm que estar ordenados, segundo o critrio de ordenao
adotado pela funo de comparao descrita abaixo.
n: o nmero de elementos do vetor.
tam: o tamanho, em bytes, de cada elemento do vetor.
cmp: o ponteiro para a funo responsvel por comparar a informao buscada e
um elemento do vetor. O primeiro parmetro dessa funo sempre o endereo
da informao buscada, e o segundo um ponteiro para um dos elementos do
vetor. O critrio de comparao adotado por essa funo deve ser compatvel
com o critrio de ordenao do vetor. Essa funo deve ter como valor de
retorno 1, 0, ou 1, dependendo se a informao buscada for menor, igual, ou
maior que a informao armazenada no elemento, respectivamente.
Para ilustrar a utilizao da funo bsearch vamos, inicialmente, considerar um vetor
de valores inteiros. Neste caso, os dois ponteiros genricos passados para a funo de
comparao representam ponteiros para int.
/* Ilustra uso do algoritmo bsearch */
#include <stdio.h>
#include <stdlib.h>
/* funo de comparao de inteiros */
int comp_int (const void* p1, const void* p2)
{
/* converte ponteiros genricos para ponteiros de int */
int *i1 = (int*)p1;
int *i2 = (int*)p2;
/* dados os ponteiros de int, faz a comparao */
if (*i1 < *i2) return 1;
else if (*i1 > *i2) return 1;
else return 0;
}
16-5
Devemos notar que o ndice do elemento, se encontrado no vetor, pode ser extrado
subtraindo-se o ponteiro do elemento do ponteiro do primeiro elemento (p-v). Essa
aritmtica de ponteiros vlida aqui pois podemos garantir que ambos os ponteiros
armazenam endereos de memria de um mesmo vetor. A diferena entre os ponteiros
representa a distncia em que os elementos esto armazenados na memria.
Vamos agora considerar que queremos efetuar uma busca num vetor de ponteiros para
alunos. A estrutura que representa um aluno pode ser dada por:
struct aluno {
char nome[81];
char mat[8];
char turma;
char email[41];
};
typedef struct aluno Aluno;
Considerando que o vetor est ordenado segundo os nomes dos alunos, podemos buscar
a ocorrncia de um determinado aluno passando para a funo de busca um nome e o
vetor. A funo de comparao ento receber dois ponteiros: um ponteiro para uma
cadeia de caracteres e um ponteiro para um elemento do vetor (no caso ser um ponteiro
para ponteiro de aluno, ou seja, um Aluno**).
/* Funo de comparao: char* e Aluno** */
int comp_alunos (const void* p2, const void* p2)
/* converte ponteiros genricos para ponteiros especficos */
char* s = (char*)p1;
Aluno **pa = (Aluno**)p2;
/* faz a comparao */
return strcmp(s,(*pa)->nome);
}
Conforme observamos, o tipo de informao a ser buscada nem sempre igual ao tipo
do elemento; para dados complexos, em geral no . A informao buscada geralmente
representa um campo da estrutura armazenada no vetor (ou da estrutura apontada por
elementos do vetor).
Devemos finalmente salientar que se tivermos os dados armazenados em uma lista
encadeada, s temos a alternativa de implementar um algoritmo de busca linear, mesmo
se os elementos estiverem ordenados. Portanto, lista encadeada no uma boa opo
para estruturarmos nossos dados, se desejarmos realizar muitas operaes de busca. A
estrutura dinmica apropriada para a realizao de busca a rvore binria de busca que
ser discutida a seguir.
Estruturas de Dados PUC-Rio
16-6
16-7
Usando essa propriedade de ordem, a busca de um valor em uma rvore pode ser
simplificada. Para procurar um valor numa rvore, comparamos o valor que buscamos
com o valor associado raiz. Em caso de igualdade, o valor foi encontrado; se o valor
dado for menor que o valor associado raiz, a busca continua na sae; caso contrrio, se
o valor associado raiz for menor, a busca continua na sad. Por essa razo, estas
rvores so freqentemente chamadas de rvores binrias de busca.
Naturalmente, a ordem a que fizemos referncia acima dependente da aplicao. Se a
informao a ser armazenada em cada n da rvore for um nmero inteiro podemos usar
o habitual operador relacional menor que (<). Porm, se tivermos que considerar
casos em que a informao mais complexa, j vimos que uma funo de comparao
deve ser definida pelo programador, especificamente para cada caso.
Operaes em rvores binrias de busca
Para exemplificar a implementao de operaes em rvores binrias de busca, vamos
considerar o caso em que a informao associada a um n um nmero inteiro, e no
vamos considerar a possibilidade de repetio de valores associados aos ns da rvore.
A Figura 16.1 ilustra uma rvore de busca de valores inteiros.
6
8
2
1
4
3
16-8
Uma vez construda uma rvore de busca, podemos imprimir os valores da rvore em
ordem crescente percorrendo os ns em ordem simtrica:
void imprime (Arv* a)
{
if (a != NULL) {
imprime(a->esq);
printf("%d\n",a->info);
imprime(a->dir);
}
}
Essas so funes anlogas s vistas para rvores binrias comuns, pois no exploram a
propriedade de ordenao das rvores de busca. No entanto, as operaes que nos
interessa analisar em detalhes so:
busca: funo que busca um elemento na rvore;
insere: funo que insere um novo elemento na rvore;
retira: funo que retira um elemento da rvore.
Operao de busca
A operao para buscar um elemento na rvore explora a propriedade de ordenao da
rvore, tendo um desempenho computacional proporcional a sua altura (O(log n) para o
caso de rvore balanceada). Uma implementao da funo de busca dada por:
Arv* busca (Arv* r, int v)
{
if (r == NULL) return NULL;
else if (r->info > v) return busca (r->esq, v);
else if (r->info < v) return busca (r->dir, v);
else return r;
}
Operao de insero
A operao de insero adiciona um elemento na rvore na posio correta para que a
propriedade fundamental seja mantida. Para inserir um valor v em uma rvore usamos
sua estrutura recursiva, e a ordenao especificada na propriedade fundamental. Se a
(sub-) rvore vazia, deve ser substituda por uma rvore cujo nico n (o n raiz)
contm o valor v. Se a rvore no vazia, comparamos v com o valor na raiz da rvore,
e inserimos v na sae ou na sad, conforme o resultado da comparao. A funo abaixo
ilustra a implementao dessa operao. A funo tem como valor de retorno o eventual
novo n raiz da (sub-) rvore.
Arv* insere (Arv* a, int v)
{
if (a==NULL) {
a = (Arv*)malloc(sizeof(Arv));
a->info = v;
a->esq = a->dir = NULL;
}
else if (v < a->info)
a->esq = insere(a->esq,v);
else /* v < a->info */
a->dir = insere(a->dir,v);
return a;
}
16-9
Operao de remoo
Outra operao a ser analisada a que permite retirar um determinado elemento da
rvore. Essa operao um pouco mais complexa que a de insero. Existem trs
situaes possveis. A primeira, e mais simples, quando se deseja retirar um elemento
que folha da rvore (isto , um elemento que no tem filhos). Neste caso, basta retirar
o elemento da rvore e atualizar o pai, pois seu filho no existe mais.
A segunda situao, ainda simples, acontece quando o n a ser retirado possui um nico
filho. Para retirar esse elemento necessrio antes acertar o ponteiro do pai, pulando
o n: o nico neto passa a ser filho direto. A Figura 16.2 ilustra esse procedimento.
6
8
2
Retira n 4
4
3
4
3
O caso complicado ocorre quando o n a ser retirado tem dois filhos. Para poder retirar
esse n da rvore, devemos proceder da seguinte forma:
a) encontramos o elemento que precede o elemento a ser retirado na ordenao.
Isto equivale a encontrar o elemento mais direita da sub-rvore esquerda;
b) trocamos a informao do n a ser retirado com a informao do n encontrado;
c) retiramos o n encontrado (que agora contm a informao do n que se deseja
retirar). Observa-se que retirar o n mais direita trivial, pois esse um n
folha ou um n com um nico filho (no caso, o filho da direita nunca existe).
O procedimento descrito acima deve ser seguido para no haver violao da ordenao
da rvore. Observamos que, anlogo ao que foi feito com o n mais direita da subrvore esquerda, pode ser feito com o n mais esquerda da sub-rvore direita (que
o n que segue o n a ser retirado na ordenao).
A Figura 16.3 exemplifica a retirada de um n com dois filhos. Na figura mostrada a
estratgia de retirar o elemento que precede o elemento a ser retirado na ordenao.
16-10
Destruio do n 6
6
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.
16-11
rvores balanceadas
fcil prever que, aps vrias operaes de insero/remoo, a rvore tende a ficar
desbalanceada, j que essas operaes, conforme descritas, no garantem o
balanceamento. Em especial, nota-se que a funo de remoo favorece uma das subrvores (sempre retirando um n da sub-rvore esquerda, por exemplo). Uma
estratgia que pode ser utilizada para amenizar o problema intercalar de qual subrvore ser retirado o n. No entanto, isso ainda no garante o balanceamento da rvore.
Para que seja possvel usar rvores binrias de busca mantendo sempre a altura das
rvores no mnimo, ou prximo dele, necessrio um processo de insero e remoo
de ns mais complicado, que mantenha as rvores balanceadas, ou equilibradas,
tendo as duas sub-rvores de cada n o mesmo peso, ou pesos aproximadamente
iguais. No caso de um nmero de ns par, podemos aceitar uma diferena de um n
entre a sae (sub-rvore esquerda) e a sad (sub-rvore direita).
A idia central de um algoritmo para balancear (equilibrar) uma rvore binria de busca
pode ser a seguinte: se tivermos uma rvore com m elementos na sae, e n m + 2
elementos na sad, podemos tornar a rvore menos desequilibrada movendo o valor da
raiz para a sae, onde ele se tornar o maior valor, e movendo o menor elemento da sad
para a raiz. Dessa forma, a rvore continua com os mesmos elementos na mesma ordem.
A situao em que a sad tem menos elementos que a sae semelhante. Esse processo
pode ser repetido at que a diferena entre os nmeros de elementos das duas subrvores seja menor ou igual a 1 . Naturalmente, o processo deve continuar
(recursivamente) com o balanceamento das duas sub-rvores de cada rvore. Um ponto
a observar que remoo do menor (ou maior) elemento de uma rvore mais simples
do que a remoo de um elemento qualquer.
Exerccio: Implemente o algoritmo para balanceamento de rvore binria descrito
acima.
16-12
Como a matrcula composta por sete dgitos, o nmero inteiro que conceitualmente
representa uma matrcula varia de 0000000 a 9999999. Portanto, precisamos
dimensionar nosso vetor com dez milhes (10.000.000) de elementos. Isso pode ser
feito por:
#define MAX 10000000
Aluno vet[MAX];
Dessa forma, o nome do aluno com matrcula mat acessado simplesmente por:
vet[mat].nome . Temos um acesso rpido, mas pagamos um preo em uso de
Estruturas de Dados PUC-Rio
17-1
memria proibitivo. Como a estrutura de cada aluno, no nosso exemplo, ocupa pelo
menos 127 bytes, estamos falando num gasto de 1.270.000.000 bytes, ou seja, acima de
1 Gbyte de memria. Como na prtica teremos, digamos, em torno de 50 alunos
cadastrados, precisaramos apenas de algo em torno de 6.350 (=127*50) bytes.
Para amenizar o problema, j vimos que podemos ter um vetor de ponteiros, em vez de
um vetor de estruturas. Desta forma, as posies do vetor que no correspondem a
alunos cadastrados teriam valores NULL . Para cada aluno cadastrado, alocaramos
dinamicamente a estrutura de aluno e armazenaramos um ponteiro para essa estrutura
no vetor. Neste caso, acessaramos o nome do aluno de matrcula mat por vet[mat]>nome. Assim, considerando que cada ponteiro ocupe 4 bytes, o gasto excedente de
memria seria, no mximo, aproximadamente 40 Mbytes. Apesar de menor, esse gasto
de memria ainda proibitivo.
A forma de resolver o problema de gasto excessivo de memria, mas ainda garantindo
um acesso rpido, atravs do uso de tabelas de disperso (hash table) que
discutiremos a seguir.
Numa turma de aluno, comum existirem vrios alunos com o mesmo ano e perodo de
ingresso. Portanto, esses trs primeiros dgitos no so bons candidatos para identificar
individualmente cada aluno. Reduzimos nosso problema para uma chave com os quatro
dgitos seqenciais. Podemos ir alm e constatar que os nmeros seqenciais mais
significativos so os ltimos, pois num universo de uma turma de alunos, o dgito que
representa a unidade varia mais do que o dgito que representa o milhar.
Desta forma, podemos usar um nmero de matrcula parcial, de acordo com a dimenso
que queremos que tenha nossa tabela (ou nosso vetor). Por exemplo, para dimensionarmos nossa tabela com apenas 100 elementos, podemos usar apenas os ltimos
dois dgitos seqenciais do nmero de matrcula. A tabela pode ento ser declarada por:
Aluno* tab[100].
Para acessarmos o nome do aluno cujo nmero de matrcula dado por mat, usamos
como ndice da tabela apenas os dois ltimos dgitos. Isso pode ser conseguido
aplicando-se o operador modulo (%): vet[mat%100]->nome.
Estruturas de Dados PUC-Rio
17-2
Podemos generalizar essa funo para tabelas de disperso com dimenso N . Basta
avaliar o modulo do nmero de matrcula por N:
int hash (int mat)
{
return (mat%N);
}
Uma funo de hash deve, sempre que possvel, apresentar as seguintes propriedades:
Ser eficientemente avaliada: isto necessrio para termos acesso rpido, pois
temos que avaliar a funo de hash para determinarmos a posio onde o
elemento se encontra armazenado na tabela.
Espalhar bem as chaves de busca: isto necessrio para minimizarmos as
ocorrncias de colises. Como veremos, o tratamento de colises requer um
procedimento adicional para encontrarmos o elemento. Se a funo de hash
resulta em muitas colises, perdemos o acesso rpido aos elementos. Um
exemplo de funo de hash ruim seria usar, como ndice da tabela, os dois
dgitos iniciais do nmero de matrcula todos os alunos iriam ser mapeados
para apenas trs ou quatro ndices da tabela.
Ainda para minimizarmos o nmero de colises, a dimenso da tabela deve guardar
uma folga em relao ao nmero de elementos efetivamente armazenados. Como regra
emprica, no devemos permitir que a tabela tenha uma taxa de ocupao superior a
17-3
75%. Uma taxa de 50% em geral traz bons resultados. Uma taxa menor que 25% pode
representar um gasto excessivo de memria.
h(x)
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];
}
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;
}
17-6
h(x)
Com essa estratgia, cada elemento armazenado na tabela ser um elemento de uma
lista encadeada. Portanto, devemos prever, na estrutura da informao, um ponteiro
adicional para o prximo elemento da lista. Nossa estrutura de aluno passa a ser dada
por:
struct aluno {
int mat;
char nome[81];
char turma;
char email[41];
struct aluno* prox;
/* encadeamento na lista de coliso */
};
typedef struct aluno Aluno;
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;
}
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
Leitura de palavras
A primeira funo que vamos discutir responsvel por capturar a prxima seqncia
de letras do arquivo texto. Essa funo receber como parmetros o ponteiro para o
arquivo de entrada e a cadeia de caracteres que armazenar a palavra capturada. A
funo tem como valor de retorno um inteiro que indica se a leitura foi bem sucedida
(1) ou no (0). A prxima palavra capturada pulando os caracteres que no so letras
e, ento, capturando a seqncia de letras do arquivo. Para identificar se um caractere
ou no letra, usaremos a funo isalpha disponibilizada pela interface ctype.h.
17-9
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;
}
17-10
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;
}
17-11
Para ordenar o vetor (de poteiros para a estrutura Palavra ) utilizaremos a funo
qsort da biblioteca padro. Precisamos ento definir a funo de comparao, que
mostrada abaixo.
int compara (const void* v1, const void* v2)
{
Palavra** pp1 = (Palavra**)v1;
Palavra** pp2 = (Palavra**)v2;
Palavra* p1 = *pp1;
Palavra* p2 = *pp2;
if (p1->n > p2->n) return -1;
else if (p1->n < p2->n) return 1;
else return strcmp(p1->pal,p2->pal);
}
Por fim, podemos escrever a funo que, dada a tabela de disperso j preenchida e
utilizando as funes mostradas acima, conta o nmero de elementos, cria o vetor,
ordena-o e exibe o resultado na ordem desejada. Ao final, a funo libera o vetor criado
dinamicamente.
void imprime (Hash tab)
{
int i;
int n;
Palavra** vet;
/* cria e ordena vetor */
n = conta_elems(tab);
vet = cria_vetor(n,tab);
qsort(vet,n,sizeof(Palavra*),compara);
/* imprime ocorrencias */
for (i=0; i<n; i++)
printf("%s = %d\n",vet[i]->pal,vet[i]->n);
/* libera vetor */
free(vet);
}
Funo principal
Uma possvel funo principal desse programa mostrada a seguir. Esse programa
espera receber como dado de entrada o nome do arquivo cujas palavras queremos contar
o nmero de ocorrncias. Para exemplificar a utilizao dos parmetros da funo
17-12
principal, utilizamos esses parmetros para receber o nome do arquivo de entrada (para
detalhes, veja seo 6.3).
#include
#include
#include
#include
...
<stdio.h>
<string.h>
<ctype.h>
<stdlib.h>
17-13
Para ilustrar sua utlizao, podemos usar essa funo para imprimir os elementos. Para
tanto, devemos escrever a funo que executa a ao de imprimir cada elemento. Essa
funo pode ser dada por:
void imprime_elemento (Palavra* p)
{
printf("%s = %d\n",p->pal,p->n);
}
Assim, para imprimir os elementos da tabela bastaria chamar a funo percorre com a
ao acima passada como parmetro.
...
percorre(tab,imprime_elemento);
...
Essa mesma funo percorre pode ser usada para, por exemplo, contar o nmero de
elementos que existe armazenado na tabela. A ao associada aqui precisa apenas
incrementar um contador do nmero de vezes que a callback chamada. Para tanto,
devemos usar uma varivel global que representa esse contador e fazer a callback
incrementar esse contador cada vez que for chamada. Nesse caso, o ponteiro do
elemento passado como parmetro para a callback no utilizado, pois o incremento ao
contador independe do elemento. Assumindo que Total uma varivel global
inicializada com o valor zero, a ao para contar o nmero de elementos dada
simplesmente por:
17-14
J mencionamos que o uso de variveis globais deve, sempre que possvel, ser evitado,
pois seu uso indiscriminado torna um programa ilegvel e difcil de ser mantido. Para
evitar o uso de variveis globais nessas funes callbacks devemos arrumar um meio de
transferir, para a funo callback, um dado do cliente. A funo que percorre os
elementos no manipula esse dado, apenas o transfere para a funo callback. Como
no sabemos a priori o tipo de dado que ser necessrio, definimos a callback
recebendo dois parmetros: o elemento sendo visitado e um ponteiro genrico (void*).
O cliente chama a funo que percorre os elementos passando como parmetros a
funo callback e um ponteiro a ser repassado para essa mesma callback.
Vamos exemplificar o uso dessa estratgia re-implementando a funo que percorre os
elementos.
void percorre (Hash tab, void (*cb)(Palavra*, void*), void* dado)
{
int i;
Palavra* p;
for (i=0; i<NTAB; i++) {
for (p=tab[i]; p!=NULL; p=p->prox)
cb(p,dado);
/* passa para a callback o ponteiro recebido */
}
}
Agora, podemos usar essa nova verso da funo para contar o nmero de elementos,
sem usar varivel global. Primeiro temos que definir a callback, que, nesse caso,
receber um ponteiro para um inteiro que representa o contador.
void conta_elemento (Palavra* p, void* dado)
{
int *contador = (int*)dado;
(*contador)++;
}
Por fim, uma funo que conta o nmero de elemento, usando as funes acima,
mostrada a seguir.
int total_elementos (Hash tab)
{
int total = 0;
percorre(tab,conta_elemento,&total);
return total;
}
17-15