Escolar Documentos
Profissional Documentos
Cultura Documentos
Estruturas de Dados
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.
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.
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.
CPU
Central de
processamento
Armazenamento Dispositivos de
secundrio entrada/sada
Memria
Execuo
PS
Programa
Fonte IM
Interpretador Sada
Dados de
Entrada
PC
CM PM
Programa
Compilador Programa
Fonte
Objeto
Execuo
PM
Dados de Sada
Programa
Entrada
Objeto
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.
#include <stdio.h>
/* faz a conversao */
t2 = converte(t1);
/* exibe resultado */
printf("A temperatura em Fahrenheit : %f\n", t2);
return 0;
}
Como alternativa, possvel definir variveis que sejam externas s funes, isto ,
variveis globais, que podem ser acessadas pelo nome por qualquer funo subseqente
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
>
Arquivo converte.c:
/* Implementao do mdulo de converso */
Arquivo principal.c:
/* Programa para converso de temperatura */
#include <stdio.h>
/* faz a conversao */
t2 = converte(t1);
/* exibe resultado */
printf("A temperatura em Fahrenheit vale: %f\n", t2);
return 0;
}
A tarefa das bibliotecas permitir que funes de interesse geral estejam disponveis com
facilidade. Nosso exemplo usa a biblioteca de entrada/sada padro do C, stdio, que
oferece funes que permitem a captura de dados a partir do teclado e a sada de dados para
a tela. Alm de bibliotecas preparadas pelo fornecedor do compilador, ou por outros
fornecedores de software, podemos ter bibliotecas preparadas por um usurio qualquer, que
pode empacotar funes com utilidades relacionadas em uma biblioteca e, dessa maneira,
facilitar seu uso em outros programas.
Em alguns casos, a funo do ligador executada pelo prprio compilador. Por exemplo,
quando compilamos o primeiro programa prog.c, o ligador foi chamado automaticamente
para reunir o cdigo do programa aos cdigos de scanf, printf e de outras funes
necessrias execuo independente do programa.
Verificao e Validao
Outro ponto que deve ser observado que os programas podem conter (e, em geral,
contm) erros, que precisam ser identificados e corrigidos. Quase sempre a verificao
realizada por meio de testes, executando o programa a ser testado com diferentes valores de
entrada. Identificado um ou mais erros, o cdigo fonte corrigido e deve ser novamente
verificado. O processo de compilao, ligao e teste se repete at que os resultados dos
testes sejam satisfatrios e o programa seja considerado validado. Podemos descrever o
ciclo atravs da Figura 1.4.
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.
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.
1
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.
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.
a = 5; /* armazena o valor 5 em a */
b = 10; /* armazena o valor 10 em b */
c = 5.3; /* 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:
Uma vez declarada a varivel, podemos armazenar valores nos respectivos espaos de
memria. Estes valores devem ter o mesmo tipo da varivel, conforme ilustrado acima. No
possvel, por exemplo, armazenar um nmero real numa varivel do tipo int. Se
fizermos:
int a;
a = 4.3; /* a varivel armazenar o valor 4 */
ser armazenada em a apenas a parte inteira do nmero real, isto , 4. Alguns compiladores
exibem uma advertncia quando encontram este tipo de atribuio.
Valores constantes
Em nossos cdigos, usamos tambm valores constantes. Quando escrevemos a atribuio:
a = b + 123;
As constantes tambm podem ser do tipo real. Uma constante real deve ser escrita com um
ponto decimal ou valor de expoente. Sem nenhum sufixo, uma constante real do tipo
double. Se quisermos uma constante real do tipo float, devemos, a rigor, acrescentar o
sufixo F ou f. Alguns exemplos de constantes reais so:
float x;
...
x = 12.45;
pois o cdigo, a rigor, armazena um valor double (12.45) numa varivel do tipo float.
Desde que a constante seja representvel dentro de um float, no precisamos nos
preocupar com este tipo de advertncia.
int a, b, c;
a = 2;
c = a + b; /* ERRO: b tem lixo */
Alguns destes erros so bvios (como o ilustrado acima) e o compilador capaz de nos
reportar uma advertncia; no entanto, muitas vezes o uso de uma varivel no definida fica
difcil de ser identificado no cdigo. Repetimos que um erro comum em programas e uma
razo para alguns programas funcionarem na parte da manh e no funcionarem na parte da
tarde (ou funcionarem durante o desenvolvimento e no funcionarem quando entregamos
para nosso cliente!). Todos os erros em computao tm lgica. A razo de o programa
poder funcionar uma vez e no funcionar outra que, apesar de indefinido, o valor da
varivel existe. No nosso caso acima, pode acontecer que o valor armazenado na memria
ocupada por b seja 0, fazendo com que o programa funcione. Por outro lado, pode
acontecer de o valor ser 293423 e o programa no funcionar.
Operadores Aritmticos
Os operadores aritmticos binrios so: +, -, *, / e o operador mdulo %. H ainda o
operador unrio -. A operao feita na preciso dos operandos. Assim, a expresso 5/2
resulta no valor 2, pois a operao de diviso feita em preciso inteira, j que os dois
operandos (5 e 2) so constantes inteiras. A diviso de inteiros trunca a parte fracionria,
pois o valor resultante sempre do mesmo tipo da expresso. Conseqentemente, a
expresso 5.0/2.0 resulta no valor real 2.5 pois a operao feita na preciso real
(double, no caso).
O operador mdulo, %, no se aplica a valores reais, seus operandos devem ser do tipo
inteiro. Este operador produz o resto da diviso do primeiro pelo segundo operando. Como
exemplo de aplicao deste operador, podemos citar o caso em que desejamos saber se o
valor armazenado numa determinada varivel inteira x par ou mpar. Para tanto, basta
analisar o resultado da aplicao do operador %, aplicado varivel e ao valor dois.
x % 2 se resultado for zero nmero par
x % 2 se resultado for um nmero mpar
Operadores de atribuio
Na linguagem C, uma atribuio uma expresso cujo valor resultante corresponde ao
valor atribudo. Assim, da mesma forma que a expresso:
5 + 3
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.
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:
x *= y + 1;
equivale a
x = x * (y + 1)
e no a
x = x * y + 1;
n++;
x = n++;
atribui 5 a x, mas:
x = ++n;
atribuiria 6 a x. Em ambos os casos, n passa a valer 6, pois seu valor foi incrementado em
uma unidade. Os operadores de incremento e decremento podem ser aplicados somente em
variveis; uma expresso do tipo x = (i + 1)++ ilegal.
a = a + 1;
a += 1;
a++;
++a;
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
int a, b;
int c = 23;
int d = c + 4;
Operador sizeof
Outro operador fornecido por C, sizeof, resulta no nmero de bytes de um determinado
tipo. Por exemplo:
int a = sizeof(float);
armazena o valor 4 na varivel a, pois um float ocupa 4 bytes de memria. Este operador
pode tambm ser aplicado a uma varivel, retornando o nmero de bytes do tipo associado
varivel.
Converso de tipo
Em C, como na maioria das linguagens, existem converses automticas de valores na
avaliao de uma expresso. Assim, na expresso 3/1.5, o valor da constante 3 (tipo int)
promovido (convertido) para double antes de a expresso ser avaliada, pois o segundo
operando do tipo double (1.5) e a operao feita na preciso do tipo mais
representativo.
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).
int a, b;
a = (int) 3.5;
b = (int) 3.5 % 2;
Operador Associatividade
( ) [ ] -> . esquerda para direita
! ~ ++ -- - (tipo) * & sizeof(tipo) 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
= += -= etc. direita para esquerda
, esquerda para direita
#include <stdio.h>
Funo printf
A funo printf possibilita a sada de valores (sejam eles constantes, variveis ou
resultado de expresses) segundo um determinado formato. Informalmente, podemos dizer
que a forma da funo :
O primeiro parmetro uma cadeia de caracteres, em geral delimitada com aspas, que
especifica o formato de sada das constantes, variveis e expresses listadas em seguida.
Para cada valor que se deseja imprimir, deve existir um especificador de formato
correspondente na cadeia de caracteres formato. Os especificadores de formato variam
com o tipo do valor e a preciso em que queremos que eles sejam impressos. Estes
especificadores so precedidos pelo caractere % e podem ser, entre outros:
%c especifica um char
%d especifica um int
%u especifica um unsigned int
%f especifica um double (ou float)
%e especifica um double (ou float) no formato cientfico
%g especifica um double (ou float) no formato mais apropriado (%f ou %e)
%s especifica uma cadeia de caracteres
Alguns exemplos:
33 5.3
Ou:
com sada:
Ainda, se desejarmos ter como sada um caractere %, devemos, dentro do formato, escrever
%%.
%4d 3 3
4
%7.2f 5 . 3 0
2
7
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 :
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:
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.
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
...
}
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;
}
Devemos, todavia, ter cuidado com o aninhamento de comandos if-else. Para ilustrar,
consideremos o exemplo abaixo.
/* temperatura (versao 1 - incorreta) */
#include <stdio.h>
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>
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>
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;
...
O comando:
maximo = a > b ? a : b ;
n != n (n 1) (n 2)...3 2 1, onde 0 != 1
while (expr)
{
bloco de comandos
...
}
/* Fatorial */
#include <stdio.h>
/* calcula fatorial */
i = 1;
while (i <= n)
{
f *= i;
i++;
}
expr_inicial;
while (expr_booleana)
{
bloco de comandos
...
expr_de_incremento
}
/* Fatorial (versao 2) */
#include <stdio.h>
/* 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);
#include <stdio.h>
/* calcula fatorial */
for (i = 1; i <= n; i++)
f *= i;
#include <stdio.h>
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.
gera a sada:
0 1 2 3 4 6 7 8 9 fim
Devemos ter cuidado com a utilizao do comando continue nos laos while. O
programa:
/* INCORRETO */
#include <stdio.h>
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 :
opi deve ser um nmero inteiro ou uma constante caractere. Se expr resultar no valor opi,
os comandos que se seguem ao caso opi so executados, at que se encontre um break. Se
o comando break for omitido, a execuo do caso continua com os comandos do caso
seguinte. O caso default (nenhum dos outros) pode aparecer em qualquer posio, mas
normalmente colocado por ltimo. Para exemplificar, mostramos a seguir um programa
que implementa uma calculadora convencional que efetua as quatro operaes bsicas. Este
programa usa constantes caracteres, que sero discutidas em detalhe quando apresentarmos
cadeias de caracteres em C. O importante aqui entender conceitualmente a construo
switch.
#include <stdio.h>
#include <stdio.h>
/* Funo principal */
int main (void)
{
int n;
scanf("%d", &n);
fat(n);
return 0;
}
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.
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:
#include <stdio.h>
A transferncia de dados entre funes feita atravs dos parmetros e do valor de retorno
da funo chamada. Conforme mencionado, uma funo pode retornar um valor para a
funo que a chamou e isto feito atravs do comando return. Quando uma funo tem
um valor de retorno, a chamada da funo uma expresso cujo valor resultante o valor
retornado pela funo. Por isso, foi vlido escrevermos na funo main acima a expresso
r = fat(n); que chama a funo fat armazenando seu valor de retorno na varivel r.
A comunicao atravs dos parmetros requer uma anlise mais detalhada. Para ilustrar a
discusso, vamos considerar o exemplo abaixo, no qual a implementao da funo fat foi
ligeiramente alterada:
#include <stdio.h>
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
Podemos, ento, analisar passo a passo a evoluo do programa mostrado acima, ilustrando
o funcionamento da pilha de execuo.
1 - Incio do programa: pilha vazia 2 - Declarao das variveis: n, r 3 - Chamada da funo: cpia do parmetro
n 5
r - fat > r -
n 5 n 5
main > main > main >
f 1.0 f 120.0
n 5 n 0
fat > fat >
r - r - r 120.0
n 5 n 5 n 5
main > main > main >
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
int a;
declaramos uma varivel com nome a que pode armazenar valores inteiros.
Automaticamente, reserva-se um espao na memria suficiente para armazenar valores
inteiros (geralmente 4 bytes).
Da mesma forma que declaramos variveis para armazenar inteiros, podemos declarar
variveis que, em vez de servirem para armazenar valores inteiros, servem para armazenar
valores de endereos de memria onde h variveis inteiras armazenadas. C no reserva
uma palavra especial para a declarao de ponteiros; usamos a mesma palavra do tipo com
os nomes das variveis precedidas pelo caractere *. Assim, podemos escrever:
int *p;
Neste caso, declaramos uma varivel com nome p que pode armazenar endereos de
memria onde existe um inteiro armazenado. Para atribuir e acessar endereos de memria,
a linguagem oferece dois operadores unrios ainda no discutidos. O operador unrio &
(endereo de), aplicado a variveis, resulta no endereo da posio da memria reservada
para a varivel. O operador unrio * (contedo de), aplicado a variveis do tipo ponteiro,
acessa o contedo do endereo de memria armazenado pela varivel ponteiro. Para
exemplificar, vamos ilustrar esquematicamente, atravs de um exemplo simples, o que
ocorre na pilha de execuo. Consideremos o trecho de cdigo mostrado na figura abaixo.
/*varivel inteiro */
int a;
/*varivel ponteiro p/ inteiro */
112
int *p; p - 108
a - 104
112
/* a recebe o valor 5 */
p - 108
a = 5; a 5 104
/* p recebe o endereo de a
112
(diz-se p aponta para a) */ p 104
108
p = &a; a 5 104
a 6
imprime o valor 2.
float *m;
char *s;
#include <stdio.h>
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.
120
py 108 116
112 px 104 112
troca >b 7 108
b 7 108
a 5 104
a 5 104 main >
main >
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.
1, se n = 0
n!=
n (n 1)!, se n > 0
Uma das diretivas reconhecidas pelo pr-processador, e j utilizada nos nossos exemplos,
#include. Ela seguida por um nome de arquivo e o pr-processador a substitui pelo
corpo do arquivo especificado. como se o texto do arquivo includo fizesse parte do
cdigo fonte.
Uma observao: quando o nome do arquivo a ser includo envolto por aspas
("arquivo"), o pr-processador procura primeiro o arquivo no diretrio atual e, caso no o
encontre, o procura nos diretrios de include especificados para compilao. Se o arquivo
colocado entre os sinais de menor e maior (<arquivo>), o pr-processador no procura o
arquivo no diretrio atual.
Outra diretiva de pr-processamento que muito utilizada e que ser agora discutida a
diretiva de definio. Por exemplo, uma funo para calcular a rea de um crculo pode ser
escrita da seguinte forma:
#define PI 3.14159
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:
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
#include <stdio.h>
#define DIF(a,b) a - b
o resultado impresso 17 e no 8, como poderia ser esperado. A razo simples, pois para
o compilador (fazendo a substituio da macro) est escrito:
#include <stdio.h>
#define PROD(a,b) (a * b)
Conclumos, portanto, que, como regra bsica para a definio de macros, devemos
envolver cada parmetro, e a macro como um todo, com parnteses.
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
v 104
Mas:
(x m )
m= , v=
2
x
N N
#include <stdio.h>
/* clculo da mdia */
med = 0.0; /* inicializa mdia com zero */
for (i = 0; i < 10; i++)
med = med + v[i]; /* acumula soma dos elementos */
med = med / 10; /* calcula a mdia */
/* clculo da varincia */
var = 0.0; /* inicializa varincia com zero */
for ( i = 0; i < 10; i++ )
var = var+(v[i]-med)*(v[i]-med); /* acumula quadrado da diferena */
var = var / 10; /* calcula a varincia */
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.
ou simplesmente:
Neste ltimo caso, a linguagem dimensiona o vetor pelo nmero de elementos inicializados.
Para exemplificar, vamos modificar o cdigo do exemplo acima, usando funes separadas
para o clculo da mdia e da varincia. (Aqui, usamos ainda os operadores de atribuio +=
para acumular as somas.)
#include <stdio.h>
med = media(10,v);
var = variancia(10,v,med);
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>
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.
1
A rigor, a alocao dos recursos bem mais complexa e varia para cada sistema operacional.
Variveis
Globais e Estticas
Memria Alocada
Dinamicamente
Memria Livre
Pilha
v = malloc(10*4);
Aps este comando, se a alocao for bem sucedida, v armazenar o endereo inicial de
uma rea contnua de memria suficiente para armazenar 10 valores inteiros. Podemos,
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 Cdigo do
Programa Programa
Variveis Variveis
Globais e Estticas Globais e Estticas
40 bytes
504
Livre
Livre
v - 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.
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.
#include <stdio.h>
#include <stdlib.h>
...
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 1 2 3 4 5 6 7 8 9
30 sp ! " # $ % & '
40 ( ) * + , - . / 0 1
50 2 3 4 5 6 7 8 9 : ;
60 < = > ? @ A B C D E
70 F G H I J K L M N O
80 P Q R S T U V W X Y
90 Z [ \ ] ^ _ ` a b c
100 d e f g h i j k l m
110 n o p q r S t u v w
120 x y z { | } ~
1
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).
char c = 97;
printf("%d %c\n",c,c);
char c = 'a';
printf("%d %c\n", c, c);
Alm de agregar portabilidade e clareza ao cdigo, o uso de constantes caracteres nos livra
de conhecermos os cdigos associados a cada caractere.
Exemplo. Suponhamos que queremos escrever uma funo para testar se um caractere c
um dgito (um dos caracteres entre '0' e '9'). Esta funo pode ter o prottipo:
int digito(char c);
e ter como resultado 1 (verdadeiro) se c for um dgito, e 0 (falso) se no for.
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.
char a;
...
scanf("%c", &a);
...
Desta forma, se o usurio digitar a letra r, por exemplo, o cdigo associado letra r ser
armazenado na varivel a. Vale ressaltar que, diferente dos especificadores %d e %f, o
especificador %c no pula os caracteres brancos2. Portanto, se o usurio teclar um espao
2
Um caractere branco pode ser um espao (' '), um caractere de tabulao ('\t') ou um caractere de
nova linha ('\n').
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).
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';
}
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.
Vamos reescrever algumas das funes mostradas acima, agora com a verso recursiva.
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]);
}
}
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:
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]);
}
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]);
}
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
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;
#include <stdio.h>
struct ponto {
float x;
float y;
};
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.
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!
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);
}
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];
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;
};
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;
Por fim, vale salientar que podemos definir a estrutura e associar mnemnicos para elas em
um mesmo comando:
typedef struct ponto {
float x;
float y;
} Ponto, *PPonto;
y pi+1
pi
yi yi+1
xi xi+1 x
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;
}
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.
Para estruturar esses dados, podemos definir um tipo que representa os dados de um aluno:
struct aluno {
char nome[81];
int mat;
char end[121];
char tel[21];
};
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;
...
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));
1
Provavelmente o tipo ocupar mais espao, pois os dados tm que estar alinhados para serem armazenados
na memria.
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.
union exemplo v;
O acesso aos campos de uma unio anlogo ao acesso a campos de uma estrutura.
Usamos o operador ponto (.) para acess-los diretamente e o operador seta (->) para
acess-los atravs de um ponteiro da unio. Assim, dada a declarao acima, podemos
escrever:
v.i = 10;
ou
v.c = 'x';
Salientamos, no entanto, que apenas um nico elemento de uma unio pode estar
armazenado num determinado instante, pois a atribuio a um campo da unio sobrescreve
o valor anteriormente atribudo a qualquer outro campo.
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).
onde resultado representa uma varivel que pode receber apenas os valores FALSE (0)
ou TRUE (1).
1
O termo esttico aqui refere-se ao fato de no usarmos alocao dinmica.
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).
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.
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[], ...);.
j=2
i=1 a b c d
e f g h
I j k l
a b c d e f g h I j k l
k = i*n+j = 1*4+2 = 6
Figura 8.2: Matriz representada por vetor simples.
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.
j=2
i=1 a b c d
e f g h
I j k l
j=2
i=1 a b c d
e f g h
I j k l
A alocao da matriz agora mais elaborada. Primeiro, temos que alocar o vetor de
ponteiros. Em seguida, alocamos cada uma das linhas da matriz, atribuindo seus endereos
aos elementos do vetor de ponteiros criado. O fragmento de cdigo abaixo ilustra esta
codificao:
int i;
float **mat; /* matriz representada por um vetor de ponteiros */
...
mat = (float**) malloc(m*sizeof(float*));
for (i=0; i<m; i++)
m[i] = (float*) malloc(n*sizeof(float));
A grande vantagem desta estratgia que o acesso aos elementos feito da mesma forma
que quando temos uma matriz criada estaticamente, pois, se mat representa uma matriz
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));
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:
struct matriz {
int lin;
int col;
float** v;
};
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;
}
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];
}
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.
(n 2 n) n (n + 1)
s =n+ =
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.
n (n + 1)
s = 1 + 2 + ... + n =
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.
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),
Para criar a matriz, basta alocarmos um nmero varivel de elementos para cada linha. O
cdigo abaixo ilustra uma possvel implementao:
O acesso aos elementos natural, desde que tenhamos o cuidado de no acessar elementos
que no estejam explicitamente alocados (isto , elementos com i<j).
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.
No caso de um programa composto por vrios mdulos, cada um desses mdulos deve
ser compilado separadamente, gerando um arquivo objeto (geralmente um arquivo com
extenso .o ou .obj) para cada mdulo. Aps a compilao de todos os mdulos, uma
outra ferramenta, denominada ligador, usada para juntar todos os arquivos objeto em
um nico arquivo executvel.
Para programas pequenos, o uso de vrios mdulos pode no se justificar. Mas para
programas de mdio e grande porte, a sua diviso em vrios mdulos uma tcnica
fundamental, pois facilita a diviso de uma tarefa maior e mais complexa em tarefas
menores e, provavelmente, mais fceis de implementar e de testar. Alm disso, um
mdulo com funes C pode ser utilizado para compor vrios programas, e assim
poupar muito tempo de programao.
Para ilustrar o uso de mdulos em C, considere que temos um arquivo str.c que
contm apenas a implementao das funes de manipulao de strings comprimento,
copia e concatena vistas no captulo 6. Considere tambm que temos um arquivo
prog1.c com o seguinte cdigo:
#include <stdio.h>
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"
Como nosso primeiro exemplo de TAD, vamos considerar a criao de um tipo de dado
para representar um ponto no R2. Para isso, devemos definir um tipo abstrato, que
denominaremos de Ponto, e o conjunto de funes que operam sobre esse tipo. Neste
exemplo, vamos considerar as seguintes operaes:
cria: operao que cria um ponto com coordenadas x e y;
libera: operao que libera a memria alocada por um ponto;
acessa: operao que devolve as coordenadas de um ponto;
atribui: operao que atribui novos valores s coordenadas de um ponto;
distancia: operao que calcula a distncia entre dois pontos.
/* 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);
Agora, mostraremos uma implementao para esse tipo abstrato de dados. O arquivo de
implementao do mdulo (arquivo ponto.c ) deve sempre incluir o arquivo de
interface do mdulo. Isto necessrio por duas razes. Primeiro, podem existir
definies na interface que so necessrias na implementao. No nosso caso, por
exemplo, precisamos da definio do tipo Ponto. A segunda razo garantirmos que as
funes implementadas correspondem s funes da interface. Como o prottipo das
funes exportadas includo, o compilador verifica, por exemplo, se os parmetros das
funes implementadas equivalem aos parmetros dos prottipos. Alm da prpria
interface, precisamos naturalmente incluir as interfaces das funes que usamos da
biblioteca padro.
#include <stdlib.h> /* malloc, free, exit */
#include <stdio.h> /* printf */
#include <math.h> /* sqrt */
#include "ponto.h"
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.
/* 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);
Arquivo matriz1.c:
#include <stdlib.h> /* malloc, free, exit */
#include <stdio.h> /* printf */
#include "matriz.h"
struct matriz {
int lin;
int col;
float* v;
};
Arquivo matriz2.c:
#include <stdlib.h> /* malloc, free, exit */
#include <stdio.h> /* printf */
#include "matriz.h"
struct matriz {
int lin;
int col;
float** v;
};
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.
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.
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.
prim
10.1.
Info1 Info2 Info3 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*).
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
Funo de insero
Uma vez criada a lista vazia, podemos inserir novos elementos nela. Para cada elemento
inserido na lista, devemos alocar dinamicamente a memria necessria para armazenar
o elemento e encade-lo na lista existente. A funo de insero mais simples insere o
novo elemento no incio da lista.
Uma possvel implementao dessa funo mostrada a seguir. Devemos notar que o
ponteiro que representa a lista deve ter seu valor atualizado, pois a lista deve passar a
ser representada pelo ponteiro para o novo primeiro elemento. Por esta razo, a funo
de insero recebe como parmetros de entrada a lista onde ser inserido o novo
elemento e a informao do novo elemento, e tem como valor de retorno a nova lista,
representada pelo ponteiro para o novo elemento.
/* insero no incio: retorna a lista atualizada */
Lista* insere (Lista* l, int i)
{
Lista* novo = (Lista*) malloc(sizeof(Lista));
novo->info = i;
novo->prox = l;
return novo;
}
Esta funo aloca dinamicamente o espao para armazenar o novo n da lista, guarda a
informao no novo n e faz este n apontar para (isto , ter como prximo elemento) o
elemento que era o primeiro da lista. A funo ento retorna o novo valor que
representa a lista, que o ponteiro para o novo primeiro elemento. A Figura 9.3 ilustra a
operao de insero de um novo elemento no incio da lista.
prim
Novo
10.2.
ULL
A seguir, ilustramos um trecho de cdigo que cria uma lista inicialmente vazia e insere
nela novos elementos.
Observe que no podemos deixar de atualizar a varivel que representa a lista a cada
insero de um novo elemento.
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.
prim
10.3.
ULL
Info1 Info2 Info3
prim
/* retira elemento */
if (ant == NULL) {
/* retira elemento do inicio */
l = p->prox;
}
else {
/* retira elemento do meio da lista */
ant->prox = p->prox;
}
free(p);
return l;
}
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).
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.
Novo
/* insere elemento */
if (ant == NULL) { /* insere elemento no incio */
novo->prox = l;
l = novo;
}
else { /* insere elemento no meio da lista */
novo->prox = ant->prox;
ant->prox = novo;
}
return l;
}
Devemos notar que essa funo, analogamente ao observado para a funo de remoo,
tambm funciona se o elemento tiver que ser inserido no final da lista.
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 */
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.
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;
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.
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;
}
b*h
r = b*h t= c = p r2
2
struct triangulo {
float b;
float h;
};
typedef struct triangulo Triangulo;
struct circulo {
float r;
};
typedef struct circulo Circulo;
Como identificador de tipo, podemos usar valores inteiros definidos como constantes
simblicas:
#define RET 0
#define TRI 1
#define CIR 2
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.
/* aloca retngulo */
r = (Retangulo*) malloc(sizeof(Retangulo));
r->b = b;
r->h = h;
/* aloca n */
p = (ListaGen*) malloc(sizeof(ListaGen));
p->tipo = RET;
p->info = r;
p->prox = NULL;
return p;
}
/* aloca tringulo */
t = (Triangulo*) malloc(sizeof(Triangulo));
t->b = b;
t->h = h;
/* aloca n */
p = (ListaGen*) malloc(sizeof(ListaGen));
p->tipo = TRI;
p->info = t;
p->prox = NULL;
return p;
}
/* aloca crculo */
c = (Circulo*) malloc(sizeof(Circulo));
c->r = r;
/* aloca n */
p = (ListaGen*) malloc(sizeof(ListaGen));
p->tipo = CIR;
p->info = c;
p->prox = NULL;
return p;
}
Uma vez criado o n, podemos inseri-lo na lista como j vnhamos fazendo com ns de
listas homogneas. As constantes simblicas que representam os tipos dos objetos
podem ser agrupadas numa enumerao (ver seo 7.5):
switch (p->tipo) {
case RET:
{
/* converte para retngulo e calcula rea */
Retangulo *r = (Retangulo*) p->info;
a = r->b * r->h;
}
break;
case TRI:
{
/* converte para tringulo e calcula rea */
Triangulo *t = (Triangulo*) p->info;
a = (t->b * t->h) / 2;
}
break;
case CIR:
{
/* converte para crculo e calcula rea */
Circulo *c = (Circulo)p->info;
a = PI * c->r * c->r;
}
break;
}
return a;
}
Numa lista circular, o ltimo elemento tem como prximo o primeiro elemento da lista,
formando um ciclo. A rigor, neste caso, no faz sentido falarmos em primeiro ou ltimo
1
Este cdigo no vlido em C++. A linguagem C++ no tem converso implcita de um ponteiro
genrico para um ponteiro especfico. Para compilar em C++, devemos fazer a converso explicitamente.
Por exemplo:
a = ret_area((Retangulo*)p->info);
ini
Exerccio: Escreva as funes para inserir e retirar um elemento de uma lista circular.
prim
Funo de insero
O cdigo a seguir mostra uma possvel implementao da funo que insere novos
elementos no incio da lista. Aps a alocao do novo elemento, a funo acertar o
duplo encadeamento.
/* insero no incio */
Lista2* insere (Lista2* l, int v)
{
Lista2* novo = (Lista2*) malloc(sizeof(Lista2));
novo->info = v;
novo->prox = l;
novo->ant = NULL;
/* verifica se lista no est vazia */
if (l != NULL)
l->ant = novo;
return novo;
}
Nessa funo, o novo elemento encadeado no incio da lista. Assim, ele tem como
prximo elemento o antigo primeiro elemento da lista e como anterior o valor NULL. A
seguir, a funo testa se a lista no era vazia, pois, neste caso, o elemento anterior do
ento primeiro elemento passa a ser o novo elemento. De qualquer forma, o novo
elemento passa a ser o primeiro da lista, e deve ser retornado como valor da lista
atualizada. A Figura 9.9 ilustra a operao de insero de um novo elemento no incio
da lista.
Novo
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.
if (p == NULL)
return l; /* no achou o elemento: retorna lista inalterada */
if (p->prox != NULL)
p->prox->ant = p->ant;
free(p);
return l;
}
Exerccio: Escreva as funes para inserir e retirar um elemento de uma lista circular
duplamente encadeada.
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).
Existem duas operaes bsicas que devem ser implementadas numa estrutura de pilha:
a operao para empilhar um novo elemento, inserindo-o no topo, e a operao para
desempilhar um elemento, removendo-o do topo. comum nos referirmos a essas duas
operaes pelos termos em ingls push (empilhar) e pop (desempilhar). A Figura 10.1
ilustra o funcionamento conceitual de uma pilha.
push (a) push (b) push (c) pop () push (d) pop ()
retorna-se c retorna-se d
c topo d topo
b topo b b topo b b topo
a topo a a a a a
H vrias implementaes possveis de uma pilha, que se distinguem pela natureza dos
seus elementos, pela maneira como os elementos so armazenados e pelas operaes
disponveis para o tratamento da pilha.
O arquivo pilha.h, que representa a interface do tipo, pode conter o seguinte cdigo:
typedef struct pilha Pilha;
A estrutura que representa o tipo pilha deve, portanto, ser composta pelo vetor e pelo
nmero de elementos armazenados.
#define MAX 50
struct pilha {
int n;
float vet[MAX];
};
Para inserir um elemento na pilha, usamos a prxima posio livre do vetor. Devemos
ainda assegurar que exista espao para a insero do novo elemento, tendo em vista que
trata-se de um vetor com dimenso fixa.
void push (Pilha* p, float v)
{
if (p->n == MAX) { /* capacidade esgotada */
printf("Capacidade da pilha estourou.\n");
exit(1); /* aborta programa */
}
/* insere elemento na prxima posio livre */
p->vet[p->n] = v;
p->n++;
}
A funo pop retira o elemento do topo da pilha, fornecendo seu valor como retorno.
Podemos tambm verificar se a pilha est ou no vazia.
float pop (Pilha* p)
{
float v;
if (vazia(p)) {
printf("Pilha vazia.\n");
exit(1); /* aborta programa */
}
/* retira elemento do topo */
v = p->vet[p->n-1];
p->n--;
return v;
}
A funo que verifica se a pilha est vazia pode ser dada por:
int vazia (Pilha* p)
{
return (p->n == 0);
}
Finalmente, a funo para liberar a memria alocada pela pilha pode ser:
void libera (Pilha* p)
{
free(p);
}
A funo cria aloca a estrutura da pilha e inicializa a lista como sendo vazia.
Pilha* cria (void)
{
Pilha* p = (Pilha*) malloc(sizeof(Pilha));
p->prim = NULL;
return p;
}
O primeiro elemento da lista representa o topo da pilha. Cada novo elemento inserido
no incio da lista e, conseqentemente, sempre que solicitado, retiramos o elemento
tambm do incio da lista. Desta forma, precisamos de duas funes auxiliares da lista:
para inserir no incio e para remover do incio. Ambas as funes retornam o novo
primeiro n da lista.
/* funo auxiliar: insere no incio */
No* ins_ini (No* l, float v)
{
No* p = (No*) malloc(sizeof(No));
p->info = v;
p->prox = l;
return p;
}
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);
}
/* funes exportadas */
Calc* cria_calc (char* f);
void operando (Calc* c, float v);
void operador (Calc* c, char op);
void libera_calc (Calc* c);
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);
/* desempilha operandos */
if (vazia(c->p))
v2 = 0.0;
else
v2 = pop(c->p);
if (vazia(c->p))
v1 = 0.0;
else
v1 = pop(c->p);
/* faz operao */
switch (op) {
case '+': v = v1+v2; break;
case '-': v = v1-v2; break;
case '*': v = v1*v2; break;
case '/': v = v1/v2; break;
}
/* empilha resultado */
push(c->p,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);
}
#include <stdio.h>
#include "calc.h"
do {
/* le proximo caractere nao branco */
scanf(" %c",&c);
/* verifica se e' operador valido */
if (c=='+' || c=='-' || c=='*' || c=='/') {
operador(calc,c);
}
/* devolve caractere lido e tenta ler nmero */
else {
ungetc(c,stdin);
if (scanf("%f",&v) == 1)
operando(calc,v);
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.
A estrutura de fila uma analogia natural com o conceito de fila que usamos no nosso
dia a dia: quem primeiro entra numa fila o primeiro a ser atendido (a sair da fila). Um
exemplo de utilizao em computao a implementao de uma fila de impresso. Se
uma impressora compartilhada por vrias mquinas, deve-se adotar uma estratgia
para determinar que documento ser impresso primeiro. A estratgia mais simples
tratar todas as requisies com a mesma prioridade e imprimir os documentos na ordem
em que foram submetidos o primeiro submetido o primeiro a ser impresso.
De modo anlogo ao que fizemos com a estrutura de pilha, neste captulo discutiremos
duas estratgias para a implementao de uma estrutura de fila: usando vetor e usando
lista encadeada. Para implementar uma fila, devemos ser capazes de inserir novos
elementos em uma extremidade, o fim, e retirar elementos da outra extremidade, o
incio.
O arquivo fila.h, que representa a interface do tipo, pode conter o seguinte cdigo:
typedef struct fila Fila;
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
0 1 2 3 4 5
0 1 2 3 4 5
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 1 98 99
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
:
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.
struct fila {
int ini, fim;
float vet[N];
};
A funo para criar a fila aloca dinamicamente essa estrutura e inicializa a fila como
sendo vazia, isto , com os ndices ini e fim iguais entre si (no caso, usamos o valor
zero).
Fila* cria (void)
{
Fila* f = (Fila*) malloc(sizeof(Fila));
f->ini = f->fim = 0; /* inicializa fila vazia */
return f;
}
Para inserir um elemento na fila, usamos a prxima posio livre do vetor, indicada por
fim. Devemos ainda assegurar que h espao para a insero do novo elemento, tendo
em vista que trata-se de um vetor com capacidade limitada. Consideraremos que a
funo auxiliar que faz o incremento circular est disponvel.
void insere (Fila* f, float v)
{
if (incr(f->fim) == f->ini) { /* fila cheia: capacidade esgotada
*/
printf("Capacidade da fila estourou.\n");
exit(1); /* aborta programa */
}
/* insere elemento na prxima posio livre */
f->vet[f->fim] = v;
f->fim = incr(f->fim);
}
A funo para retirar o elemento do incio da fila fornece o valor do elemento retirado
como retorno. Podemos tambm verificar se a fila est ou no vazia.
float retira (Fila* f)
{
float v;
if (vazia(f)) {
printf("Fila vazia.\n");
exit(1); /* aborta programa */
}
/* retira elemento do incio */
v = f->vet[f->ini];
f->ini = incr(f->ini);
return v;
}
A funo que verifica se a fila est vazia pode ser dada por:
int vazia (Fila* f)
{
return (f->ini == f->fim);
}
ini fim
O n da lista para armazenar valores reais, como j vimos, pode ser dado por:
struct no {
float info;
struct no* prox;
};
typedef struct no No;
A funo cria aloca a estrutura da fila e inicializa a lista como sendo vazia.
Fila* cria (void)
{
Fila* f = (Fila*) malloc(sizeof(Fila));
f->ini = f->fim = NULL;
return f;
}
Cada novo elemento inserido no fim da lista e, sempre que solicitado, retiramos o
elemento do incio da lista. Desta forma, precisamos de duas funes auxiliares de lista:
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;
}
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]);
}
#include <stdio.h>
#include "fila.h"
O arquivo fila2.h, que representa a interface do tipo, pode conter o seguinte cdigo:
typedef struct fila2 Fila2;
Para solucionar esse problema, temos que lanar mo da estrutura de lista duplamente
encadeada (veja a seo 9.5). Nessa lista, cada n guarda, alm da referncia para o
prximo elemento, uma referncia para o elemento anterior: dado o ponteiro de um n,
podemos acessar ambos os elementos adjacentes. Este arranjo resolve o problema de
acessarmos o elemento anterior ao ltimo. Devemos salientar que o uso de uma lista
duplamente encadeada para implementar a fila bastante simples, pois s manipulamos
os elementos das extremidades da lista.
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;
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;
}
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 sub-
diretrios 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.
n raiz
... sub-rvores
* 5
3 6 4 1
Numa rvore binria, cada n tem zero, um ou dois filhos. De maneira recursiva,
podemos definir uma rvore binria como sendo:
uma rvore vazia; ou
um n raiz tendo duas sub-rvores, identificadas como a sub-rvore da
direita (sad) e a sub-rvore da esquerda (sae).
A Figura 13.4 ilustra a definio de rvore binria. Essa definio recursiva ser usada
na construo de algoritmos, e na verificao (informal) da correo e do desempenho
dos mesmos.
vazia
sae sad
b c
d e f
Figura 13.5: Exemplo de rvore binria
Para descrever rvores binrias, podemos usar a seguinte notao textual: a rvore vazia
representada por <>, e rvores no vazias por <raiz sae sad>. Com essa notao,
a rvore da Figura 13.5 representada por:
<a<b<><d<><>>><c<e<><>><f<><>>>>.
Pela definio, uma sub-rvore de uma rvore binria sempre especificada como
sendo a sae ou a sad de uma rvore maior, e qualquer das duas sub-rvores pode ser
vazia. Assim, as duas rvores da Figura 13.6 so distintas.
a a
b b
Isto tambm pode ser visto pelas representaes textuais das duas rvores, que so,
respectivamente: <a <b<><>> <> > e <a <> <b<><>> >.
Exerccio: Mostrar que uma rvore binria de altura h tem, no mnimo, h+1 ns, e, no
mximo, 2h+1 1.
Representao em C
Anlogo ao que fizemos para as demais estruturas de dados, podemos definir um tipo
para representar uma rvore binria. Para simplificar a discusso, vamos considerar que
a informao que queremos armazenar nos ns da rvore so valores de caracteres
simples. Vamos inicialmente discutir como podemos representar uma estrutura de
rvore binria em C. Que estrutura podemos usar para representar um n da rvore?
Cada n deve armazenar trs informaes: a informao propriamente dita, no caso um
caractere, e dois ponteiros para as sub-rvores, esquerda e direita. Ento a estrutura
de C para representar o n da rvore pode ser dada por:
struct arv {
char info;
struct arv* esq;
struct arv* dir;
};
Da mesma forma que uma lista encadeada representada por um ponteiro para o
primeiro n, a estrutura da rvore como um todo representada por um ponteiro para o
n raiz.
Como acontece com qualquer TAD (tipo abstrato de dados), as operaes que fazem
sentido para uma rvore binria dependem essencialmente da forma de utilizao que se
pretende fazer da rvore. Nesta seo, em vez de discutirmos a interface do tipo abstrato
para depois mostrarmos sua implementao, vamos optar por discutir algumas
operaes mostrando simultaneamente suas implementaes. Ao final da seo
apresentaremos um arquivo que pode representar a interface do tipo. Nas funes que se
seguem, consideraremos que existe o tipo Arv definido por:
typedef struct arv Arv;
Vamos procurar identificar e descrever apenas operaes cuja utilidade seja a mais geral
possvel. Uma operao que provavelmente dever ser includa em todos os casos a
inicializao de uma rvore vazia. Como uma rvore representada pelo endereo do
n raiz, uma rvore vazia tem que ser representada pelo valor NULL. Assim, a funo
que inicializa uma rvore vazia pode ser simplesmente:
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;
}
Exemplo: Usando as operaes inicializa e cria, crie uma estrutura que represente
a rvore da Figura 13.5.
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:
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.
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.
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 sae */
libera(a->dir); /* libera sad */
free(a); /* libera raiz */
}
return NULL;
}
Devemos notar que a definio de rvore, por ser recursiva, no faz distino entre
rvores e sub-rvores. Assim, cria pode ser usada para acrescentar (enxertar) uma
sub-rvore em um ramo de uma rvore, e libera pode ser usada para remover
(podar) uma sub-rvore qualquer de uma rvore dada.
importante observar que, anlogo ao que fizemos para a lista, o cdigo cliente que
chama a funo libera responsvel por atribuir o valor atualizado retornado pela
funo, no caso uma rvore vazia. No exemplo acima, se no tivssemos feito a
atribuio, o endereo armazenado em r->dir->esq seria o de uma rea de memria
no mais em uso.
Exerccio: Escreva uma funo que percorre uma rvore binria para determinar sua
altura. O prottipo da funo pode ser dado por:
int altura(Arv* a);
Uma outra funo que podemos considerar percorre a rvore buscando a ocorrncia de
um determinado caractere c em um de seus ns. Essa funo tem como retorno um
valor booleano (um ou zero) indicando a ocorrncia ou no do caractere na rvore.
int busca (Arv* a, char c){
if (vazia(a))
return 0; /* rvore vazia: no encontrou */
else
return a->info==c || busca(a->esq,c) || busca(a->dir,c);
}
Note que esta forma de programar busca, em C, usando o operador lgico || (ou)
faz com que a busca seja interrompida assim que o elemento encontrado. Isto acontece
porque se c==a->info for verdadeiro, as duas outras expresses no chegam a ser
avaliadas. Analogamente, se o caractere for encontrado na sub-rvore da esquerda, a
busca no prossegue na sub-rvore da direita.
equivalente a:
Para funo para liberar a rvore, por exemplo, tivemos que adotar a ps-ordem:
libera(a->esq); /* libera sae */
libera(a->dir); /* libera sad */
free(a); /* libera raiz */
Como veremos, as funes para manipularem uma rvore genrica tambm sero
implementadas de forma recursiva, e sero baseadas na seguinte definio: uma rvore
genrica composta por:
um n raiz; e
zero ou mais sub-rvores.
Estritamente, segundo essa definio, uma rvore no pode ser vazia, e a rvore vazia
no sequer mencionada na definio. Assim, uma folha de uma rvore no um n
com sub-rvores vazias, como no caso da rvore binria, mas um n com zero sub-
rvores. Em qualquer definio recursiva deve haver uma condio de contorno, que
permita a definio de estruturas finitas, e, no nosso caso, a definio de uma rvore se
encerra nas folhas, que so identificadas como sendo ns com zero sub-rvores.
Como as funes que implementaremos nesta seo sero baseadas nessa definio, no
ser considerado o caso de rvores vazias. Esta pequena restrio simplifica as
implementaes recursivas e, em geral, no limita a utilizao da estrutura em
aplicaes reais. Uma rvore de diretrio, por exemplo, nunca vazia, pois sempre
existe o diretrio base o diretrio raiz.
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.
b f g
c e h i
d j
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.
b f g
c e h i
d j
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);
}
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 sub-
rvores 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);
}
A outra grande diferena entre memria principal e secundria (disco) consiste no fato
das informaes em disco serem persistentes, e em geral so lidas por programas e
pessoas diferentes dos que as escreveram, o que faz com que seja mais prtico atribuir
nomes aos elementos de informao armazenados do disco (em vez de endereos),
falando assim em arquivos e diretrios (pastas). Cada arquivo identificado por seu
nome e pelo diretrio onde encontra-se armazenado numa determinada unidade de
disco. Os nomes dos arquivos so, em geral, compostos pelo nome em si, seguido de
uma extenso. A extenso pode ser usada para identificar a natureza da informao
armazenada no arquivo ou para identificar o programa que gerou (e capaz de
interpretar) o arquivo. Assim, a extenso .c usada para identificar arquivos que tm
cdigos fontes da linguagem C e a extenso .doc , no Windows, usada para
identificar arquivos gerados pelo editor Word da Microsoft.
Um arquivo pode ser visto de duas maneiras, na maioria dos sistemas operacionais: em
modo texto, como um texto composto de uma seqncia de caracteres, ou em modo
binrio, como uma seqncia de bytes (nmeros binrios). Podemos optar por salvar (e
recuperar) informaes em disco usando um dos dois modos, texto ou binrio. Uma
vantagem do arquivo texto que pode ser lido por uma pessoa e editado com editores
de textos convencionais. Em contrapartida, com o uso de um arquivo binrio possvel
salvar (e recuperar) grandes quantidades de informao de forma bastante eficiente. O
sistema operacional pode tratar arquivos texto de maneira diferente da utilizada para
tratar arquivos binrios. Em casos especiais, pode ser interessante tratar arquivos de
um tipo como se fossem do outro, tomando os cuidados apropriados.
Uma das informaes que mantida pelo sistema operacional um ponteiro de arquivo
(file pointer), que indica a posio de trabalho no arquivo. Para ler um arquivo, este
apontador percorre o arquivo, do incio at o fim, conforme os dados vo sendo
recuperados (lidos) para a memria. Para escrever, normalmente, os dados so
acrescentados quando o apontador se encontra no fim do arquivo.
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.
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.
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.
Apesar do tipo do valor de retorno ser int, o valor retornado o caractere lido. Se o
fim do arquivo for alcanado, a constante EOF (end of file) retornada.
Uma outra funo muito utilizada para ler linhas de um arquivo a funo fgets. Essa
funo recebe como parmetros trs valores: a cadeia de caracteres que armazenar o
contedo lido do arquivo, o nmero mximo de caracteres que deve ser lido e o ponteiro
do arquivo. O prottipo da funo :
char* fgets (char* s, int n, FILE* fp);
A funo fprintf anloga a funo printf que temos usado para imprimir dados
na sada padro em geral, o monitor. A diferena consiste na presena do parmetro
que indica o arquivo para o qual o dado ser salvo. O valor de retorno dessa funo
representa o nmero de bytes escritos no arquivo. O prottipo da funo dado por:
int fprintf(FILE* fp, char* formato, ...);
O valor de retorno dessa funo o prprio caractere escrito, ou EOF se ocorrer um erro
na escrita.
#include <stdio.h>
/* l caractere a caractere */
while ((c = fgetc(fp)) != EOF) {
if (c == '\n')
nlinhas++;
}
/* fecha arquivo */
fclose(fp);
return 0;
}
#include <stdio.h>
#include <ctype.h> /* funo toupper */
/* fecha arquivos */
fclose(e);
fclose(s);
return 0;
}
Para implementar esse programa, vamos utilizar a funo strstr , que procura a
ocorrncia de uma sub-cadeia numa cadeia de caracteres maior. A funo retorna o
endereo da primeira ocorrncia ou NULL , se a sub-cadeia no for encontrada. O
prottipo dessa funo :
char* strstr (char* s, char* sub);
#include <stdio.h>
#include <string.h> /* funo strstr */
/* l linha a linha */
while (fgets(linha,121,fp) != NULL) {
n++;
if (strstr(linha,subcadeia) != NULL) {
achou = 1;
break;
}
}
/* fecha arquivo */
fclose(fp);
/* exibe sada */
if (achou)
printf("Achou na linha %d.\n", n);
else
printf("Nao achou.");
return 0;
}
Como segundo exemplo de arquivos manipulados linha a linha, podemos citar o caso
em que salvamos os dados com formatao por linha. Para exemplificar, vamos
considerar que queremos salvar as informaes da lista de figuras geomtricas que
discutimos na seo 9.3. A lista continha retngulos, tringulos e crculos.
Para salvar essas informaes num arquivo, temos que escolher um formato apropriado,
que nos permita posteriormente recuperar a informao salva. Para exemplificar um
formato vlido, vamos adotar uma formatao por linha: em cada linha salvamos um
caractere que indica o tipo da figura (r, t ou c), seguido dos parmetros que definem a
figura, base e altura para os retngulos e tringulos ou raio para os crculos. Para
r 2.0 1.2
c 5.8
# t 1.23 12
t 4 1.02
c 5.1
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, ...);
Faremos a interpretao do arquivo da seguinte forma: para cada linha lida do arquivo,
tentaremos ler do contedo da linha um caractere (desprezando eventuais caracteres
brancos iniciais) seguido de dois nmeros reais. Se nenhum dado for lido com sucesso,
significa que temos uma linha vazia e devemos desprez-la. Se pelo menos um dado (no
caso, o caractere) for lido com sucesso, podemos interpretar o tipo da figura geomtrica
armazenada na linha, ou detectar a ocorrncia de um comentrio. Se for um retngulo
ou um tringulo, os dois valores reais tambm devero ter sido lidos com sucesso. Se
for um crculo, apenas um valor real dever ter sido lido com sucesso. O fragmento de
cdigo abaixo ilustra essa implementao. Supe-se que fp representa um ponteiro para
um arquivo com formato vlido aberto para leitura, em modo texto.
char c;
float v1, v2;
FILE* fp;
char linha[121];
...
while (fgets(linha,121,fp)) {
int n = sscanf(linha," %c %f %f",&c,&v1,&v2);
if (n>0) {
switch(c) {
case '#':
/* desprezar linha de comentrio */
break;
case 'r':
if (n!=3) {
/* tratar erro de formato do arquivo */
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).
RETANGULO
b h
TRIANGULO
b h
CIRCULO
r
POLIGONO
n
x 1 y1
x 2 y2
x n yn
A funo para ler (recuperar) dados de arquivos binrios anloga, sendo que agora o
contedo do disco copiado para o endereo de memria passado como parmetro. O
prottipo da funo pode ser dado por:
int fread (void* p, int tam, int nelem, FILE* fp);
Para exemplificar a utilizao dessas funes, vamos considerar que uma aplicao tem
um conjunto de pontos armazenados num vetor. O tipo que define o ponto pode ser:
struct ponto {
float x, y, z;
};
typedef struct ponto Ponto;
Uma funo para salvar o contedo de um vetor de pontos pode receber como
parmetros o nome do arquivo, o nmero de pontos no vetor, e o ponteiro para o vetor.
Uma possvel implementao dessa funo ilustrada abaixo:
void salva (char* arquivo, int n, Ponto* vet)
{
FILE* fp = fopen(arquivo,"wb");
if (fp==NULL) {
printf("Erro na abertura do arquivo.\n");
exit(1);
}
fwrite(vet,sizeof(Ponto),n,fp);
fclose(fp);
}
1
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).
Para exemplificar, vamos considerar que os elementos do vetor que queremos ordenar
so valores inteiros. Assim, consideremos a ordenao do seguinte vetor:
25 48 37 12 57 86 33 92
Neste ponto, o segundo maior elemento, 86, j est na sua posio final.
25 12 37 48 33 57 86 92 25x12 troca
12 25 37 48 33 57 86 92 25x37
12 25 37 48 33 57 86 92 37x48
12 25 37 48 33 57 86 92 48x33 troca
12 25 37 33 48 57 86 92 48x57
12 25 37 33 48 57 86 92 final da terceira passada
Uma funo que implementa esse algoritmo apresentada a seguir. A funo recebe
como parmetros o nmero de elementos e o ponteiro do primeiro elemento do vetor
que se deseja ordenar. Vamos considerar o ordenao de um vetor de valores inteiros.
/* Ordenao bolha */
void bolha (int n, int* v)
{
int i,j;
for (i=n-1; i>=1; i--)
for (j=0; j<i; j++)
if (v[j]>v[j+1]) { /* troca */
int temp = v[j];
v[j] = v[j+1];
v[j+1] = temp;
}
}
Uma funo cliente para testar esse algoritmo pode ser dada por:
/* Testa algoritmo de ordenao bolha */
#include <stdio.h>
A varivel troca guarda o valor 0 (falso) quando uma passada do vetor (no for
interno) se faz sem nenhuma troca.
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].
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 (>).
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.
Uma varivel ponteiro para armazenar o endereo dessa funo declarada como:
int(*cmp)(void*,void*);
Agora sim, podemos escrever nossa funo de ordenao genrica, recebendo como
parmetro adicional o ponteiro da funo de comparao:
/* Ordenao bolha (genrica) */
void bolha_gen (int n, void* v, int tam, int(*cmp)(void*,void*))
{
int i, j;
for (i=n-1; i>0; i--) {
int fez_troca = 0;
for (j=0; j<i; j++) {
void* p1 = acessa(v,j,tam);
void* p2 = acessa(v,j+1,tam);
if (cmp(p1,p2))
troca(p1,p2,tam);
fez_troca = 1;
}
if (fez_troca == 0) /* nao houve troca */
return;
}
}
Esse cdigo genrico pode ser usado para ordenar vetores com qualquer informao.
Para exemplificar, vamos us-lo para ordenar um vetor de nmeros reais. Para isso,
temos que escrever o cdigo da funo que faz a comparao, agora especializada para
nmero reais:
Suponha que este elemento, x , deva ocupar a posio i do vetor, de acordo com a
ordenao, ou seja, que essa seja a sua posio definitiva no vetor. Sem ordenar o vetor
completamente, este fato pode ser reconhecido quando todos os elementos v[0],
v[i-1] so menores que x, e todos os elementos v[i+1], , v[n-1] so maiores que
x . Supondo que x j est na sua posio correta, com ndice i , h dois problemas
menores para serem resolvidos: ordenar os (sub-) vetores formados por v[0], v[i-
1] e por v[i+1], , v[n-1]. Esses sub-problemas so resolvidos (recursivamente) de
forma semelhante, cada vez com vetores menores, e o processo continua at que os
vetores que devem ser ordenados tenham zero ou um elementos, caso em que sua
ordenao j est concluda.
A grande vantagem desse algoritmo que ele pode ser muito eficiente. O melhor caso
ocorre quando o elemento piv representa o valor mediano do conjunto dos elementos
do vetor. Se isto acontece, aps o posicionamento do piv em sua posio, restar dois
sub-vetores para serem ordenados, ambos com o nmero de elementos reduzido a
metade, em relao ao vetor original. Pode-se mostrar que, neste melhor caso, o esforo
computacional do algoritmo proporcional a n log(n), e dizemos que o algoritmo
O(n log(n)). Um desempenho muito superior ao O(n2) apresentado pelo algoritmo de
ordenao bolha. Infelizmente, no temos como garantir que o piv seja o mediano. No
pior caso, o piv pode sempre ser, por exemplo, o maior elemento, e recamos no
algoritmo de ordenao bolha. No entanto, mostra-se que o algoritmo quicksort ainda
apresenta, no caso mdio, um desempenho O(n log(n)).
A verso do quicksort que vamos apresentar aqui usa x=v[0] como primeiro
elemento a ser colocado em sua posio correta. O processo compara os elementos
v[1], v[2], at encontrar um elemento v[a]>x. Ento, a partir do final do vetor,
compara os elementos v[n-1] , v[n-2] , at encontrar um elemento v[b]<=x .
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
(0-7)12 25 37 48 57 86 33 92
com 25 em sua posio correta, e dois vetores menores para ordenar. Valores menores
que 25:
(0-0) 12
E valores maiores:
(2-7) 37 48 57 86 33 92
restando os vetores
(2-2) 33
e
(4-7) 57 86 48 92
/* troca piv */
v[0] = v[b];
v[b] = x;
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]).
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>
qsort(v,8,sizeof(float),comp_reais);
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);
}
Busca linear
A forma mais simples de fazermos uma busca num vetor consiste em percorrermos o
vetor, elemento a elemento, verificando se o elemento de interesse igual a um dos
elementos do vetor. Esse algoritmo pode ser implementado conforme ilustrado pelo
cdigo a seguir, considerando-se um vetor de nmeros inteiros. A funo apresentada
tem como valor de retorno o ndice do vetor no qual foi encontrado o elemento; se o
elemento no for encontrado, o valor de retorno 1.
int busca (int n, int* vet, int elem)
{
int i;
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.
Alm do pior caso, devemos analisar o caso mdio, isto , o caso que ocorre na mdia.
J vimos que o algoritmo em questo requer n comparaes quando o elemento no est
presente no vetor. E no caso do elemento estar presente, quantas operaes de
comparao so, em mdia, necessrias? Na mdia, podemos concluir que so
necessrias n/2 comparaes. Em termos de ordem de complexidade, no entanto,
continuamos a ter uma variao linear, isto , O(n), pois dizemos que O(k n), onde k
uma constante, igual a O(n).
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;
Se quisermos que a funo tenha como valor de retorno o ndice do elemento, devemos
acertar o valor retornado pela chamada recursiva na segunda parte do vetor. Uma
implementao com essa modificao apresentada abaixo:
int busca_bin_rec (int n, int* vet, int elem)
{
/* testa condio de contorno: parte com tamanho zero */
if (n <= 0)
return -1;
else {
/* deve buscar o elemento entre os ndices 0 e n-1 */
int meio = (n - 1) / 2;
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.
p = (int*)bsearch(&e,v,8,sizeof(int),comp_int);
if (p == NULL)
printf("Elemento nao encontrado.\n");
else
printf("Elemento encontrado no indice: %d\n", p-v);
return 0;
}
Devemos notar que o ndice do elemento, se encontrado no vetor, pode ser extrado
subtraindo-se o ponteiro do elemento do ponteiro do primeiro elemento (p-v). Essa
aritmtica de ponteiros vlida aqui pois podemos garantir que ambos os ponteiros
armazenam endereos de memria de um mesmo vetor. A diferena entre os ponteiros
representa a distncia em que os elementos esto armazenados na memria.
Vamos agora considerar que queremos efetuar uma busca num vetor de ponteiros para
alunos. A estrutura que representa um aluno pode ser dada por:
struct aluno {
char nome[81];
char mat[8];
char turma;
char email[41];
};
typedef struct aluno Aluno;
Considerando que o vetor est ordenado segundo os nomes dos alunos, podemos buscar
a ocorrncia de um determinado aluno passando para a funo de busca um nome e o
vetor. A funo de comparao ento receber dois ponteiros: um ponteiro para uma
cadeia de caracteres e um ponteiro para um elemento do vetor (no caso ser um ponteiro
para ponteiro de aluno, ou seja, um Aluno**).
/* Funo de comparao: char* e Aluno** */
int comp_alunos (const void* p2, const void* p2)
/* converte ponteiros genricos para ponteiros especficos */
char* s = (char*)p1;
Aluno **pa = (Aluno**)p2;
/* faz a comparao */
return strcmp(s,(*pa)->nome);
}
Conforme observamos, o tipo de informao a ser buscada nem sempre igual ao tipo
do elemento; para dados complexos, em geral no . A informao buscada geralmente
representa um campo da estrutura armazenada no vetor (ou da estrutura apontada por
elementos do vetor).
1 + 2 + 22 + + 2h-1 + 2h = 2h+1-1
Assim, dizemos que uma rvore binria de altura h pode ter no mximo O(2h) ns, ou,
pelo outro lado, que uma rvore binria com n ns pode ter uma altura mnima de
O(log n). Essa relao entre o nmero de ns e a altura mnima da rvore importante
porque se as condies forem favorveis, podemos alcanar qualquer um dos n ns de
uma rvore binria a partir da raiz em, no mximo, O(log n) passos. Se tivssemos os n
ns em uma lista linear, o nmero mximo de passos seria O(n), e, para os valores de n
encontrados na prtica, log n muito menor do que n.
A altura de uma rvore , certamente, uma medida do tempo necessrio para encontrar
um dado n. No entanto, importante observar que para acessarmos qualquer n de
maneira eficiente necessrio termos rvores binrias balanceadas, em que os ns
internos tm todos, ou quase todos, o mximo nmero de filhos, no caso 2. Lembramos
que o nmero mnimo de ns de uma rvore binria de altura h h+1, de forma que a
altura mxima de uma rvore com n ns O(n). Esse caso extremo corresponde
rvore degenerada, em que todos os ns tm apenas 1 filho, com exceo da (nica)
folha.
Uma variao possvel permite que haja repetio de valores na rvore: o valor
associado raiz sempre maior que o valor associado a qualquer n da sae, e sempre
menor ou igual ao valor associado a qualquer n da sad. Nesse caso, como a repetio
de valores permitida, quando a rvore percorrida em ordem simtrica, os valores so
encontrados em ordem no decrescente.
2 8
1 4
3
Figura 16.1: Exemplo de rvore binria de busca.
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;
}
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 6
2 8 2 8
Retira n 4
1 4 1 4
3 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 sub-
rvore 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).
6 4 4
2 8 2 8 2 8
troca 6 com 4
retira n 6
1 4 1 6 1 6
3 3 3
Figura 16.3: Exemplo da operao para retirar o elemento com informao igual a 6.
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 sub-
rvores 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.
Para apresentar a idia das tabelas de disperso, vamos considerar um exemplo onde
desejamos armazenar os dados referentes aos alunos de uma disciplina. Cada aluno
individualmente identificado pelo seu nmero de matrcula. Podemos ento usar o
nmero de matrcula como chave de busca do conjunto de alunos armazenados. Na
PUC-Rio, o nmero de matrcula dos alunos dado por uma seqncia de oito dgitos,
sendo que o ltimo representa um dgito de controle, no sendo portanto parte efetiva
do nmero de matrcula. Por exemplo, na matricula 9711234-4, o ultimo dgito 4, aps
o hfen, representa o dgito de controle. O nmero de matrcula efetivo nesse caso
composto pelos primeiros sete dgitos: 9711234.
Para permitir um acesso a qualquer aluno em ordem constante, podemos usar o nmero
de matrcula do aluno como ndice de um vetor vet. Se isso for possvel, acessamos
os dados do aluno cuja matrcula dado por mat indexando o vetor vet[mat]. Dessa
forma, o acesso ao elemento se d em ordem constante, imediata. O problema que
encontramos que, nesse caso, o preo pago para se ter esse acesso rpido muito
grande.
Vamos considerar que a informao associada a cada aluno seja representada pela
estrutura abaixo:
struct aluno {
int mat;
char nome[81];
char email[41];
char turma;
};
typedef struct aluno Aluno;
Como a matrcula composta por sete dgitos, o nmero inteiro que conceitualmente
representa uma matrcula varia de 0000000 a 9999999. Portanto, precisamos
dimensionar nosso vetor com dez milhes (10.000.000) de elementos. Isso pode ser
feito por:
#define MAX 10000000
Aluno vet[MAX];
Dessa forma, o nome do aluno com matrcula mat acessado simplesmente por:
vet[mat].nome . Temos um acesso rpido, mas pagamos um preo em uso de
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.
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 dimen-
sionarmos 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.
Podemos generalizar essa funo para tabelas de disperso com dimenso N . Basta
avaliar o modulo do nmero de matrcula por 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.
x 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.
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.
h' ( x) = N - 2 - x%( N - 2)
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;
O programa para contar o uso das palavras um programa relativamente simples, que
no precisa ser subdividido em mdulos para ser construdo. Aqui, vamos projetar o
programa identificando as diversas funes necessrias para a construo do programa
como um todo. Cada funo tem sua finalidade especfica e o programa principal (a
funo main) far uso dessas funes.
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.
if (c == EOF)
return 0;
else
s[i++] = c; /* primeira letra j foi capturada */
return 1;
}
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;
}
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.
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;
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;
/* 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
if (argc != 2) {
printf("Arquivo de entrada nao fornecido.\n");
return 0;
}
/* imprime ordenado */
imprime (tab);
return 0;
}
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:
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.
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;
}