Escolar Documentos
Profissional Documentos
Cultura Documentos
ESTRUTURAS
DE DADOS
E
ALGORITMOS
EM
Prefcio
Este texto serve de suporte disciplina de Programao II, cujo objectivo o de fornecer uma
familiarizao com o ambiente de programao fornecido pelo Unix, na sua variante mais popularizada
Linux e o domnio da linguagem C, na sua norma ANSI, para o desenvolvimento de programas de
mdia e elevada complexidade.
Comeamos por apresentar os aspectos essenciais da linguagem C em dois captulos. Depois
introduzirmos as construes mais complexas da linguagem de forma gradual, medida que so
necessrias construo de estruturas de dados mais complexas, bem como para a optimizao e
generalizao de algoritmos. Os aspectos fundamentais apresentados no texto so os seguintes:
x A familiarizao progressiva com a linguagem de programao C e com as suas bibliotecas.
x A apresentao de algoritmos recursivos e sua comparao com os algoritmos iterativos
equivalentes.
x A introduo da metodologia de decomposio modular das solues, ou seja, o paradigma da
programao modular.
x O estudo da organizao da Memria de Acesso Aleatrio (RAM), nas suas implementaes
esttica e semiesttica, e, de um conjunto significativo de algoritmos de pesquisa e de ordenao.
x O estudo da organizao de memrias mais complexas que a Memria de Acesso Aleatrio, como
por exemplo, a Memria Fila de Espera (FIFO), a Memoria Pilha (Stack) e a Memria Associativa
(CAM), nas suas implementaes esttica, semiesttica e dinmica, e, dos algoritmos associados
para pesquisa, introduo e retirada de informao.
Assume-se que os alunos frequentaram a disciplina de Programao I, e portanto, j esto
familiarizados com a metodologia de decomposio hierrquica das solues, estabelecendo
dependncias de informao e no encapsulamento da informao com a criao de novas instrues no
mbito da linguagem Pascal, ou seja, com o paradigma da programao procedimental. Bem como,
com a criao de estruturas de dados estticas com alguma complexidade que modelam correctamente a
resoluo dos problemas. Pelo que, a apresentao da linguagem C feita por comparao com a
linguagem Pascal.
Pretende-se ainda, que os alunos se familiarizem com a terminologia informtica apresentada nos textos
de referncia da rea das Cincias da Computao, pelo que, se tenha optado pela apresentao
sistemtica, em itlico e entre parntesis, dos nomes dos algoritmos e das estruturas de dados em ingls.
Captulo 1
INTRODUO AO C
Sumrio
Este captulo dedicado introduo das primeiras noes sobre a gramtica da linguagem
C. Comeamos por apresentar a estrutura de um programa e os seus elementos bsicos.
Explicamos os tipos de dados bsicos existentes, a definio de constantes e de variveis.
Apresentamos os vrios tipos de expresses e operadores existentes e a instruo de
atribuio, que a instruo bsica de uma linguagem imperativa. Apresentamos de seguida
as estruturas de controlo, que permitem alterar o fluxo da sequncia das instrues.
Apresentamos ainda as instrues de leitura de dados do teclado scanf e de escrita de
dados no monitor printf. Finalmente, apresentamos as bibliotecas que contm as funes
mais usuais e que estendem a operacionalidade da linguagem.
1.1 Introduo
Em 1972, Dennis M. Ritchie desenvolveu a linguagem C, nos Laboratrios Bell da
companhia AT & T, que a principal empresa de telecomunicaes dos Estados Unidos da
Amrica, como uma linguagem de programao concebida para a escrita de sistemas
operativos, aquilo que se designa por Programao de Sistemas. Como a linguagem C era
to flexvel e permitia que os compiladores produzissem cdigo em linguagem mquina
muito eficiente, em 1973, Dennis M. Ritchie e Ken Thompson reescreveram quase
totalmente o sistema operativo Unix em C. Devido a esta ligao ntima, medida que o
Unix se tornou popular no meio acadmico, tambm a linguagem C se tornou a linguagem
preferida para o desenvolvimento de aplicaes cientficas. Pelo que, apesar de ter sido
concebida para a escrita de sistemas operativos, a linguagem C hoje encarada como uma
linguagem de uso geral.
A principal caracterstica da linguagem C que combina as vantagens de uma linguagem de
alto nvel descendente do AlGOL 68, com a eficincia da linguagem assembly, uma vez que
permite a execuo de operaes aritmticas sobre ponteiros e operaes sobre palavras
binrias. A linguagem C tambm tem uma sintaxe muito compacta e permite que
operadores de tipos diferentes possam ser combinados livremente.
Esta liberdade e poder da linguagem C, permite aos programadores experientes escreverem
cdigo compacto e eficiente que dificilmente poderiam ser escritos noutras linguagens de
programao. Mas, como fracamente estruturada em termos semnticos, tambm permite
que construes sem sentido aparente, escritas por programadores inexperientes, sejam
aceites pelo compilador como vlidas. O facto da linguagem C ser muito poderosa, exige
portanto, do programador muita disciplina e rigor na utilizao das construes da
linguagem, para que o cdigo escrito seja legvel e facilmente altervel.
Apesar da linguagem C ter sido desenvolvida no princpio da dcada de 1970, a norma
ANSI (American National Standards Institute) foi apenas aprovada em 1989 (norma
ISO/IEC 9899-1990).
CAPTULO 1 : INTRODUO AO C
A incluso de ficheiros de interface num dado ficheiro fonte feita usando a directiva do
pr-processador #include numa das suas duas variantes:
x No caso do ficheiro de interface pertencer linguagem C, ento ele est armazenado no
directrio por defeito e usa-se a directiva #include <nome do ficheiro de interface>.
x No caso do ficheiro de interface ter sido criado pelo utilizador e no estar armazenado
no directrio por defeito, usa-se a directiva #include "nome do ficheiro de interface".
No mnimo, todos os ficheiros que contenham cdigo que faa acesso aos dispositivos
convencionais de entrada e de sada tm que incluir o ficheiro de interface stdio.h, que
descreve as funes e que contm as definies associadas com o acesso aos dispositivos
de entrada e de sada e aos ficheiros. Normalmente, o dispositivo de entrada o teclado e o
dispositivo de sada o monitor. Portanto, qualquer programa interactivo tem pelo menos
a aluso a este ficheiro de interface, tal como se apresenta na Figura 1.2.
A seguir s definies de objectos externos segue-se a aluso s funes locais que vo ser
usadas na funo main, bem como a definio de estruturas de dados e constantes locais.
Locais para a aplicao, mas que para o ficheiro, se comportam como definies globais.
Repare que a estruturao do programa, muito diferente do Pascal, sendo que as funes
so primeiramente aludidas ou referidas, para se tornarem visveis em todo o ficheiro e s
depois da funo main que so definidas. Neste exemplo, define-se apenas, atravs da
directiva #define, um identificador constante MIL_QUI, que representa o factor de
converso de milhas para quilmetros. Ele visvel para todo o cdigo do ficheiro, ou seja,
uma constante global. A funo main implementada com instrues simples, e com
recurso apenas s funes de entrada e de sada de dados da biblioteca stdio.
/* Programa de converso de distncias de milhas para quilmetros */
/* Instrues para o pr-processador */
#include <stdio.h>
#define
MIL_QUI
1.609
/* factor de converso */
/* Converso da distncia */
Vamos agora analisar com detalhe na Figura 1.3 a definio da funo main, que tal como
qualquer outra funo na linguagem C, supe a especificao do seu cabealho e do seu
corpo. No cabealho, indica-se o tipo do valor devolvido, que como j foi referido
anteriormente sempre do tipo inteiro, o nome, e entre parnteses curvos, a lista de
CAPTULO 1 : INTRODUO AO C
cabealho
corpo
CAPTULO 1 : INTRODUO AO C
A Figura 1.6 apresenta os tipos de dados simples existentes na linguagem C. Estes tipos de
dados tambm se designam por escalares, uma vez que, todos os seus valores esto
distribudos ao longo de uma escala linear. Dentro dos tipos de dados simples, temos o tipo
ponteiro (pointer), o tipo enumerado (enum) e os tipos aritmticos, que se dividem em
tipos inteiros e tipos reais. Os tipos aritmticos e o tipo enumerado designam-se por tipos
bsicos. Os tipos aritmticos inteiros podem armazenar valores negativos e positivos, que
o estado por defeito ou usando o qualificativo signed, ou em alternativa, podem
armazenar apenas valores positivos, usando para o efeito o qualificativo unsigned que lhe
duplica a gama dinmica positiva. O tipo aritmtico int pode ainda ser qualificado como
short, reduzindo-lhe a capacidade de armazenamento. O qualificativo long pode ser usado
para aumentar a capacidade de armazenamento do tipo inteiro int e do tipo real double.
Tipos de Dados Simples
Qualificativos
Aritmticos
Ponteiro
short
pointer
long
Tipos Inteiros
Tipos Reais
signed
int
char
float
double
Tipos Bsicos
Enumerado
unsigned
enum
O tamanho dos tipos inteiros int e long e a sua gama dinmica dependem do compilador e
do hardware utilizados. Esta informao indicada no ficheiro limits.h localizado no
directrio include do ambiente de desenvolvimento. No caso do computador utilizado nesta
disciplina, cujo processador de 32 bits, os tipos int e unsigned int so representados em
4 bytes, pelo que, permitem armazenar respectivamente valores entre -2147483648 e
2147483647 e valores entre 0 e 4294967295. Os tipos long e unsigned long so tambm
representados em 4 bytes.
char NUM, CAR; /* declarao das variveis NUM e CAR de tipo char */
...
NUM = 3;
/* armazena na posio de memria NUM o valor 3 */
CAR = '3';
/* armazena na posio de memria CAR o valor 51 */
CAPTULO 1 : INTRODUO AO C
identificador de constante
expresso
identificador de constante
( expresso )
Um dos erros mais frequentemente cometido, por programadores que se esto a iniciar na
utilizao da linguagem C, quando utilizam esta directiva na definio de um identificador
constante a sua terminao com o ;. Nesse caso o ; torna-se parte da substituio
podendo gerar situaes de erro.
As constantes numricas inteiras podem ser representadas no sistema decimal, no sistema
octal, em que a constante precedida pelo dgito 0 ou no sistema hexadecimal, em que a
constante precedida pelo dgito 0 e pelo carcter x. Quando as constantes esto
representadas nos sistemas octal ou hexadecimal, o sinal normalmente expresso de uma
maneira implcita, usando a representao em complemento verdadeiro. A Figura 1.8
apresenta alguns exemplos, considerando uma representao do tipo int em 32 bits.
Sistema decimal
54
-135
em complemento verdadeiro
0
Sistema octal
066
-0207
037777777571
00
Sistema hexadecimal
0x36
-0x87
0xFFFFFF79
0x0
O compilador atribui por defeito o tipo int a uma constante numrica inteira. Quando este
tipo no tem capacidade de armazenamento suficiente, os tipos seguintes so
sucessivamente atribudos. No caso de uma constante decimal, usa-se o tipo long, ou se
ainda for insuficiente o tipo unsigned long. No caso de uma constante octal ou
hexadecimal, usa-se pela seguinte ordem o tipo unsigned int, ou o tipo long, ou o tipo
unsigned long.
10
A atribuio do tipo pode ser forada pelos sufixos U para unsigned e L para long. Uma
constante seguida do sufixo U do tipo unsigned int ou do tipo unsigned long. Uma
constante seguida do sufixo L do tipo long ou do tipo unsigned long. Uma constante
seguida do sufixo UL do tipo unsigned long.
As constantes numricas reais so sempre expressas no sistema decimal, usando quer a
representao em parte inteira e parte fraccionria, quer a chamada notao cientfica. O
compilador atribui por defeito o tipo double a uma constante numrica real. A atribuio
do tipo pode ser forada pelos sufixos F e L. Uma constante seguida do sufixo F do tipo
float. Uma constante seguida do sufixo L do tipo long double. So exemplos de
constantes reais os valores 0.0148, 1.48e-2 e 0.0.
As constantes de tipo carcter podem ser expressas indiferentemente pelo respectivo
smbolo grfico, colocado entre aspas simples, ou atravs do valor do seu cdigo de
representao nos sistemas octal e hexadecimal, precedidas do carcter '\', tal como se
mostra na Figura 1.9.
Sistema decimal
'B'
Sistema octal
'\102'
Sistema hexadecimal
'\x42'
Para alguns caracteres de controlo, pode ainda ser usada uma representao alternativa que
consiste numa letra do alfabeto minsculo precedida do carcter '\'. Por exemplo o carcter
de fim de linha o '\n', o carcter de backspace o '\b', o carcter de tabulao o '\t', o
carcter aspas duplas o '\' e o carcter ponto de interrogao o '\?'.
As constantes de tipo cadeia de caracteres so expressas como uma sequncia de caracteres,
representados por qualquer dos mtodos anteriores, colocados entre aspas duplas. Por
exemplo Ola malta!\n.
Uma varivel um objecto, cujo valor se altera em princpio durante a execuo do
programa, excepo eventualmente feita s variveis de entrada, cujo valor depois de lido
do teclado , em princpio, mantido inalterado at ao fim da execuo do programa.
Todas as variveis usadas num programa tm que ser previamente definidas ou declaradas.
O objectivo da declarao simultaneamente a reserva de espao em memria para o
armazenamento dos valores que as variveis vo sucessivamente tomar, e a associao de
cada identificador com a rea de memria correspondente.
A Figura 1.10 apresenta a definio formal da declarao de variveis na linguagem C. Para
declarar variveis comea-se por identificar o tipo de dados seguido da varivel, ou lista de
variveis que se pretendem declarar desse tipo, separadas por vrgulas, terminando a
declarao com o separador ;. conveniente agrupar a declarao de variveis do mesmo
tipo na mesma linha para aumentar a legibilidade do programa.
A reserva de espao em memria no pressupe, em princpio, a atribuio de um valor
inicial varivel. Em consequncia, nada deve ser presumido sobre o seu valor antes que
uma primeira atribuio tenha sido efectivamente realizada. No entanto, a linguagem C
permite combinar a definio de uma varivel com a atribuio de um valor inicial, usando
para o efeito o operador de atribuio seguido da expresso de inicializao.
11
CAPTULO 1 : INTRODUO AO C
A
char A, B = 'G';
unsigned int C = 1324;
double D = -2.5, E;
rea no reservada
'G'
1324
4 bytes
-2.5
8 bytes
4 bytes
rea reservada
mas no inicializada
rea reservada
e inicializada
8 bytes
12
1.6 Sequenciao
Tal como foi referido anteriormente, o corpo de uma funo delimitado pelos
separadores { e }, correspondentes, respectivamente, aos separadores begin e end do
Pascal, e contm a declarao das variveis locais, a aluso a funes usadas na sequncia
de instrues e a sequncia de instrues propriamente dita. A Figura 1.12 apresenta a
definio formal de uma sequncia de instrues. Ao contrrio do que se passa em Pascal,
na linguagem C cada instruo simples obrigatoriamente terminada com o separador ;, a
menos que o ltimo smbolo da instruo seja o separador }.
sequncia de instrues simples ::= instruo simples |
sequncia de instrues simples instruo simples
instruo simples ::= instruo de atribuio |
instruo decisria |
instruo repetitiva |
instruo de entrada-sada |
invocao de uma funo
Figura 1.12 - Definio formal da sequncia de instrues.
Em Pascal, a invocao de uma funo, sendo uma expresso, no pode ser considerada
uma instruo. Na linguagem C, contudo, o conceito de procedimento no tem uma
existncia separada. Define-se como sendo uma funo de um tipo especial, o tipo void.
Pelo que, a invocao de um procedimento , por isso, em tudo semelhante invocao de
uma funo de qualquer outro tipo, quando o valor devolvido no tido em considerao.
Logo, nestas circunstncias, na linguagem C, a pura e simples invocao de uma funo de
qualquer tipo considerada uma instruo.
A sequenciao de instrues a forma mais simples de controlo de fluxo num programa,
em que as instrues so executadas pela ordem em que aparecem no programa. Dentro
das instrues simples, apenas as instrues de atribuio, de entrada-sada e de invocao
de uma funo, so verdadeiramente instrues de sequenciao, j que, as instrues
decisrias e repetitivas permitem alterar a ordem do fluxo do programa.
Tal como no Pascal, na linguagem C tambm existe o conceito de instruo composta, cuja
definio formal se apresenta na Figura 1.13, e que composta por uma sequncia de
instrues simples encapsuladas entre os separadores { e }. Uma instruo composta um
bloco de instrues simples que se comporta como uma instruo nica e usada em
instrues decisrias e repetitivas.
instruo composta ::= { sequncia de instrues simples }
Figura 1.13 - Definio formal da instruo composta.
1.6.1 Expresses
A Figura 1.14 apresenta a definio formal de uma expresso. Uma expresso uma
frmula que produz um valor. Pode assumir as seguintes formas: ser uma constante; ser
uma varivel; ser o resultado da invocao de uma funo; ser uma expresso composta
por operandos e operadores, sendo que existem operadores unrios e binrios; e ser uma
expresso entre parnteses curvos.
13
CAPTULO 1 : INTRODUO AO C
long double o double o float o unsigned long o long o unsigned int o int
Alm das converses automticas que foram referidas, a converso de uma expresso num
tipo especfico qualquer pode ser sempre forada atravs do operador cast.
( qualquer tipo de dados escalar vlido em C ) ( expresso numrica )
14
DIVISAO = (double) A / B;
/* DIVISAO = 2.5 */
Vamos agora apresentar alguns exemplos representativos destes problemas. A Figura 1.17
apresenta um exemplo da situao em que se atribui um valor negativo de uma varivel int
a uma varivel unsigned int. O valor armazenado na memria em binrio vai ser
interpretado como sendo positivo, pelo que, h uma mudana na interpretao do valor.
int A = -1024;
...
B = A;
unsigned int B;
A Figura 1.18 apresenta um exemplo da situao em que se atribui uma varivel unsigned
int a uma varivel unsigned char, que tem uma menor capacidade de armazenamento. O
valor que vai ser armazenado em B constitudo pelos ltimos 8 bits de A, ou seja, o resto
da diviso de A por 256, que o mximo valor que se pode armazenar num byte.
15
CAPTULO 1 : INTRODUO AO C
unsigned char B;
A Figura 1.19 apresenta um exemplo da situao em que se atribui uma varivel int a uma
varivel char, que tem uma menor capacidade de armazenamento. Como o valor de A
excede a capacidade de armazenamento de B, ento temos uma situao de overflow. Esta
situao facilmente detectada, uma vez que, o valor de chegada negativo, quando o
valor de partida era positivo.
int A = 1152;
...
B = A;
char B;
No clculo de uma expresso complexa, uma vez fixado o agrupamento, segundo as regras
da prioridade e da associatividade dos operadores envolvidos, modificadas ou no pela
introduo de parnteses curvos, a ordem pela qual o compilador calcula as diferentes
subexpresses em larga medida arbitrria. O compilador pode mesmo reorganizar a
expresso, se isso no afectar o resultado final. Em geral, esta questo no acarreta
consequncias graves.
Contudo, sempre que o clculo de uma expresso envolva operadores com efeitos
colaterais, ou seja, uma expresso em que o valor de uma ou mais variveis afectado pelo
processo de clculo, normalmente devido utilizao dos operadores unrios incremento e
decremento, o cdigo resultante pode deixar de ser portvel. Assim, de extrema
importncia organizar cuidadosamente a formao das expresses para se evitar que tais
situaes ocorram.
Quando no clculo de uma expresso existe o risco de ocorrncia de overflow em resultado
de uma possvel transformao da expresso numa equivalente, tal como se mostra na
Figura 1.20, isso deve ser impedido por decomposio do clculo da expresso em duas ou
mais expresses parcelares, seno o cdigo resultante pode deixar de ser portvel.
int X, K1 = 1024, K2 = 4096, K3 = 4094;
...
X = K1 * K1 * (K2 K3);
/* se o compilador transformar a expresso na expresso */
/* aparentemente equivalente X = K1 * K1 * K2 K1 * K1 * K3 */
/* vai ocorrer overflow para uma representao int em 32 bits */
Figura 1.20 - Exemplo de uma expresso onde existe o risco de ocorrncia de overflow.
16
1.6.2 Operadores
A Figura 1.21 apresenta os operadores aritmticos disponveis na linguagem C.
Operadores Unrios
Operador
Simtrico
Incremento de 1
Smbolo
+
++
Decremento de 1
--
Sintaxe
+x
-x
++x
x++
--x
x--
Observaes
Operadores Binrios
Operador
Smbolo
Sintaxe
Observaes
Adio
+
x + y
Subtraco
x - y
Multiplicao
*
x * y
Diviso
/
x / y
y 0
Resto da diviso inteira
%
x % y
y 0
e x e y tm de ser expresses inteiras
Y = 5 * ++X;
/* aps o clculo da expresso X = 3 e Y = 15 */
17
CAPTULO 1 : INTRODUO AO C
Figura 1.23 - Exemplo de uma expresso com utilizao incorrecta do operador ps-incremento.
(truncatura)
(aproximao ao maior inteiro menor ou igual)
Operador
Igual
Diferente
Maior
Menor
Maior ou igual
Menor ou igual
Smbolo
==
!=
>
<
>=
<=
18
Sintaxe
x == y
x != y
x > y
x < y
x >= y
x <= y
Smbolo
!
Sintaxe
!x
Operadores Binrios
Operador
Conjuno (and)
Disjuno inclusiva (or)
Smbolo
&&
||
Sintaxe
x && y
x || y
A Figura 1.26 apresenta os operadores para manipulao de bits que se aplicam apenas a
expresses numricas inteiras.
Operador Unrio
Operador
Complemento (not)
Smbolo
~
Sintaxe
~x
Operadores Binrios
Operador
Conjuno (and)
Disjuno inclusiva (or)
Disjuno exclusiva (xor)
Deslocamento direita
Deslocamento esquerda
Smbolo
&
|
^
>>
<<
Sintaxe
x & y
x | y
x ^ y
x >> y
x << y
19
CAPTULO 1 : INTRODUO AO C
Os operadores lgicos actuam isoladamente sobre cada um dos bits dos operandos. Para os
operadores lgicos binrios, as operaes so efectuadas em paralelo sobre os bits
localizados em posies correspondentes de cada um dos operandos.
O resultado da operao uma quantidade inteira do tipo do operando com maior
capacidade de armazenamento, no caso dos operadores lgicos binrios, ou do tipo do
operando x, no caso do operador complemento booleano ou dos operadores de
deslocamento. Para os operadores de deslocamento, o operando y representa o nmero de
posies a deslocar no sentido pretendido. O resultado da operao no est definido,
quando y maior ou igual do que o comprimento em bits do operando x, ou negativo. A
norma ANSI impe a realizao de um deslocamento lgico, quando x for de um tipo
qualquer unsigned. Contudo, nada garantido quando x for de um tipo signed, embora
normalmente o deslocamento seja ento aritmtico. Assim, para se obter uma portabilidade
completa, deve fazer-se sempre um cast para tipos unsigned.
(unsigned int ou unsigned long) x >> y
(unsigned int ou unsigned long) x << y
A Figura 1.27 apresenta a tabela de associatividade e de precedncia, por ordem
decrescente, entre os operadores das expresses numricas.
Operadores na classe
Associatividade
Precedncia
operadores unrios
operador cast
operador sizeof
+
++ ! ~
direita
direita
direita
direita
o
o
o
o
esquerda
esquerda
esquerda
esquerda
maior
operadores binrios
* / %
+
>> <<
>= <= >
=! ==
&
^
|
&&
||
<
esquerda
esquerda
esquerda
esquerda
esquerda
esquerda
esquerda
esquerda
esquerda
esquerda
o
o
o
o
o
o
o
o
o
o
direita
direita
direita
direita
direita
direita
direita
direita
direita
direita
menor
20
X, Y, Z;
X + 2 * Z;
X + 1;
pow (Y, 2);
/* equivalente a Y = Y + 5 */
/* equivalente a Z = Z * (5 + X) */
21
CAPTULO 1 : INTRODUO AO C
A importncia desta notao tem a ver de novo com a compactao resultante e com a
possibilidade fornecida ao compilador de proceder mais facilmente a uma optimizao do
cdigo gerado. A Figura 1.31 apresenta dois exemplos.
int X, Y;
...
Y++;
--X;
/* equivalente a Y = Y + 1 */
/* equivalente a X = X - 1 */
else ABSX = X; */
int X, Y, Z;
...
X = Y = Z;
X += Y -= Z;
22
/* equivalente a X = (Y = Z); */
/* equivalente a X = (X + (Y = (Y-Z)); */
Associatividade
direita o esquerda
Precedncia
maior
direita o esquerda
menor
Uma questo muito importante para editar programas legveis o alinhamento das
instrues. A Figura 1.36 apresenta como se deve alinhar a instruo if. No caso da
variante mais simples e se existir apenas uma instruo simples curta, ento a instruo if
pode ser toda escrita na mesma linha, mas se a instruo simples for longa deve ser escrita
na linha seguinte mais alinhada para a direita. Caso a instruo seja composta, ento os
separadores { e } devem ser alinhados com o if. No caso da variante completa, devemos
alinhar o separador else com o if.
23
CAPTULO 1 : INTRODUO AO C
if ( expresso )
{
instruo simples;
...
instruo simples;
}
if ( expresso )
{
instruo simples;
...
instruo simples;
}
else
{
instruo simples;
...
instruo simples;
}
Vamos agora considerar a situao apresentada na Figura 1.38, em que queremos ter o
separador else para o primeiro if, mas em que o segundo if no o tem. Neste tipo de
situao, que se designa por else desligado (dangling else), o separador else vai ser atribudo
pelo compilador ao segundo if, independentemente de ter sido alinhado com o primeiro if.
if (V1 > 0)
if (V2 > 0)
else V1++;
V2++;
/* situao do else desligado */
Para resolver este problema existem as duas solues apresentadas na Figura 1.39. A
primeira, consiste em usar uma instruo composta a abraar o segundo if, de maneira a
informar o compilador onde acaba o segundo if. A segunda, consiste em usar um
separador else com uma instruo nula, para emparelhar com o segundo if, e assim forar
o emparelhamento do segundo separador else com o primeiro if. A instruo nula o ;.
if
{
24
(V1 > 0)
if (V1 > 0)
if (V2 > 0) V2++;
else ;
else V1++;
B = 2; */
Figura 1.40 - Erro devido troca do operador identidade pelo operador de atribuio.
25
CAPTULO 1 : INTRODUO AO C
simples;
simples;
simples;
simples;
simples;
simples;
switch (CAR)
/* na linguagem C */
{
case 'a' :
case 'e' :
case 'o' : printf ("Vogais speras\n");
break;
case 'i' :
case 'u' : printf ("Vogais doces\n");
break;
default : printf ("Outros smbolos grficos\n");
}
26
while ( expresso )
{
instruo simples;
...
instruo simples;
}
27
CAPTULO 1 : INTRODUO AO C
A Figura 1.46 apresenta como se deve alinhar a instruo do while. As palavras reservadas
do e while devem ser alinhadas e a condio booleana de terminao deve ser escrita
frente do terminador while. As instrues que constituem o corpo do ciclo repetitivo so
escritas uma por linha e todas alinhadas mais direita de maneira a que seja legvel onde
comea e acaba o ciclo repetitivo.
do
{
instruo simples;
...
instruo simples;
} while ( expresso );
A Figura 1.47 faz a comparao dos ciclos repetitivos do while e while, para calcular a
mdia de um nmero indeterminado de nmeros lidos do teclado, sendo que, a leitura
termina quando lido o valor zero. No caso do ciclo repetitivo do while, as instrues
constituintes do corpo do ciclo repetitivo, contagem do nmero til de nmeros lidos e sua
soma, necessitam de ser protegidas quando lido o valor de terminao, para no provocar
um clculo errneo.
SOMA = 0.0;
/* clculo da mdia com o ciclo do while */
do
{
printf ("Introduza um numero? ");
scanf ("%lf", &NUMERO);
if (NUMERO != 0.0)
{
SOMA += NUMERO;
N++;
}
} while (NUMERO != 0.0);
SOMA = 0.0;
/* clculo da mdia com o ciclo while */
printf ("Introduza um numero? ");
scanf ("%lf", &NUMERO);
while (NUMERO != 0.0)
{
SOMA += NUMERO;
N++;
printf ("Introduza um numero? ");
scanf ("%lf", &NUMERO);
}
Figura 1.47 - Exemplo comparativo da utilizao dos ciclos repetitivos do while e while.
28
A expresso deve ser de um tipo escalar bsico. A expresso falsa se for igual a zero e
verdadeira se assumir qualquer outro valor. Nestas condies, o compilador aceita qualquer
expresso numrica como expresso decisria vlida. Porm, por questes de clareza, isto
deve ser evitado. de bom estilo que a expresso decisria represente sempre uma
expresso booleana. Finalmente, a parte de actualizao executada no fim de cada
iterao. Em geral, a sua funo actualizar os valores de uma ou mais variveis usadas no
ciclo repetitivo, entre as quais, sempre a varivel contadora. Normalmente, as expresses de
actualizao das variveis recorrem aos operadores unrios incremento e decremento ou ao
operador de atribuio precedido por um qualquer operador binrio.
instruo for ::= for ( inicializao ; terminao ; actualizao )
instruo simples ou composta
inicializao ::= identificador de varivel = expresso |
inicializao , identificador de varivel = expresso
terminao ::= expresso
actualizao ::= identificador de varivel operador binrio= expresso |
operador unrio inc_dec identificador de varivel |
identificador de varivel operador unrio inc_dec |
actualizao , identificador de varivel operador binrio= expresso |
actualizao , operador unrio inc_dec identificador de varivel |
actualizao , identificador de varivel operador unrio inc_dec
operador unrio inc_dec ::= operador unrio ++ | operador unrio
Figura 1.48 - Definio formal da instruo for.
A instruo for deve ser alinhada da mesma maneira que a instruo while. A Figura 1.49
faz a comparao da instruo for do Pascal e do C usando como exemplo, o clculo das
primeiras N potncias de 2.
POT := 1;
/* na linguagem Pascal */
for I := 1 to N do
begin
writeln ('Potencia de 2 = ', POT:10);
POT := POT * 2
end;
Figura 1.49 - Exemplo do clculo das potncias de 2 com um ciclo repetitivo for.
Qualquer uma das trs partes componentes da instruo for, a inicializao, a terminao
ou a actualizao pode ser omitida. Situaes especiais correspondem aos seguintes casos:
x Quando todos os elementos so omitidos, ou seja, for ( ; ; ), temos um ciclo repetitivo
infinito.
x Quando a inicializao e a actualizao so omitidas, ou seja, for ( ; terminao ; ),
temos um ciclo repetitivo funcionalmente equivalente ao ciclo repetitivo while.
Ao contrrio do que se passa em Pascal, a varivel contadora tem um valor bem definido
aps o esgotamento do ciclo repetitivo, que ser aquele que conduziu a um valor falso da
expresso de terminao.
29
CAPTULO 1 : INTRODUO AO C
Devido sua grande versatilidade, a instruo for deve ser utilizada com extremo cuidado.
de bom estilo us-la apenas como uma generalizao da instruo for de Pascal. O que
significa, nomeadamente, que o valor da varivel contadora nunca deve ser modificado
dentro do ciclo repetitivo.
Um dos erros mais frequentemente cometido por programadores que se esto a iniciar na
utilizao da linguagem C, consiste em esquecer que o ciclo repetitivo for actua enquanto a
expresso de terminao for verdadeira. Se no exemplo anterior fosse utilizada a condio
I == N, o ciclo repetitivo no seria executado uma nica vez, a no ser que N fosse 1,
quando se pretende que o ciclo repetitivo seja executado N vezes.
Os ciclos repetitivos podem ser encadeados, tal como a instruo condicional binria,
dando origem a estruturas repetitivas em que, uma instruo do corpo do ciclo repetitivo
ela mesmo um ciclo repetitivo. A este tipo de construo d-se o nome de ciclos
imbricados (nested loops).
30
N := 0;
/* na linguagem Pascal */
SOMA := 0.0;
readln (NUMERO);
while (N < 10) and (NUMERO >= 0.0) do
begin
N := N + 1;
SOMA := SOMA + NUMERO;
readln (NUMERO)
end;
N = 0;
/* na linguagem C */
SOMA = 0.0;
scanf ("%lf", &NUMERO);
while (N < 10)
{
if (NUMERO < 0.0) break;
N++;
SOMA += NUMERO;
scanf ("%lf", &NUMERO);
}
31
CAPTULO 1 : INTRODUO AO C
for ( ; ; )
/* com o ciclo for */
instruo simples ou composta
32
Por outro lado, o fluxo de texto de sada implementado numa regio da memria
principal, directamente acessvel pelo sistema operativo, que se designa por
armazenamento tampo de sada (output buffer). Aps a escrita de uma sequncia de
caracteres pela instruo de sada, o sistema operativo alertado para esse facto e,
eventualmente, vai transferir essa sequncia, carcter a carcter, para um registo do
controlador do monitor. Em resultado disso, a mensagem associada reproduzida no
monitor a partir da posio actual do cursor e da esquerda para a direita. Alm de
caracteres com representao grfica, as sequncias escritas podem conter caracteres de
controlo diversos que possibilitam, entre outras aces mais ou menos especficas, o
deslocamento do cursor para uma outra posio do monitor.
Na linguagem C, as instrues de entrada e de sada so funes de tipo int que pertencem
biblioteca de execuo ANSI e cuja descrio est contida no ficheiro de interface stdio.h.
33
CAPTULO 1 : INTRODUO AO C
34
A Figura 1.56 apresenta um excerto de cdigo em que se utiliza o scanf para ler uma
varivel de tipo carcter, mas, de forma equivalente ao readln do Pascal.
#include <stdio.h>
int main (void)
{
char CAR,
/* escolha da opo do menu
CARX;
/* varivel auxiliar para teste de fim de linha
...
do
{
printf ("1 - Opo_1\n");
printf ("2 - Opo_2\n");
printf ("3 - Opo_3\n");
printf ("Qual a sua escolha? ");
scanf ("%c", &CAR);
/* ler o carcter que representa a opo
if (CAR != '\n')
do
/* descartar todos os eventuais restantes caracteres
{
/* da linha, incluindo o carcter fim de linha
scanf ("%c", &CARX);
} while (CARX != '\n');
} while ((CAR < '1') || (CAR > '3'));
...
}
*/
*/
*/
*/
*/
35
CAPTULO 1 : INTRODUO AO C
Depois da leitura da varivel de tipo carcter, caso tenha sido lido um carcter diferente de
fim de linha ento vo ser lidos todos os caracteres que eventualmente existam no
armazenamento tampo de entrada at leitura do carcter fim de linha inclusive, usando
para o efeito uma varivel auxiliar de tipo carcter.
Figura 1.57 - Exemplo da leitura de uma cadeia de caracteres com o especificador de converso %s.
36
char FRASE[80];
...
scanf ("%79[^\n]", FRASE);
/* ou em alternativa scanf ("%79[^\n]", &FRASE[0]); */
Um dos erros mais frequentes dos programadores que se esto a iniciar na utilizao da
linguagem C, consiste em usar simultaneamente os dois especificadores de converso de
cadeia de caracteres, inventando o especificador de converso %s[ ]. Estes dois
especificadores de converso so alternativos, e, portanto, no podem ser combinados.
A Figura 1.59 apresenta um excerto de cdigo em que se utiliza o scanf para ler uma
varivel de tipo cadeia de caracteres, mas, de forma equivalente ao readln do Pascal. A
funo invocada para ler uma sequncia de caracteres quaisquer, constituda no mximo
por 79 caracteres. Depois descartam-se todos os caracteres que eventualmente existam no
armazenamento tampo de entrada at leitura do carcter fim de linha. De seguida l-se e
descarta-se o carcter fim de linha. Estas duas aces no podem ser combinadas numa s,
porque caso no existam caracteres extras, ento a instruo de leitura terminaria
abruptamente sem efectuar a leitura do carcter fim de linha, que ficaria no armazenamento
tampo de entrada disponvel para posteriores invocaes da funo. Caso no tenha sido
lido qualquer carcter para a varivel FRASE, o que pode ser indagado aferindo o resultado
devolvido pelo scanf e armazenado na varivel T, que zero nesse caso, ento constri-se
37
CAPTULO 1 : INTRODUO AO C
<stdio.h>
*/
*/
*/
*/
*/
*/
Figura 1.59 - Leitura de uma cadeia de caracteres equivalente instruo readln do Pascal.
(tipo
(tipo
(tipo
(tipo
signed)
unsigned)
unsigned)
unsigned)
Sequncia admissvel
tipos inteiros
[+/-]#...#
[+/-]0x#...#
[+/-]0#...#
[+/-]#...#
[+/-]#...#
[+/-]0x#...#
[+/-]0#...#
Observao
#
#
#
#
#
#
#
algarismo
algarismo
algarismo
algarismo
algarismo
algarismo
algarismo
decimal
hexadecimal
octal
decimal
decimal
hexadecimal
octal
tipos reais
f,e,g
[+/-]#...#
# - algarismo decimal
[+/-][#...#][.]#...#
[+/-][#...#][.]#...#[e/E[+/-]#...#]
38
Por causa do literal '\n' no fim da cadeia de caracteres de definio, a primeira invocao da
funo scanf s termina quando se tecla o carcter 3, pelo que, a mensagem da segunda
invocao da funo printf s escrita no monitor depois de introduzida a segunda linha
de dados, quando deveria aparecer antes.
A Figura 1.63 apresenta um excerto de cdigo em que se utiliza o scanf para ler uma
varivel de tipo numrica, mas, de forma equivalente ao readln do Pascal. A funo
invocada para ler uma varivel numrica, que neste caso inteira de tipo int. Depois
descartam-se todos os caracteres que eventualmente existam no armazenamento tampo de
entrada at leitura do carcter fim de linha. De seguida l-se e descarta-se o carcter fim de
linha. Estas duas aces no podem ser combinadas numa s, porque caso no existam
caracteres extras, ento a instruo de leitura terminaria abruptamente sem efectuar a leitura
do carcter fim de linha, que ficaria no armazenamento tampo de entrada disponvel para
posteriores invocaes da funo. Caso no tenha sido lido qualquer valor numrico para a
varivel VAL, o que pode ser indagado aferindo o resultado devolvido pelo scanf e
armazenado na varivel T, que zero nesse caso, ento repete-se o processo at que seja
efectivamente lido um valor numrico.
39
CAPTULO 1 : INTRODUO AO C
#include
<stdio.h>
*/
*/
*/
*/
*/
*/
Figura 1.63 - Leitura de uma varivel de tipo numrica equivalente instruo readln do Pascal.
A Figura 1.64 apresenta o cdigo necessrio para ler uma informao horria vlida, de tipo
HH:MM:SS. O processo de leitura tem que ler trs quantidades numricas positivas da o
tipo de dados utilizado ser unsigned. Como os valores no excedem 23 para as horas e 60
para os minutos e segundos, ento podem ser de tipo short. A leitura repetida enquanto
houver um erro de formato e enquanto houver valores fora dos limites.
#include
<stdio.h>
*/
*/
*/
*/
*/
*/
*/
int NUM,
/* valor a ser lido
N;
/* contador de caracteres lidos
...
N = 0;
/* inicializao do contador de caracteres lidos
scanf ("Numero = %*d%n%d", &N, &NUM);
/* leitura do valor e utilizao do contador
40
*/
*/
*/
*/
41
CAPTULO 1 : INTRODUO AO C
42
43
CAPTULO 1 : INTRODUO AO C
/* ->
675 */
/* ->000000000675 */
/* ->
0675 */
/* ->
/* ->
/* ->
printf
printf
printf
printf
printf
printf
printf
/*
/*
/*
/*
/*
/*
/*
("->%12.6g\n", 675.0000);
("->%#12.6g\n", 675.0000);
("->%12.3g\n", 675.0);
("->%12.2g\n", 675.0);
("->%12.1g\n", 675.0);
("->%#12.1g\n", 675.0);
("->%#12.1G\n", 675.0);
->
->
->
->
->
->
->
675.950000 */
675.9500 */
676.0 */
675
675.000
675
6.8e+02
7e+02
7.e+02
7.E+02
*/
*/
*/
*/
*/
*/
*/
/* ->*<- */
/* ->
*<- */
/* ->*
<- */
44
...";
pam pum, cada bola ...<pam pum, cada bola ...<pam pum, cada bola ...</* ->pim pam pum</* -></* ->
pim</* ->pim
<-
*/
*/
*/
*/
*/
*/
*/
*/
*/
*/
*/
*/
*/
*/
45
CAPTULO 1 : INTRODUO AO C
*/
*/
*/
*/
*/
*/
*/
46
Classe de pertena
caracteres alfabticos e algarismos decimais
caracteres alfabticos
caracteres de controlo
algarismos decimais
todos os caracteres com representao grfica
caracteres alfabticos minsculos
todos os caracteres com representao grfica
mais o carcter espao
ispunct
todos os caracteres com representao grfica
menos os caracteres alfabticos e os algarismos decimais
isspace
espao, '\f', '\n', '\r', '\t' e '\v'
isupper
caracteres alfabticos maisculos
isxdigit
algarismos hexadecimais
funes de converso
Nome da funo
tolower
toupper
Tipo de converso
do alfabeto maisculo para o alfabeto minsculo
do alfabeto minsculo para o alfabeto maisculo
47
CAPTULO 1 : INTRODUO AO C
Significado
ex
x = frexp (x,y) * 2*y, com
frexp (x,y) ]0.5,1]
ldexp (double x, int y);
x * 2y
log (double x);
loge(x), com x>0
log10 (double x);
log10(x), com x>0
modf (double x, double *y);
x = modf (x,y) + *y, com
|modf (x,y)| [0,1[
pow (double x, double y);
xy, com xz0 ou y>0
e xt0 ou ento y no tem parte fraccionria
sqrt (double x);
x
48
A sua aplicao prtica vem muitas vezes associada com o envio da mensagem de erro
correspondente para o dispositivo convencional de sada de erro, que normalmente o
mesmo que o dispositivo convencional de sada, ou seja, o monitor.
Isto feito usando a funo perror, descrita no ficheiro de interface stdio.h, que imprime no
dispositivo convencional de sada de erro uma combinao de duas mensagens, separadas
pelo carcter :.
mensagem definida pelo programador : mensagem associada ao valor de errno
A Figura 1.76 apresenta um exemplo da utilizao da funo perror.
#include <stdio.h>
#include <errno.h>
...
errno = 0;
Z = log (X);
if (errno != 0)
/* ocorreu uma situao de erro */
{
perror ("erro no clculo de um logaritmo na rotina ...");
...
}
...
rand()
aps a
produzem
s, sendo
Significado
{0, 1, ... , RAND_MAX}
invocao desta funo,
uma sequncia de valores
s a semente de gerao
49
CAPTULO 1 : INTRODUO AO C
Significado
|x|
|x|
quociente e resto da diviso
inteira de x por y
quociente e resto da diviso
inteira de x por y
/* quociente */
/* resto */
A definio do tipo ldiv_t formalmente semelhante, substitui-se apenas int por long em
todas as ocorrncias.
A Figura 1.79 apresenta as funes para converso de cadeias de caracteres em quantidades
numricas.
Nome da funo
Significado
double atof (const char *str);
converte a cadeia de caracteres
apontada por str numa quantidade real de tipo double
int atoi (const char *str);
converte a cadeia de caracteres
apontada por str numa quantidade inteira de tipo int
Para alm dos tipos div_t e ldiv_t, a biblioteca stdlib define ainda o tipo size_t que o tipo
do resultado do operador sizeof. Contm tambm dois identificadores que podem ser
usados pela funo exit para indicar o estado de execuo de um programa. A constante
EXIT_FAILURE, que vale 1, pode ser usada para indicar a finalizao do programa sem
sucesso. A constante EXIT_SUCCESS, que vale 0, pode ser usada para indicar a
finalizao do programa com sucesso.
Esta biblioteca contm ainda as funes para gesto de memria, que permitem adjudicar e
libertar memria dinmica, que sero apresentadas mais tarde.
Captulo 2
COMPLEMENTOS SOBRE C
Sumrio
Este captulo dedicado aos aspectos mais avanados da linguagem C. Comeamos por
apresentar o conceito de funo generalizada, que o nico tipo de subprograma existente
e como atravs dela se implementam funes e procedimentos. Explicamos a passagem de
parmetros s funes e introduzimos os primeiros conceitos sobre ponteiros.
De seguida, apresentamos o modo como se definem os tipos de dados estruturados,
nomeadamente, os agregados unidimensionais, bidimensionais e tridimensionais, as cadeias
de caracteres e a biblioteca string que providencia funes para a sua manipulao e os
registos, que na linguagem C se designam por estruturas. Apresentamos tambm como se
definem tipos de dados enumerados.
Devido interaco entre os agregados e os ponteiros, o que se costuma designar por
dualidade ponteiro agregado, e necessidade de implementar programas mais eficientes
recorrendo a este novo tipo de dados, vamos expondo as suas caractersticas mais
avanadas medida que explicamos os tipos de dados estruturados. Apresentamos,
designadamente, os seus operadores especficos, a aritmtica de ponteiros, a dualidade
ponteiro agregado, para agregados unidimensionais e multidimensionais e a construo de
estruturas de dados versteis envolvendo agregados e ponteiros.
Apresentamos tambm as classes de armazenamento das variveis e redefinimos os
conceitos de objectos locais e globais, bem como dos nveis de visibilidade dos objectos
aplicados ao facto da linguagem C permitir a construo de aplicaes distribudas por
vrios ficheiros fonte.
2.1 Funes
No estabelecimento de solues de problemas complexos essencial controlar o grau de
complexidade da descrio. A descrio deve ser organizada de uma forma hierarquizada,
tambm designada de decomposio do topo para a base (Top-Down Decomposition), de
modo a que corresponda, a cada nvel de abstraco considerado, uma decomposio num
nmero limitado de operaes, com recurso a um nmero tambm limitado de variveis.
S assim possvel:
x Minimizar a interaco entre as diferentes operaes, impondo regras estritas na
formulao das dependncias de informao.
x Desenvolver metodologias que, de uma forma simples, conduzam demonstrao da
correco dos algoritmos estabelecidos.
x Conceber um desenho da soluo que:
x Promova a compactao atravs da definio de operaes reutilizveis em
diferentes contextos.
x Enquadre a mudana ao longo do tempo, possibilitando a alterao de
especificaes com um mnimo de esforo.
A transcrio da soluo numa linguagem de programao deve, depois, procurar reflectir a
hierarquia de decomposio que foi explicitada na descrio. Isto consegue-se fazendo um
uso intensivo dos mecanismos de encapsulamento de informao, presentes na
linguagem utilizada, para implementar cada operao como uma seco autnoma de
cdigo, ou seja, com um subprograma e promover, assim, uma visibilidade controlada dos
detalhes de implementao.
Desta forma, possvel:
x Separar a especificao da operao, ou seja, a descrio da sua funcionalidade interna e
do mecanismo de comunicao com o exterior, constituda pelas variveis de entrada e de
sada, da sua implementao.
x Introduzir caractersticas de robustez e de desenho para o teste no cdigo produzido.
x Validar de uma maneira controlada e integrar de um modo progressivo os diferentes
componentes da aplicao
x Planear com eficcia a distribuio de tarefas pelos diversos membros da equipa de
trabalho.
Na linguagem Pascal existem dois tipos de subprogramas. O procedimento, que se
apresenta na Figura 2.1, um mecanismo de encapsulamento de informao que permite a
construo de operaes mais complexas a partir de uma combinao de operaes mais
simples. Aps a sua definio, a nova operao identificada por um nome e por uma lista
opcional de parmetros de comunicao. Os parmetros de entrada, que representam
valores necessrios realizao da operao, e os parmetros de sada, que representam
valores produzidos pela realizao da operao.
Parmetros
de
Entrada
Procedimento
Parmetros
de
Sada
(0)
(0)
Figura 2.1 - Esquema de um procedimento.
Parmetros
de
Entrada
Resultado
de
Sada
Funo
(0)
Figura 2.2 - Esquema de uma funo.
Parmetros
de
Entrada
(0)
Funo
Generalizada
Parmetros
de
Sada
(0)
Resultado
de Sada
Aps a sua definio, a nova operao identificada por um nome, por uma lista opcional
de parmetros de comunicao, ou seja, os parmetros de entrada, os parmetros de sada,
e pela indicao do tipo do valor devolvido.
A Figura 2.4 apresenta a definio da funo de converso de distncias de milhas para
quilmetros no Pascal e na linguagem C.
function CONVERTE_DISTANCIA (ML: real): real;
(* no Pascal *)
const MIL_QUI = 1.609;
begin
CONVERTE_DISTANCIA := MIL_QUI * ML
end;
tipo de
sada
nome da
funo
lista de
parmetros
cabealho
ML * MIL_QUI;
corpo
uma varivel. Nessa situao indica que a varivel de tipo ponteiro. E quando utilizado
na lista de parmetros, precedendo um parmetro, indica que o parmetro um parmetro
de entrada-sada, ou seja, que uma passagem por referncia.
A Figura 2.7 apresenta um exemplo da manipulao de uma varivel atravs de um
ponteiro. A primeira linha de cdigo declara uma varivel A de tipo int e uma varivel PA
de tipo ponteiro para int. A atribuio PA = &A, coloca na varivel PA o endereo da
varivel A, da que, a varivel PA fica a apontar para a varivel A. A atribuio *PA = 23
coloca o valor 23 na varivel A, atravs do ponteiro PA.
int A, *PA;
...
PA = &A;
*PA = 23;
23
PA
/* aluso funo */
/* aluso funo */
/* definio das variveis */
/* invocao da funo */
tipo base
A[0]
10
A[1]
21
A[2]
-5
A[3]
13
B[0]
10.2
B[1]
11.8
B[2]
0.0
C[0]
22
C[1]
12
10
Existe, porm, uma diferena subtil entre as variveis de tipo agregado unidimensional de
um tipo base e as variveis de tipo ponteiro para o mesmo tipo base. Como se verifica do
mapa de reserva de espao em memria apresentado na Figura 2.14, no existe espao
directamente associado com a varivel A e, portanto, A um ponteiro constante, cujo valor
no pode ser modificado. Assim, instrues do tipo A = expresso; so ilegais.
11
int X[10];
...
INICIALIZAR (X, 10);
/* invocao da funo */
Figura 2.16 - Utilizao do operador sizeof para calcular o nmero de elementos de um agregado.
Um dos erros mais frequentemente cometido, por programadores que se esto a iniciar na
utilizao da linguagem C, ultrapassar a dimenso de um agregado, nomeadamente em
ciclos repetitivos for. No exemplo apresentado na Figura 2.17, o for vai processar o
agregado desde o elemento de ndice 0 at ao elemento de ndice 10, que no existe no
agregado. Acontece que nesta posio de memria que est armazenada a varivel I que
controla o for, pelo que, o seu valor vai ser reinicializado a zero e portanto o for de novo
repetido. Estamos perante um ciclo repetitivo infinito criado por engano.
int I, AR[10];
...
for (I = 0; I <= 10; I++) AR[I] = 0;
/* situao de erro */
Figura 2.17 - Situao de erro na utilizao do ciclo for para processar um agregado.
12
13
OLA[0]
'o'
OLA[1]
'l'
OLA[2]
'a'
OLA[3]
'\0'
TB[0]
't'
TB[1]
'u'
TB[2]
'd'
TB[3]
'o'
TB[4]
' '
TB[5]
'b'
TB[6]
'e'
TB[7]
'm'
TB[8]
'\0'
possvel criar um ponteiro para uma cadeia de caracteres e simultaneamente inici-lo com
uma cadeia de caracteres constante, usando para esse efeito a seguinte instruo.
char *ponteiro = "constante cadeia de caracteres"
A Figura 2.20 apresenta o mapa de reserva de espao em memria de um exemplo deste
tipo de declarao. O ponteiro PTS aponta para a cadeia de caracteres Aveiro, que uma
cadeia de caracteres constante, pelo que, no pode ser alterada. O valor de PTS pode ser
alterado, por exemplo, atribuindo-lhe outra constante, ou uma varivel, de tipo cadeia de
caracteres, e caso o seja, perde-se o acesso cadeia de caracteres Aveiro.
char *PTS = "Aveiro";
'A'
'v'
'e'
'i'
'r'
'o'
'\0'
PTS
4 bytes
Figura 2.20 - Mapa de reserva de espao em memria de um ponteiro para uma cadeia de caracteres
inicializado com uma cadeia de caracteres constante.
14
*/
*/
*/
*/
*/
*/
15
alfabeto minsculo
...
Figura 2.24 - Deslocamento circular de um carcter trs posies para a frente.
16
A Figura 2.25 apresenta a funo que codifica um carcter minsculo. A funo no testa
se o carcter ou no minsculo, pelo que, s deve ser invocada para caracteres
minsculos. Se o valor do deslocamento circular, que representado pelo parmetro de
entrada K, for positivo ento o carcter codificado K posies para a frente, se for
negativo ento o carcter codificado K posies para a trs, e se K for zero o carcter sai
inalterado. Apresentam-se as solues em Pascal e na linguagem C.
(* no Pascal *)
function CODIFICAR_MINUSCULO (CAR: char; K: integer): char;
var CAR_S : char;
begin
CAR_S := chr(ord('a') + (ord(CAR) - ord('a') + 26 + K) mod 26);
CODIFICAR_MINUSCULO := CAR_S
end;
Todas as funes copiam byte a byte o contedo da regio de memria apontada por zp para
a regio de memria apontada por zd e devolvem a localizao de zd. Est subjacente a
qualquer das funes que zd referencia uma regio de memria, cuja reserva de espao de
armazenamento foi previamente efectuada e que tem tamanho suficiente para que a
transferncia seja efectivamente possvel.
Para as funes memcpy e memmove, a transferncia realizada sem ser atribuda
qualquer interpretao ao contedo dos bytes transferidos, enquanto que, para strcpy e
strncpy, se supe que zp referencia uma cadeia de caracteres, ou seja, um agregado de
caracteres terminado obrigatoriamente pelo carcter nulo.
17
Ambas as funes copiam a cadeia de caracteres referenciada por zp, incluindo um carcter
nulo final, para o fim da cadeia de caracteres referenciada por zd e devolvem a localizao
de zd. O ponto de juno a localizao do carcter nulo final da cadeia de caracteres
referenciada por zd, que substitudo pelo primeiro carcter da cadeia de caracteres
referenciada por zp. Est subjacente a qualquer das funes que zd aponta para uma regio
de memria, cuja reserva de espao de armazenamento tem tamanho suficiente para conter
a cadeia de caracteres resultante. No caso da funo strncat, so copiados no mximo n
caracteres, excluindo o carcter nulo final, da cadeia de caracteres referenciada por zp. As
regies de memria apontadas por zp e zd tm que ser disjuntas.
A Figura 2.28 apresenta um exemplo da utilizao das funes strcpy e strcat.
char MENS1[] = "Ola", MENS2[] = "bom dia", MENS3[15];
...
strcpy (MENS3, MENS1);
strcat (MENS3, " ");
strcat (MENS3, MENS2);
18
As funes comparam byte a byte o contedo das regies de memria referenciadas por z1 e
z2. O processo de comparao termina logo que uma deciso possa ser univocamente
tomada.
O contedo da regio z1 diz-se menor do que o contedo da regio z2 quando, para o
primeiro par de posies correspondentes que so distintas, se verifica que o contedo da
posio de z1 menor do que o contedo da posio de z2, ambos os contedos
interpretados como unsigned char. Adicionalmente, o contedo da regio z1 menor do
que o contedo da regio z2 quando o tamanho da regio z1 menor do que o tamanho da
regio z2 e o contedo de cada posio de z1 igual ao contedo da posio
correspondente de z2.
O contedo da regio z1 diz-se igual ao contedo da regio z2 quando as duas regies tm
o mesmo tamanho e o contedo de cada posio de z1 igual ao contedo da posio
correspondente de z2. O contedo da regio z1 diz-se maior do que o contedo da regio
z2 quando se tem em alternativa que o contedo da regio z2 menor do que o contedo
da regio z1.
O valor devolvido ser positivo, quando o contedo da regio z1 for maior do que o
contedo da regio z2, zero, quando o contedo da regio z1 for igual ao contedo da
regio z2, e, negativo, quando o contedo da regio z1 for menor do que o contedo da
regio z2.
No caso da funo memcmp, comparam-se sempre n bytes sem ser atribuda qualquer
interpretao ao seu contedo, enquanto que, para strcmp e strncmp, se supe que z1 e
z2 referenciam cadeias de caracteres, ou seja, agregados de caracteres terminados
obrigatoriamente pelo carcter nulo, e o processo de comparao decorre at tomada
unvoca de uma deciso, no caso da funo strcmp, ou at um mximo de n caracteres
terem sido comparados no caso da funo strncmp.
A Figura 2.30 apresenta um exemplo que l e processa frases de texto, at um mximo de
NMAX frases ou at ao aparecimento da frase FIM. Quando se detecta a frase de
terminao, usando para esse efeito a funo strcmp, o ciclo repetitivo while
interrompido com a instruo break, de forma a evitar o processamento da frase de
terminao. Como a funo devolve 0 quando as duas cadeias de caracteres so iguais,
ento usa-se o operador ! para que a expresso seja verdadeira. Ou seja, a expresso
!strcmp(FRASE, FIM) equivalente expresso booleana strcmp(FRASE, FIM) == 0.
#define NMAX 10
...
char FRASE[80], FIM[] = "FIM"; int N = 0;
...
do
{
N++;
printf ("Escreva a frase -> ");
scanf ("%79[^\n]", FRASE);
/* leitura da frase
scanf ("%*[^\n]");
/* descartar todos os outros caracteres
scanf ("%*c");
/* descartar o carcter de fim de linha
if ( !strcmp (FRASE, FIM) ) break;
...
/* processar a frase lida
} while ( N < NMAX );
*/
*/
*/
*/
19
20
ponteiro nulo. Se existir, esse carcter substitudo pelo carcter nulo e constitui o fim da
palavra actual, cuja localizao devolvida pela funo. Antes de terminar, porm, a funo
strtok armazena internamente a localizao do carcter seguinte ao carcter nulo. Esta
referncia vai constituir o ponto de partida da prxima pesquisa que, para ser feita, exige a
invocao da funo com um ponteiro nulo em substituio de z.
A Figura 2.32 apresenta um excerto de cdigo da utilizao da funo strtok para
decompor uma cadeia de caracteres constituda por palavras separadas pelos caracteres $ e
#, e a Figura 2.33 apresenta a sua visualizao grfica. A primeira invocao da funo
detecta o incio da primeira palavra a seguir ocorrncia do carcter #. Enquanto a funo
no devolver o ponteiro nulo, e o agregado de ponteiros para char tiver capacidade de
armazenamento de informao, a funo invocada para encontrar o incio da prxima
palavra a seguir a um dos caracteres separadores. A segunda palavra comea a seguir ao
carcter #, a terceira palavra comea a seguir ao carcter $ e a quarta palavra comea a
seguir ao carcter #. Quando a funo acaba de processar a frase, ela ficou decomposta em
palavras, pelo que, ficou corrompida.
char FRASE[] = "#era$#uma$vez#um$"; char *PALAVRA[4], *PS; int N;
...
N = 0;
PS = strtok (FRASE, "$#");
while ( (PS != NULL) && (N < 4) )
{
PALAVRA[N] = PS;
N++;
PS = strtok (NULL, "$#");
}
'#'
'e' 'r'
'a'
'#'
'u' 'm'
'a'
'v'
'e'
'z'
FRASE
FINAL
'#'
'e' 'r'
'u' 'm'
'e'
'$'
'$'
'#'
'u'
'm'
PALAVRA[0]
PALAVRA[1]
PALAVRA[2]
PALAVRA[3]
'$' '\0'
21
A funo memset copia o valor c, previamente convertido para unsigned char, para os n
primeiros bytes da regio de memria referenciada por z. A funo strerror devolve a
mensagem associada com a varivel global errno. A funo strlen devolve o comprimento
da cadeia de caracteres, ou seja, o nmero de caracteres armazenados at ao carcter nulo.
A Figura 2.35 apresenta um exemplo da utilizao da funo strerror. Esta funo devolve
uma cadeia de caracteres que pode ser usada para escrever mensagens de erro associadas
varivel global de erro. Neste exemplo, logo aps a invocao da funo matemtica raiz
quadrada, a varivel global de erro testada para em caso de erro escrever a mensagem
respectiva no monitor atravs do printf. No clculo de uma raiz quadrada, h erro se o
valor de X for negativo, pelo que, nesse caso a mensagem indicar que o argumento X est
fora da gama permitida para o clculo da raiz quadrada.
#include <stdio.h>
#include <errno.h>
#include <math.h>
#include <string.h>
...
errno = 0;
Z = sqrt (X);
if (errno != 0)
/* se ocorreu uma situao de erro */
{
printf("ERRO -> %s\n", strerror(errno));
...
-> Numerical argument out of domain */
}
A funo sscanf decompe uma cadeia de caracteres referenciada por z, segundo as regras
impostas pelo formato de leitura indicado por formato, armazenando sucessivamente os
valores convertidos nas variveis, cuja localizao indicada na lista de ponteiros de
variveis. As definies do formato e da lista de ponteiros de variveis so as mesmas que
para as funes scanf e fscanf.
A funo sscanf fundamentalmente equivalente a fscanf. A diferena principal que a
sequncia de caracteres a converter obtida da cadeia de caracteres referenciada por z, em
vez de ser lida do ficheiro. Assim, a deteco do carcter nulo, que caracteriza a situao de
se ter atingido o fim da cadeia de caracteres, vai corresponder situao de deteco do
carcter de fim de ficheiro.
22
A Figura 2.37 apresenta um exemplo da utilizao da funo sscanf para decompor uma
cadeia de caracteres em subcadeias de caracteres. A cadeia de caracteres FRASE, que
armazena uma data decomposta nas suas componentes, CIDADE, DIA, MES e ANO,
para posterior impresso no monitor com outro formato.
char FRASE[] = "Aveiro, 25 de Fevereiro de 2003";
char CIDADE[10], MES[10]; int DIA, ANO;
...
sscanf (FRASE, "%9[^,],%d de %9s de %d", CIDADE, &DIA, MES, &ANO);
/* CIDADE igual a "Aveiro", DIA = 25, */
/* MES igual a Fevereiro e ANO = 2003 */
...
printf ("(%s) %2d/%s/%4d\n", CIDADE, DIA, MES, ANO);
/* impresso no monitor (Aveiro) 25/Fevereiro/2003 */
A funo sprintf cria uma cadeia de caracteres referenciada por z, constituda por texto e
pelos valores das expresses que formam a lista de expresses, segundo as regras impostas
pelo formato de escrita, indicado por formato. As definies do formato e da lista de
expresses so as mesmas que para as funes printf e fprintf.
A funo sprintf fundamentalmente equivalente a fprintf. A diferena principal que a
sequncia de caracteres convertida armazenada na regio de memria referenciada por z,
em vez de ser escrita no ficheiro. Assim, torna-se necessrio garantir que foi reservado
previamente espao de armazenamento suficiente para a sequncia de caracteres convertida
e para o carcter nulo final.
A Figura 2.38 apresenta um exemplo da utilizao da funo sprintf para a construo
dinmica de um formato de leitura para a funo scanf. Se pretendermos fazer a leitura da
cadeia de caracteres FRASE, que foi declarada com uma dimenso parametrizada pela
constante MAX_CAR, no podemos usar o formato %MAX_CARs, uma vez que o
formato de leitura da funo scanf s aceita literais. A soluo passa pela construo
dinmica do formato de leitura. A cadeia de caracteres FORMATO constituda pela
concatenao do carcter %, atravs do especificador de converso %%, com o valor de
MAX_CAR, atravs do especificador de converso %d, e com o carcter s, ou seja,
armazena o formato de leitura %40s.
#define MAX_CAR 40
...
char FRASE[MAX_CAR+1], FORMATO[20];
...
sprintf (FORMATO, "%%%ds", MAX_CAR); /* FORMATO igual a "%40s" */
...
scanf (FORMATO, FRASE);
/* equivalente a scanf ("%40s", FRASE); */
23
A[0]
A[1]
A[2]
B[0][0]
B[0][1]
B[0][2]
B[1][0]
B[1][1]
B[1][2]
A Figura 2.40 apresenta um excerto de cdigo que atribui valores aos agregados A e B. Para
aceder a todos os elementos do agregado bidimensional B necessitamos de um duplo ciclo
repetitivo for. Procure descobrir quais os valores que so armazenados nos agregados.
int A[3], B[2][3]; unsigned int I, J;
...
for (I = 0; I < 3; I++) A[I] = I;
for (I = 0; I < 2; I++)
for (J = 0; J < 3; J++)
...
B[I][J] = A[ (I+J)%3 ];
24
B[0][0]
B[0][1]
B[0][2]
B[1][0]
B[1][1]
B[1][2]
C[0][0]
10
C[0][1]
C[0][2]
C[1][0]
13
C[1][1]
14
C[1][2]
C[2][0]
C[2][1]
C[2][2]
O agregado B, que tem 23 elementos, inicializado com duas listas de inicializao, cada
uma delas constituda por trs constantes inteiras. Pelo que, todos os seus elementos so
inicializados. O agregado C, que tem 33 elementos, inicializado com apenas duas listas
de inicializao. A primeira lista de inicializao constituda apenas por uma constante,
pelo que, o elemento C[0][0] inicializado a 10 e os elementos C[0][1] e C[0][2] so
inicializados a 0. A segunda lista de inicializao constituda por duas constantes, pelo
que, os elementos C[1][0] e C[1][1] so inicializados a 13 e a 14 respectivamente, e o
elemento C[1][2] inicializado a 0. Como no existe a terceira lista de inicializao, os
elementos C[2][0], C[2][1] e C[2][2] so inicializados a 0.
Para alm dos agregados bidimensionais, por vezes existe a necessidade de utilizar
agregados tridimensionais. Por exemplo, para a simulao de um campo electromagntico
no espao. A Figura 2.42 apresenta a declarao e inicializao de dois agregados
tridimensionais, bem como da sua colocao na memria. Tal como na declarao de
agregados unidimensionais possvel omitir o descritor de dimenso, ou seja, fazer uma
definio incompleta, mas apenas da primeira dimenso, tal como feito na declarao do
agregado E. A primeira dimenso pode ser inferida pelo compilador a partir da expresso
de inicializao, e neste exemplo 2.
25
int D[2][2][2] = {
{
{0,
{2,
},
{
{4,
{6,
}
};
1},
3}
5},
7}
int E[ ][2][2] = {
{
{10},
{12, 13}
},
{
{14}
}
};
D[0][0][0]
D[0][0][1]
D[0][1][0]
D[0][1][1]
D[1][0][0]
D[1][0][1]
D[1][1][0]
D[1][1][1]
E[0][0][0]
10
E[0][0][1]
E[0][1][0]
12
E[0][1][1]
13
E[1][0][0]
14
E[1][0][1]
E[1][1][0]
E[1][1][1]
O agregado D, que tem 222 elementos, inicializado com quatro listas de inicializao,
cada uma delas constituda por 2 constantes inteiras. Pelo que, todos os seus elementos so
inicializados. O agregado E, que tem a mesma dimenso, inicializado com apenas trs
listas de inicializao com um total de 4 constantes. A primeira lista de inicializao
constituda apenas por uma constante, pelo que, o elemento E[0][0][0] inicializado a 10 e
o elemento E[0][0][1] inicializado a 0. A segunda lista de inicializao constituda por
duas constantes, pelo que, os elementos E[0][1][0] e E[0][1][1] so inicializados a 12 e a 13
respectivamente. A terceira lista de inicializao constituda por apenas uma constante,
pelo que, o elemento E[1][0][0] inicializado a 14 e o elemento E[1][0][1] inicializado a 0.
Como no existe a quarta lista de inicializao, os elementos E[1][1][0] e E[1][1][1] so
inicializados a 0.
Mas, apesar de normalmente no serem necessrios agregados com mais de duas ou trs
dimenses, a linguagem C tal como o Pascal no limita o nmero de dimenses de um
agregado. Segundo a norma ANSI, os compiladores devem suportar pelo menos seis
dimenses.
Na passagem de agregados multidimensionais a uma funo preciso passar um ponteiro
para o incio do agregado, usando para o efeito o nome do agregado seguido dos parnteses
rectos, tal como, na passagem de um agregado unidimensional. Mas, como o elemento
inicial de um agregado multidimensional tambm um agregado, obrigatrio indicar o
nmero de elementos de cada uma das N1 dimenses direita. Ou seja, apenas a
dimenso mais esquerda pode ser omitida. A Figura 2.43 apresenta a definio de uma
funo que inicializa a zero todos os elementos de um agregado tridimensional. A funo
tem um parmetro de entrada-sada que o agregado a inicializar e um parmetro de
entrada que a primeira dimenso do agregado.
26
int AMULT[L][M][N];
...
INICIALIZAR (AMULT, L);
/* invocao da funo */
int AMULT[L][M][N];
...
INICIALIZAR (AMULT, L, M, N);
/* invocao da funo */
2.5 Ponteiros
Como j foi referido anteriormente, um ponteiro embora assuma o valor de um endereo,
no propriamente um endereo, porque ele est sempre associado a um tipo de dados
bem definido. Da que duas variveis de tipo ponteiro podem conter o mesmo valor, ou
seja, o mesmo endereo, e no entanto constiturem de facto entidades distintas.
27
PI
PD
VD
28
int A;
void V; /* declarao ilegal porque a varivel V no tem sentido
void *PV;
/* a varivel PV representa um ponteiro genrico
...
PV = &A;
*PV = 5; /* instruo ilegal porque a varivel *PV no tem sentido
*((int *) PV) = 5;
/* agora A = 5
*/
*/
*/
*/
*/
*/
*/
*/
*/
Na Figura 2.49 a varivel PPA foi declarada do tipo ponteiro para ponteiro para int. A
Figura 2.50 apresenta a sua visualizao grfica e a atribuio do valor 45 varivel A,
atravs de uma dupla referncia indirecta com a instruo de atribuio **PPA = 45.
int A, *PA = &A, **PPA = &PA;
...
**PPA = 45;
45
PA
PPA
29
*/
*/
*/
*/
*/
*/
*/
30
Associatividade
Precedncia
operadores primrios
( )
[ ]
referncia a campo ->
acesso a campo .
esquerda
esquerda
esquerda
esquerda
o
o
o
o
direita
direita
direita
direita
operadores unrios
operador
operador
operador
operador
cast
sizeof
endereo &
apontado por *
maior
direita
direita
direita
direita
o
o
o
o
esquerda
esquerda
esquerda
esquerda
menor
AP[0]
AP[1]
AP[2]
AP[3]
31
TIPO_BASE (*PA)[4];
PA
(*PA)[0]
(*PA)[1]
(*PA)[2]
(*PA)[3]
B[0]
B[1]
32
Existe, porm, uma diferena subtil entre as variveis de tipo agregado bidimensional de
um tipo base e as variveis de tipo ponteiro para um agregado unidimensional, de tamanho
igual segunda dimenso do agregado bidimensional do mesmo tipo base. Como se
verifica do mapa de reserva de espao em memria apresentado na Figura 2.56, no existe
espao directamente associado com a varivel B e, portanto, B, B[0] e B[1] so ponteiros
constantes, cujos valores no podem ser modificados. Assim, instrues do tipo
B = expresso; ou B[i] = expresso; so ilegais e tem-se que *(B+i) = B[i].
Perante a declarao TIPO_BASE A[N1], B[N1][N2], C[N1][N2][N3]; tem-se que:
x A um ponteiro constante para TIPO_BASE.
x B um ponteiro constante para um agregado unidimensional de N2 elementos do
TIPO_BASE e B[i] um ponteiro constante para TIPO_BASE, com 0 d i < N1.
x C um ponteiro constante para um agregado bidimensional de N2N3 elementos do
TIPO_BASE, C[i] um ponteiro constante para um agregado unidimensional de N3
elementos do TIPO_BASE, com 0 d i < N1, e, C[i][j] um ponteiro constante para
TIPO_BASE, com 0 d i < N1 e 0 d j < N2.
FLORES[0]
FLORES[1]
FLORES[2]
FLORES[3]
'r'
'd'
'c'
'o'
'a'
'r'
's'
'h'
'a'
'a'
'l'
'v'
'\0'
'i'
'o'
'a'
'\0'
'\0'
representao grfica
de ponteiro nulo
33
return MES_EXTENSO[0];
return MES_EXTENSO[MES];
}
34
35
struct tdados_pessoa
{
char NOME[60];
char SEXO;
unsigned int DIA;
unsigned int MES;
unsigned int ANO;
} PESSOA, GRUPO PESSOAS[10], *PPESSOA;
Figura 2.62 - Exemplo da declarao de uma estrutura e da declarao de variveis desse tipo.
Existe uma outra forma de criar um novo tipo de dados em linguagem C usando para esse
efeito a instruo typedef, que equivalente instruo type do Pascal. A Figura 2.63
apresenta esta forma alternativa.
typedef struct
{
char NOME[60];
char SEXO;
unsigned int DIA;
unsigned int MES;
unsigned int ANO;
} TDADOS_PESSOA;
36
Na linguagem C possvel atribuir uma estrutura a outra estrutura, tal como no Pascal. A
Figura 2.66 apresenta alguns exemplos de instrues de atribuio envolvendo estruturas.
TDADOS_PESSOA FUNC_PESSOA (void); /* aluso funo FUNC_PESSOA */
...
TDADOS_PESSOA PESSOA1 = { "Vincent Van Gogh", 'M', 30, 3, 1853 };
TDADOS_PESSOA PESSOA2, *PPESSOA;
...
PESSOA2 = PESSOA1;
...
PPESSOA = &PESSOA1;
PESSOA2 = *PPESSOA;
...
PESSOA2 = FUNC_PESSOA ();
...
/* acesso ao campo DIA atravs da varivel PESSOA */
PESSOA.DIA = 30;
...
/* acesso ao campo DIA atravs do ponteiro PPESSOA */
PPESSOA->DIA = 30;
/* equivalente a (*PPESSOA).DIA = 30; */
Na formao de expresses de acesso aos campos de uma estrutura, preciso ter em conta
que os operadores de acesso aos campos de estruturas so operadores primrios, com
associatividade da esquerda para a direita e com menor precedncia do que os operadores
primrios ( ) e [ ], tal como se mostra na Figura 2.53.
37
da
estrutura
TDADOS_PESSOA,
typedef struct
{
unsigned int DIA;
unsigned int MES;
unsigned int ANO;
} TDATA;
typedef struct
{
char NOME[60];
char SEXO;
TDATA DATA_NASCIMENTO;
} TDADOS_PESSOA;
...
/* acesso ao campo DIA da DATA atravs da varivel PESSOA */
PESSOA.DATA_NASCIMENTO.DIA = 30;
...
/* acesso ao campo DIA da DATA atravs do ponteiro PPESSOA */
PPESSOA->DATA_NASCIMENTO.DIA = 30;
Figura 2.70 - Acesso aos campos de uma estrutura interna a outra estrutura.
38
Uma estrutura pode conter ponteiros para estruturas ainda no definidas. A Figura 2.72
apresenta duas estruturas que se referenciam mutuamente. Cada estrutura tem campos que
vo conter a informao a armazenar na estrutura, e um campo de tipo ponteiro que
aponta para a outra estrutura.
typedef struct ts1
{
...;
...;
struct ts2 *PST;
} TS1;
typedef struct ts2
{
...;
...;
struct ts1 *PST;
} TS2;
TDADOS_PESSOA PESSOA;
...
LER DADOS PESSOA (&PESSOA);
/* invocao da funo */
Figura 2.73 - Definio e invocao de uma funo com uma estrutura passada por referncia.
39
TDADOS_PESSOA PESSOA;
...
ESCREVER DADOS PESSOA (PESSOA);
/* invocao da funo */
Figura 2.74 - Definio e invocao de uma funo com uma estrutura passada por valor.
Figura 2.76 - Exemplo de uma funo que processa um agregado de estruturas (1 verso).
40
A Figura 2.77 apresenta uma segunda verso, em que o acesso aos elementos do agregado
feito atravs de um ponteiro que aponta para o elemento pretendido.
int NUMERO_HOMENS (TDADOS_PESSOA GRUPO_PESSOAS[ ], int N)
{
int I, NUM = 0; TDADOS_PESSOA *P = GRUPO_PESSOAS;
for (I = 0; I < N; P++, I++)
if (P->SEXO == 'M') NUM++;
return NUM;
}
Figura 2.77 - Exemplo de uma funo que processas um agregado de estruturas (2 verso).
41
A Figura 2.81 apresenta os mesmos exemplos, mas recorrendo ao typedef para definir
primeiro os tipos de dados enumerados T_COR e T_DIA_UTIL, ficando a declarao
das variveis para depois. Como os valores atribudos aos identificadores da lista do dia da
semana so seguidos, ento atribui-se apenas o valor inicial ao primeiro elemento da lista.
typedef enum { BRANCO, AZUL, VERDE, ROSA, PRETO } T_COR;
...
T_COR COR;
42
T_DIA_UTIL DIA;
...
do
{
printf ("Dia da semana (SEGUNDA=2 ... SEXTA=6)? ");
scanf ("%1d", &DIA);
} while (DIA<2 || DIA>6);
/* admitindo que foi introduzido o valor 2, ento DIA = SEGUNDA */
DIA += 4;
/* agora DIA = SEXTA */
DIA--;
/* agora DIA = QUINTA */
...
if (DIA > SEXTA)
/* a expresso falsa */
printf ("*** Aleluia Fim de Semana ***\n");
43
Uma varivel de durao permanente s pode ser inicializada com uma expresso que
contenha apenas literais. Se a varivel no for inicializada dentro de um funo, ento fica
automaticamente inicializada a zero.
A linguagem C permite que o programador pea ao compilador para colocar o contedo de
uma varivel num registo do processador. As operaes envolvendo dados armazenados
em registos so mais rpidas que as operaes envolvendo dados armazenados em posies
de memria, porque, evitam a perda de tempo necessria a que que os dados sejam trazidos
da memria para o processador e que o resultado da operao seja colocado de novo na
memria. Para esse efeito declara-se a varivel com o qualificativo register. Infelizmente, o
nmero de registos existentes no processador limitado, pelo que, no h qualquer garantia
que o pedido seja respeitado.
Porque as variveis declaradas com o qualificativo register, podem no existir na memria,
o operador endereo no pode ser usado sobre essas variveis, ou seja, os registos no so
endereveis. S podem ser declaradas com o qualificativo register variveis declaradas
dentro de funes. Esta prorrogativa deve ser apenas usada para variveis que so acedidas
frequentemente.
A linguagem C tambm permite declarar variveis, cujo valor no pode ser alterado depois
da sua inicializao. Nesse caso estamos perante uma constante que ao contrrio de uma
constante definida com a directiva de define, que designada por constante simblica, ocupa
espao em memria. Para declarar uma constante usa-se o qualificativo const. A Figura
2.84 apresenta uma nova verso da funo CONVERTE_DISTANCIA em que o factor
de converso uma constante real local.
double CONVERTE_DISTANCIA (double ML)
{
const double MIL_QUI = 1.609;
/* definio da funo */
return ML * MIL_QUI;
}
A grande vantagem de declarar uma varivel constante assegurar que a varivel est
protegida contra escrita e portanto, no perde o seu valor original. Estas variveis so
muitas vezes utilizadas como parmetros de entrada de funes, como por exemplo em
funes da biblioteca de execuo ANSI string, de forma a assegurar que o parmetro no
corrompido pela execuo da funo. Veja por exemplo o prottipo da funo strcpy.
char *strcpy (char *zd, const char *zp);
No caso da declarao de ponteiros, a palavra reservada const, pode aparecer de duas
formas distintas, tendo por isso significados diferentes. A seguinte declarao, declara um
ponteiro constante para um inteiro, ou seja, um ponteiro que aponta sempre para o mesmo
endereo de memria.
int *const PCONSTANTE;
No entanto, a seguinte declarao, declara um ponteiro que aponta para um inteiro
constante, ou seja, um ponteiro que pode apontar para qualquer endereo, desde que este
seja o de uma varivel de tipo inteiro e constante.
int const *PINTCONST;
44
/* Varivel Local */
preciso ter em ateno ao duplo significado do qualificativo static. Quando uma varivel
static declarada fora de uma funo, significa que a varivel global mas com alcance
circunscrito ao ficheiro. Quando uma varivel static declarada dentro de uma funo,
significa que a varivel local mas de durao permanente.
As variveis globais so armazenadas na memria RAM, enquanto que as variveis locais
so armazenadas na memria stack. Uma funo como uma caixa preta, em que os
detalhes associados com a implementao da operao, nomeadamente as variveis locais,
so invisveis externamente. Mas, por outro lado, todas as variveis declaradas globalmente
so visveis dentro das funes. Em caso de conflito, a varivel instanciada aquela que
est declarada mais perto.
Numa funo no possvel declarar uma varivel local com o mesmo nome de um
parmetro da funo. Mas, como as variveis locais so invisveis externamente, pode-se
declarar variveis locais com o mesmo nome em funes diferentes. A Figura 2.86
apresenta exemplos de declarao e utilizao de variveis com diferente visibilidade.
45
static int I, J;
Quando temos uma aplicao distribuda por vrios ficheiros fonte, por vezes necessrio
que uma funo de um ficheiro aceda a variveis globalmente globais que foram declaradas
noutros ficheiros. At agora estivemos a considerar que cada declarao de uma varivel
produz alocao de memria para a varivel. No entanto, a alocao de memria s feita
quando h uma definio de uma varivel. Uma varivel global pode ser declarada sem
produzir alocao de memria. Essa declarao chama-se aluso e usa-se para o efeito o
qualificativo extern. A Figura 2.87 apresenta a aluso a uma varivel globalmente global. O
objectivo de uma aluso permitir que o compilador faa a verificao de tipos. As aluses
de variveis so normalmente colocadas em ficheiros cabealho, assegurando assim aluses
consistentes. Para definir uma varivel global, a varivel deve ser declarada sem o
qualificativo extern e deve incluir a inicializao da varivel. Para aludir uma varivel global
ela deve ser declarada com o qualificativo extern e no deve incluir qualquer inicializao.
int main (void)
{
int I;
extern int J;
...
}
Por vezes tambm existe a necessidade de que uma funo seja apenas visvel no ficheiro
onde est definida. Desta forma, pode-se definir funes com o mesmo nome em ficheiros
diferentes. Para esse efeito, coloca-se o qualificativo static antes da definio da funo.
normalmente aplicado a funes internas a outras funes, que portanto, no tm a
necessidade de serem reconhecidas fora do ficheiro onde esto definidas.
Captulo 3
FICHEIROS EM C
Sumrio
Em muitas aplicaes prticas, como por exemplo na utilizao de bases de dados, a
quantidade de informao de entrada tanta, que o simples facto de termos que a
introduzir de novo sempre que executamos o programa torna-o pouco funcional. Por
outro lado, neste tipo de aplicaes os resultados de sada precisam de ser salvaguardados
para posterior utilizao. O tipo ficheiro um tipo estruturado que tem como suporte de
armazenamento a memria de massa, e no a memria principal do computador, pelo que,
um ficheiro no s armazena dados a ttulo definitivo, como tambm permite a
comunicao de informao entre programas. Como um ficheiro tambm uma estrutura
de dados dinmica, permite tambm ultrapassar a limitao das estruturas de dados
estticas, como so os agregados de registos, pelo que, a estrutura de dados adequada
para suportar o processamento de bases de dados.
A norma ANSI da linguagem C cria um modelo uniforme de acesso aos diferentes
dispositivos do sistema computacional. Quer se trate dos dispositivos de entrada e de sada,
ou seja, dos perifricos, ou de ficheiros localizados na memria de massa, o acesso
sempre efectuado associando um fluxo de comunicao ao dispositivo. Um fluxo de
comunicao uma sequncia ordenada de bytes, armazenada numa dada regio da
memria principal, que materializa o fluxo de dados entre o programa e o dispositivo.
Portanto, ao contrrio da linguagem Pascal, em que um ficheiro visto como tendo uma
estrutura interna ou registo associado, na linguagem C um ficheiro no mais do que uma
sequncia ordenada de bytes. Ler de, ou escrever para, um dado ficheiro, significa portanto,
ler do, ou escrever no, fluxo de comunicao associado ao ficheiro. Na linguagem C
existem dois tipos de fluxos de comunicao, que so os fluxos de texto e os fluxos
binrios. Vamos apresentar os dois atravs de exemplos de aplicao.
CAPTULO 3 : FICHEIROS EM C
x O fluxo de texto stdin est associado com o dispositivo convencional de entrada, que
o teclado.
x O fluxo de texto stdout est associado com o dispositivo convencional de sada, que o
monitor.
x O fluxo de texto stderr est associado com o dispositivo convencional de sada de erro,
que tambm o monitor.
Para garantir a portabilidade, o ficheiro de interface stdio.h, define ainda duas constantes.
A constante EOF (end of file), que sinaliza o fim de ficheiro e a constante NULL, que o
ponteiro nulo, ou seja o valor de um ponteiro que no localiza qualquer regio de memria.
A escrita s pode ser efectuada no fim do contedo j existente. Este modo usado
tipicamente para ficheiros.
x a+ Abertura ou criao de um dispositivo para escrita no fim e leitura.
A escrita s pode ser efectuada no fim do contedo j existente, a leitura pode ocorrer em
qualquer ponto. Este modo usado tipicamente para ficheiros.
A Figura 3.2 sumaria as permisses dos modos de acesso.
Modo de acesso
Existncia prvia do dispositivo
Dispositivo aberto ou criado sem contedo
Leitura do fluxo permitida
Escrita no fluxo permitida
Escrita permitida s no fim do fluxo
r
*
*
*
*
*
*
r+ w+ a+
*
*
*
*
*
*
*
*
*
CAPTULO 3 : FICHEIROS EM C
*/
*/
*/
*/
*/
*/
A funo fscanf l do fluxo de texto, cujo nome indicado pelo identificador do fluxo de
entrada, sequncias de caracteres e processa-as segundo as regras impostas pelo formato de
leitura, armazenando sucessivamente os valores convertidos nas variveis, cuja localizao
indicada na lista de ponteiros de variveis. As definies de formato de leitura e de lista de
ponteiros de variveis so as mesmas que para a funo scanf.
Salvo em duas situaes especiais, deve existir uma relao de um para um entre cada
especificador de converso e cada varivel da lista de ponteiros de variveis. Se o nmero
de variveis da lista de ponteiros de variveis for insuficiente, o resultado da operao no
est definido. Se, pelo contrrio, o nmero de variveis for demasiado grande, as variveis
em excesso no so afectadas. O tipo da varivel e o especificador de converso devem ser
compatveis, j que a finalidade deste ltimo indicar, em cada caso, que tipos de
sequncias de caracteres so admissveis e como devem ser tratadas. Quando o
especificador de converso no vlido, o resultado da operao no est definido. O
processo de leitura s termina quando o formato de leitura se esgota, quando lido o
carcter de fim de ficheiro, ou quando existe um conflito de tipo entre o que est indicado no
formato de leitura e a correspondente quantidade a ser lida. Neste ltimo caso, o carcter
que causou o conflito mantido no fluxo de texto. A funo devolve o nmero de valores
lidos e armazenados, ou o valor EOF (end of file), se o carcter de fim de ficheiro lido antes
que qualquer converso tenha lugar. Se, entretanto, ocorreu um conflito, o valor devolvido
corresponde ao nmero de valores lidos e armazenados at ocorrncia do conflito.
O maior problema na leitura de um ficheiro a correcta deteco do carcter de fim de
ficheiro. Na linguagem Pascal a deteco do carcter de fim de ficheiro feito em avano
quando se l o ltimo carcter til do ficheiro, da que se pode ler um ficheiro com um
ciclo repetitivo repeat until. Como na linguagem C o carcter de fim de ficheiro s
detectado aps uma tentativa frustrada de leitura, ento o ciclo de leitura no pode ser do
tipo do while, tem que ser do tipo while. Por outro lado, a forma mais eficaz de o
detectar, consiste em aproveitar o facto da funo fscanf devolver o valor EOF quando o
carcter de fim de ficheiro lido antes que qualquer converso tenha lugar.
CAPTULO 3 : FICHEIROS EM C
A Figura 3.8 apresenta a segunda soluo, que consiste em assinalar com uma mensagem
de erro sempre que se l uma linha desformatada, e tenta passar por cima dela de forma a
continuar a ler, se possvel, o ficheiro at ao fim.
#define NITEMS 4
...
FILE *FPENT; int NLIDOS;
...
/* leitura da linha completa de acordo com o formato esperado */
while ( (NLIDOS = fscanf (FPENT, "formato\n", ponteiros)) != EOF )
{
/* no ocorreu EOF mas preciso aferir a consistncia da linha */
if ( NLIDOS != NITEMS )
/* lido o nmero esperado de items? */
fprintf (stderr, "Linha desformatada\n");
else ... ;
...
}
...
fclose (FPENT);
...
/* fecho do ficheiro */
#define NITEMS 4
...
FILE *FPENT; int NLIDOS;
...
while ( !feof(FPENT) )
{
/* leitura da linha completa de acordo com o formato esperado */
NLIDOS = fscanf (FPENT, "formato\n", ponteiros)
/* no ocorreu EOF mas preciso aferir a consistncia da linha */
if ( NLIDOS != NITEMS )
/* lido o nmero esperado de items? */
fprintf (stderr, "Linha desformatada\n");
else ... ;
}
...
fclose (FPENT);
...
stderr
Figura 3.10 - Definio formal da funo fprintf.
A funo fprintf escreve sucessivamente no fluxo de texto, cujo nome indicado pelo
identificador do fluxo de sada, sequncias de caracteres, representativas de texto e dos
valores das expresses que formam a lista de expresses, segundo as regras impostas pelo
formato de escrita. As definies de formato de escrita e de lista de expresses so as
mesmas que para a funo printf.
O texto especificado no formato de escrita pela introduo de literais, que so copiados
sem modificao para o fluxo de sada. O modo como o valor das expresses convertido,
descrito pelos especificadores de converso. Salvo numa situao especial, deve existir
uma relao de um para um entre cada especificador de converso e cada expresso da lista
de expresses. Se o nmero de expresses for insuficiente, o resultado da operao no
est definido. Se, pelo contrrio, esse nmero for demasiado grande, as expresses em
excesso so ignoradas.
O tipo da expresso e o especificador de converso devem ser compatveis, j que a
finalidade deste ltimo indicar, em cada caso, o formato da sequncia convertida. Quando
o especificador de converso no vlido, o resultado da operao no est definido. O
processo de escrita s termina quando o formato de escrita se esgota, ou quando ocorre
um erro. A funo devolve o nmero de caracteres escritos no fluxo de sada, ou o valor
1, se ocorreu um erro.
CAPTULO 3 : FICHEIROS EM C
A Figura 3.11 apresenta um exemplo em que vamos ler um ficheiro de texto formatado e
escrever a informao lida noutro ficheiro de texto formatado, eventualmente com um
formato diferente. Vamos considerar que o ficheiro de entrada foi aberto com sucesso,
sendo FPENT o ponteiro para FILE identificador do fluxo de texto de entrada, e que o
ficheiro para escrita foi aberto com sucesso, sendo FPSAI o ponteiro para FILE
identificador do fluxo de texto de sada. S as linhas que esto de acordo com o formato de
leitura esperado sero escritas no ficheiro de sada com o formato pretendido.
#define NITEMS 4
...
FILE *FPENT, *FPSAI; int NLIDOS;
...
/* leitura da linha completa de acordo com o formato esperado */
while ( (NLIDOS = fscanf (FPENT, "formato\n", ponteiros)) != EOF )
{
/* no ocorreu EOF mas preciso aferir a consistncia da linha */
if ( NLIDOS != NITEMS )
/* lido o nmero esperado de items? */
fprintf (stderr, "Linha desformatada\n");
else
{
... ;
/* processamento da informao lida */
/* escrita da linha completa de acordo com o formato pretendido */
fprintf (FPSAI, "formato\n", expresses);
}
...
}
...
fclose (FPENT);
fclose (FPSAI);
...
10
A funo fgets l caracteres do fluxo de texto especificado por fluxo, para a cadeia de
caracteres referenciada por s. O fluxo de texto foi inicializado pela invocao prvia de
fopen, nos modos de acesso que permitam a leitura. Os caracteres so lidos at que seja
detectado o carcter de fim de linha, ou o carcter de fim de ficheiro, ou at terem sido lidos
n1 caracteres. A funo adiciona automaticamente o carcter nulo final. Se bem sucedida,
a funo devolve um ponteiro para a cadeia de caracteres s. Se foi encontrado o carcter de
fim de ficheiro antes da leitura de qualquer carcter, a cadeia de caracteres s fica inalterada e
devolvido o ponteiro nulo. Se, entretanto, ocorreu um erro, devolvido o ponteiro nulo,
mas o contedo da cadeia de caracteres imprevisvel.
A funo fputs escreve a cadeia de caracteres referenciada por s, no fluxo de texto
especificado por fluxo. A cadeia de caracteres tem de ser obrigatoriamente terminada com
o carcter nulo final, carcter esse que no escrito no fluxo de texto. preciso ter em
ateno que, a funo no escreve o carcter de fim de linha. Se bem sucedida, a funo
devolve o valor 0 (zero), ou um valor diferente de 0, em caso contrrio.
11
CAPTULO 3 : FICHEIROS EM C
12
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
int main (int argc, char *argv[])
{
FILE *FPENT, *FPSAI; char CAR;
if ( argc < 3 )
/* o nmero de argumentos suficiente? */
{
fprintf (stderr, "Uso: %s ficheiro ficheiro\n", argv[0]);
exit (EXIT_FAILURE);
}
/* abertura do ficheiro de entrada cujo nome argv[1] */
if ( (FPENT = fopen (argv[1], "r") ) == NULL )
{
fprintf (stderr, "No foi possvel abrir o ficheiro %s\n"\
, argv[1]);
exit (EXIT_FAILURE);
}
/* criao do ficheiro de sada cujo nome argv[2] */
if ( (FPSAI = fopen (argv[2], "w") ) == NULL )
{
fprintf (stderr, "No foi possvel criar o ficheiro %s\n"\
, argv[2]);
fclose (FPENT);
/* fecho do ficheiro de entrada */
exit (EXIT_FAILURE);
}
/* leitura do carcter do ficheiro de entrada e escrita do */
/* carcter convertido para minsculo no ficheiro de sada */
while ( fscanf (FPENT, "%c", &CAR) != EOF )
fprintf (FPSAI, "%c", tolower(CAR));
fclose (FPENT);
fclose (FPSAI);
return EXIT_SUCCESS;
}
13
CAPTULO 3 : FICHEIROS EM C
if ( argc < 2 )
/* o nmero de argumentos suficiente? */
{
fprintf (stderr, "Uso: %s nome do ficheiro\n", argv[0]);
exit (EXIT_FAILURE);
}
/* abertura do ficheiro de entrada cujo nome argv[1] */
if ( (FPENT = fopen (argv[1], "r") ) == NULL )
{
fprintf (stderr, "No foi possvel abrir o ficheiro %s\n"\
, argv[1]);
exit (EXIT_FAILURE);
}
printf ("Aniversariantes a 29 de Fevereiro\n");
/* leitura da linha do ficheiro de entrada */
while ( (NLIDOS = fscanf (FPENT, "%*d:%40[^:]:%d:%d:%*d:%9s\n"\
, NOME, &DIA, &MES, TEL)) != EOF )
if ( NLIDOS != NITEMS )
/* lido o nmero esperado de items? */
fprintf (stderr, "Linha desformatada\n");
/* escrita no monitor dos aniversariantes a 29/Fev */
else if ( DIA == 29 && MES == 2)
printf ("NOME -> %-40.40s TELEFONE -> %s\n", NOME, TEL);
fclose (FPENT);
return EXIT_SUCCESS;
/* fecho do ficheiro */
Figura 3.15 - Processamento de um ficheiro constitudo por linhas com um formato especfico.
Como exerccio de treino, altere o programa para escrever a informao de sada num
ficheiro de texto, cujo nome passado na linha de comando.
14
A funo fread l do fluxo binrio, cujo nome indicado pelo identificador do fluxo, o
nmero de elementos indicado, todos com o mesmo tamanho em bytes, e armazena-os na
regio de memria referenciada pela localizao indicada. A regio de memria referenciada
tem que ter capacidade para o armazenamento da informao solicitada. O processo de
leitura termina quando lido o nmero total de bytes pretendido, ou quando lido o
carcter de fim de ficheiro, ou quando ocorreu um erro. A funo devolve o nmero de
elementos que foram efectivamente lidos e armazenados, que pode ser menor do que o
nmero de elementos pretendidos, se o carcter de fim de ficheiro lido antes, ou se ocorreu
um erro. Se o nmero de elementos lidos for zero, ento o valor devolvido 0 (zero) e,
quer a regio de memria referenciada, quer o fluxo binrio no so afectados.
A funo fwrite escreve no fluxo binrio, cujo nome indicado pelo identificador do fluxo,
o nmero de elementos indicado, todos com o mesmo tamanho em bytes, que esto
armazenados na regio de memria referenciada pela localizao indicada. O processo de
escrita termina quando escrito o nmero total de bytes pretendido, ou quando ocorre um
erro. A funo devolve o nmero de elementos que foram efectivamente escritos no fluxo
de sada, que pode ser menor do que o nmero de elementos pretendidos, se ocorreu um
erro. Se o nmero de elementos escritos for zero, o valor devolvido 0 (zero) e o fluxo
binrio no afectado.
A Figura 3.17 exemplifica a utilizao das funes fread e fwrite para ler e escrever uma
estrutura de tipo TDADOS_PESSOA em ficheiros binrios.
TDADOS_PESSOA PESSOA;
...
/* leitura de uma estrutura */
fread (&PESSOA, sizeof (TDADOS_PESSOA), 1, FPENT);
...
/* escrita de uma estrutura */
fwrite (&PESSOA, sizeof (TDADOS_PESSOA), 1, FPOUT);
15
CAPTULO 3 : FICHEIROS EM C
16
A funo remove remove do sistema de ficheiros o ficheiro cujo nome indicado por
nome do ficheiro. A funo devolve 0 (zero), se tiver sucesso, e um valor no nulo, em
caso contrrio.
A funo rename altera o nome do ficheiro cujo nome indicado por nome antigo para o
nome novo. A funo devolve 0 (zero), se tiver sucesso, e um valor no nulo, em caso
contrrio.
A funo tmpfile cria um ficheiro binrio temporrio que automaticamente apagado
quando fechado, ou quando o programa termina. O ficheiro criado no modo de acesso
wb+.
A funo tmpnam gera um nome vlido de ficheiro que distinto de qualquer nome j
existente. At TMP_MAX vezes, no mnimo 25, garantido que os nomes sucessivamente
gerados so diferentes. Se o nome do ficheiro for um ponteiro nulo, a funo devolve a
localizao de uma regio de memria interna onde est armazenado o nome, caso
contrrio, dever apontar para uma regio de memria com capacidade de armazenamento
de L_tmpnam caracteres, e ser esse o valor devolvido. O valor de L_tmpnam est
definido no ficheiro de interface limits.h.
17
CAPTULO 3 : FICHEIROS EM C
Uma vez que se pretende efectuar a listagem do ficheiro por ordem alfabtica do nome,
ento temos que manter a agenda telefnica sempre ordenada inserindo cada novo registo
no stio certo. O que implica pesquisar o ficheiro procura do local de insero do novo
registo e depois deslocar todos os registos que se encontram abaixo do local de insero,
um registo para baixo, de forma a abrir espao para escrever o novo registo. A Figura 3.22
apresenta a estrutura do ficheiro binrio, que constitudo por um cabealho que indica o
nmero de registos presentes no ficheiro e o corpo que um conjunto de registos idnticos
que esto ordenados por ordem alfabtica do nome, para facilitar a listagem do ficheiro.
cabealho
registo base
nome
nmero de telefone
data de aniversrio
corpo
18
A Figura 3.23 apresenta o programa da agenda telefnica, ou seja, a funo main. Para a sua
implementao o programa tem como variveis de entrada, o nome do ficheiro e a opo
pretendida pelo utilizador. Como o ficheiro binrio vai ser utilizado como estrutura de
dados de suporte da agenda telefnica, ento o ficheiro, ou melhor o fluxo binrio
associado com o ficheiro, uma varivel interna do programa.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct
{
unsigned int DIA, MES, ANO;
} TDATA;
#define
#define
NMX
TMX
41
16
typedef struct
{
char NOME[NMX];
char NTELEF[TMX];
TDATA ANIVERSARIO;
} TREG;
void
void
void
void
void
void
void
/* nome do ficheiro */
/* escolha da opo */
/* ponteiro para o fluxo binrio */
LER_NOME_FICHEIRO (NOMEFICH);
/* abrir/criar o ficheiro */
do
{
/* processamento */
/* fechar o ficheiro */
return EXIT_SUCCESS;
}
19
CAPTULO 3 : FICHEIROS EM C
"%1d", OP);
"%*[^\n]");
"%*c");
|| *OP > 4);
/* ler a opo */
/* ler e descartar outros dados */
/* ler e descartar o fim de linha */
Figura 3.24 - Funo para escrever o menu de operaes e ler a opo pretendida.
20
21
CAPTULO 3 : FICHEIROS EM C
*/
*/
*/
*/
*/
22
23
CAPTULO 3 : FICHEIROS EM C
*/
*/
*/
*/
24
/* TMX-1 */
25
CAPTULO 3 : FICHEIROS EM C
26
A insero do registo no ficheiro exige deslocar para baixo os registos do ficheiro a partir
do ndice que foi detectado como local de insero do novo registo. A Figura 3.39
apresenta a funo DESLOCAR_PARA_BAIXO. Tem como parmetros de entrada o
ficheiro, o nmero de registos armazenados e o ndice de insero do registo. Cada registo
lido do ficheiro e escrito de novo no ficheiro mas uma posio mais abaixo. O
deslocamento dos registos feito do fim do ficheiro at ao ndice de insero, como se
exemplifica na parte esquerda da Figura 3.41.
void DESLOCAR_PARA_BAIXO (FILE *FP, unsigned int NR, unsigned int P)
{
TREG REGAUX;
/* registo auxiliar */
unsigned int R;
/* contador do ciclo for */
void LER_REGISTO_FICHEIRO (FILE *, unsigned int, TREG *);
void ESCREVER_REGISTO_FICHEIRO (FILE *, unsigned int, TREG);
for (R = NR; R > P; R--)
{
LER_REGISTO_FICHEIRO (FP, R-1, ®AUX);
ESCREVER_REGISTO_FICHEIRO (FP, R, REGAUX);
}
}
A eliminao do registo no ficheiro exige deslocar para cima os registos do ficheiro a partir
do ndice que foi detectado como local de eliminao do novo registo, a no ser que o
registo seja o ltimo registo do ficheiro. A Figura 3.40 apresenta a funo
DESLOCAR_PARA_ACIMA. Tem como parmetros de entrada o ficheiro, o nmero de
registos armazenados e o ndice de eliminao do registo. Cada registo lido do ficheiro e
escrito de novo no ficheiro mas uma posio mais acima. O deslocamento dos registos
feito do ndice de eliminao at ao fim do ficheiro, como se exemplifica na parte direita da
Figura 3.41.
void DESLOCAR_PARA_CIMA (FILE *FP, unsigned int NR, unsigned int P)
{
TREG REGAUX;
/* registo auxiliar */
unsigned int R;
/* contador do ciclo for */
void LER_REGISTO_FICHEIRO (FILE *, unsigned int, TREG *);
void ESCREVER_REGISTO_FICHEIRO (FILE *, unsigned int, TREG);
for (R = P; R < NR-1; R++)
{
LER_REGISTO_FICHEIRO (FP, R+1, ®AUX);
ESCREVER_REGISTO_FICHEIRO (FP, R, REGAUX);
}
}
27
CAPTULO 3 : FICHEIROS EM C
NR
NR+1
ler
escrever
ler
escrever
ler
escrever
ler
escrever
P
4
3
2
NR-1
1
NR
escrever
ler
escrever
ler
escrever
ler
escrever
ler
1
2
3
4
Figura 3.42 - Funo para ler o nmero de registos armazenado no cabealho do ficheiro.
void ESCREVER_NUMREG (FILE *FP, unsigned int NR)
{
if ( fseek (FP, 0, SEEK_SET) != 0 )
{
perror ("erro no posicionamento do ficheiro - cabealho");
exit (EXIT_FAILURE);
}
if ( fwrite (&NR, sizeof (unsigned int), 1, FP) != 1 )
{
fprintf (stderr, "erro na escrita do cabealho do ficheiro\n");
exit (EXIT_FAILURE);
}
if ( fflush (FP) != 0 )
{
perror ("erro na escrita efectiva no ficheiro");
exit (EXIT_FAILURE);
}
}
28
Captulo 4
RECURSIVIDADE
Sumrio
Neste captulo apresentamos exemplos da implementao de funes recursivas. Uma
funo recursiva, ou recorrente, uma funo que se invoca a si prpria. Tal como a
decomposio hierarquizada, a recursividade permite ao programador decompor um
problema em problemas mais pequenos, que tm a particularidade de serem exactamente
do mesmo tipo do problema original.
O que no significa que os algoritmos recursivos sejam mais eficientes que os algoritmos
iterativos equivalentes. Pelo contrrio, alguns algoritmos recursivos desencadeiam um
nmero arbitrariamente grande de invocaes sucessivas da funo, pelo que, nestes casos
temos uma ineficincia acrescida associada invocao de funes. Por outro lado, alguns
algoritmos recursivos so computacionalmente ineficientes, devido ao facto de as mltiplas
invocaes recursivas repetirem o clculo de valores.
No entanto, os algoritmos recursivos so apropriados para resolver problemas que so por
natureza recursivos. Por vezes, existem problemas que tm solues recursivas que so
simples, concisas, elegantes e para os quais difcil esboar solues iterativas com a
mesma simplicidade e clareza. Mas, como alguns algoritmos recursivos so menos
eficientes que os algoritmos iterativos equivalentes, a verdadeira importncia da
recursividade consiste em resolver esses problemas para os quais no existem solues
iterativas simples.
Vamos analisar alguns exemplos clssicos de algoritmos recursivos, com excepo de
algoritmos de ordenao recursivos, que so algoritmos de ordenao muito eficientes, mas
que sero tratados mais tarde no captulo de Pesquisa e Ordenao.
Devemos ter sempre presente, que a invocao de uma funo, produz um desperdcio de
tempo e de memria devido criao de uma cpia local dos parmetros de entrada que
so passados por valor, bem como na salvaguarda do estado do programa na altura da
invocao, na memria de tipo pilha (stack), para que, o programa possa retomar a
execuo na instruo seguinte invocao da funo, quando a execuo da funo
terminar. Este problema ainda mais relevante no caso de funes recursivas,
principalmente quando existem mltiplas invocaes recursivas. Geralmente, quando
existem solues recursivas e iterativas para o mesmo problema, a soluo recursiva requer
mais tempo de execuo e mais espao de memria devido s invocaes extras da funo.
CAPTULO 4 : RECURSIVIDADE
/* clculo do produtrio */
Mas, se tivermos em conta que n! = n (n1)!, sendo que 0! = 1, ento a funo factorial
pode ser implementada de forma recursiva, tal como se apresenta na Figura 4.3.
unsigned int FACTORIAL (int N)
{
if (N < 0) return 0;
/* invocao anormal (valor devolvido 0) */
if (N == 0) return 1;
/* condio de paragem */
return N * FACTORIAL (N-1);
/* invocao recursiva */
}
A primeira condio de teste evita que a funo entre num processo recursivo infinito, caso
a funo seja invocada para um valor de N negativo. Uma das instrues da funo
factorial recursiva a invocao recursiva, quando a funo se invoca a si prpria. Para
cada nova invocao, o valor do factorial a calcular diminudo de uma unidade, at que
acabar por atingir o valor zero. Nesse caso, a funo no se invoca mais e retorna o valor
1. Assim temos que N ser igual a zero, funciona como condio de paragem da
recursividade. Quando a recursividade termina, ento calculado o valor final da funo,
no retorno das sucessivas invocaes da funo.
A Figura 4.4 representa a anlise grfica da invocao da funo factorial recursiva para o
clculo de 3!. Cada caixa representa o espao de execuo de uma nova invocao da
funo. Se considerarmos cada caixa como um n estamos perante o que se designa por
rvore de recorrncia da funo. Temos que 3! = 3 2!, por sua vez 2! = 2 1!, por sua
vez 1! = 1 0!, e finalmente a invocao recursiva termina com a condio de paragem,
sendo 0! = 1. O valor de 3! calculado no retorno das sucessivas invocaes da funo
recursiva, pelo que, 3! = 1 1 2 3 = 6.
A implementao recursiva no tem qualquer vantagem sobre a implementao repetitiva.
Mas, tem a desvantagem associada invocao sucessiva de funes, que o tempo gasto
na invocao de uma funo e o eventual esgotamento da memria de tipo pilha. Apesar da
implementao recursiva no usar variveis locais, no entanto, gasta mais memria porque
a funo tem de fazer a cpia do parmetro de entrada que passado por valor. Pelo que, a
verso iterativa mais eficiente e portanto, deve ser usada em detrimento da verso
recursiva. O maior factorial que se consegue calcular em aritmtica inteira de 32 bits o 12!
que igual a 479001600.
FACTORIAL(3) = 3 * FACTORIAL(2)
3 * 2
FACTORIAL(2) = 2 * FACTORIAL(1)
2 * 1
FACTORIAL(1) = 1 * FACTORIAL(0)
1 * 1
FACTORIAL(0) = 1
seno( x, N )
x 2 n 1
1n u
2n 1 !
n 0
seno( x,5 )
x
x3
x5
x7
x9
...
3!
5!
7!
9!
Como bem sabido, a funo factorial explode rapidamente, assumindo valores de tal
maneira elevados que s podem ser armazenados em variveis de tipo real, com a
consequente perda de preciso. Pelo que, uma forma de resolver o problema, passa por
eliminar a necessidade do clculo do factorial recorrendo a um processo recorrente. Uma
vez que se trata de uma srie geomtrica, poderamos calcular cada elemento da srie a
partir do elemento anterior e adicionar os elementos calculados. Mas, se aplicarmos de um
modo sistemtico a propriedade distributiva da multiplicao relativamente adio,
obtemos uma expresso, em que o clculo do factorial desaparece.
seno( x, N )
x2
x2
x2
x2
u 1
u 1
u 1
u ...
x u 1
2u3
4
u
5
6
u
7
8
u
9
Esta expresso pode ser calculada de duas maneiras. Podemos calcul-la de baixo para cima
(bottom-up), ou seja, do termo P4 para o termo P1, atravs de um processo iterativo, ou em
alternativa podemos calcular a expresso de cima para baixo (top-down), ou seja, do termo P1
para o termo P4, atravs de um processo recursivo.
Vamos comear por analisar a implementao iterativa. Neste caso vamos calcular de
forma repetitiva a expresso entre parntesis da direita para a esquerda, ou seja, do termo
P4 at ao termo P1, utilizando o passo iterativo dado pela seguinte expresso.
CAPTULO 4 : RECURSIVIDADE
Pi 1
1
x2
u Pi
2 u i u 2 u i 1
Figura 4.5 - Funo iterativa que calcula a expanso em srie de Taylor da funo seno.
Vamos agora analisar a implementao recursiva. Neste caso vamos calcular a expresso
entre parntesis da esquerda para a direita, ou seja, do termo P1 at ao termo P4, sendo que,
cada termo calculado em funo do termo seguinte ainda por calcular. Da a necessidade
do processo ser recursivo. O passo recursivo dado pela seguinte expresso.
Seno( x, N , i )
1
x2
u Seno( x, N , i 1)
2 u i u 2 u i 1
Seno( x, N , N )
1
x2
2 u N u 2 u N 1
Caso o nmero de termos pretendidos na expanso seja apenas um, o valor final do seno
x e no h a necessidade de invocar o clculo recursivo. Caso, o nmero de termos seja
maior do que um, ento o valor final do seno igual a x vezes o valor obtido pelo clculo
recursivo do primeiro elemento, ou seja, seno( x, N ) x u Seno( x, N ,1) .
Pelo que, para implementar o clculo de forma recursiva precisamos de decompor a
soluo em duas funes, tal como se apresenta na Figura 4.6. Uma a funo recursiva
propriamente dita que implementa a expanso em srie de Taylor, e a outra a funo que
calcula o valor final e que invoca, caso seja necessrio, a funo recursiva.
double TAYLOR (double X, int N, int I)
{
/* condio de paragem */
if (I == N) return 1 - X*X / (2*N * (2*N+1));
return 1 - X*X / (2*I * (2*I+1)) * TAYLOR (X, N, I+1);
}
double SENO_TAYLOR_RECURSIVO (double X, int N)
{
if (N == 1) return X;
return X * TAYLOR (X, N, 1);
}
Figura 4.6 - Funo recursiva que calcula a expanso em srie de Taylor da funo seno.
Neste exemplo, a verso recursiva para alm do desperdcio de tempo gasto com a
invocao sucessiva da funo, tem uma implementao mais extensa em termos de
cdigo, uma vez que a soluo tem de ser decomposta em duas funes. Pelo que, a verso
iterativa mais eficiente e simples de programar.
0, se n 0
1, se n 1
Fibonacci( n - 1) Fibonacci( n - 2), se n t 2
FIBONACCI(4)
FIBONACCI(3)
FIB(4) = FIB(3)+FIB(2)
FIB(3) = FIB(2)+FIB(1)
FIBONACCI(3)
FIBONACCI(2)
FIBONACCI(2)
FIBONACCI(1)
FIB(3) = FIB(2)+FIB(1)
FIB(2) = FIB(1)+FIB(0)
FIB(2) = FIB(1)+FIB(0)
FIB(1) = 1
FIBONACCI(2)
FIBONACCI(1)
FIBONACCI(1)
FIBONACCI(0)
FIBONACCI(1)
FIBONACCI(0)
FIB(2) = FIB(1)+FIB(0)
FIB(1) = 1
FIB(1) = 1
FIB(0) = 0
FIB(1) = 1
FIB(0) = 0
FIBONACCI(1)
FIBONACCI(0)
FIB(1) = 1
FIB(0) = 0
Como se pode ver, esta soluo computacionalmente ineficiente, porque para calcular o
Fibonacci de 5, calcula repetidamente alguns valores intermdios. Mais concretamente,
duas vezes o Fibonacci de 3, trs vezes o Fibonacci de 2, cinco vezes o Fibonacci de 1 e
trs vezes o Fibonacci de 0. Por outro lado, devido dupla invocao recursiva o nmero
de operaes explode rapidamente. Como se pode ver na Figura 4.8, o clculo do
Fibonacci de 5 custa 7 adies. Para se calcular o Fibonacci de 6, temos que calcular o
Fibonacci de 5 e o Fibonacci de 4, o que d um total de 12 adies. O Fibonacci de 7 custa
20 adies, o Fibonacci de 8 custa 33 adies. Ou seja, o nmero de adies quase que
duplica quando se incrementa o argumento da funo de uma unidade. Pelo que, o tempo
de execuo desta funo praticamente exponencial.
CAPTULO 4 : RECURSIVIDADE
/* Fibonacci de 0 */
/* Fibonacci de 1 e de 2 */
return 0;
return 1;
/* Fibonacci de 0 */
/* Fibonacci de 1 */
/* Fibonacci de n >= 2 */
return PROXIMO;
}
Esta soluo iterativa a melhor das trs solues. a mais rpida e a que gasta menos
memria. O tempo de execuo desta soluo linear, ou seja, o clculo do Fibonacci de
2N custa aproximadamente o dobro do tempo do clculo do Fibonacci de N. O maior
nmero de Fibonacci que se consegue calcular em aritmtica inteira de 32 bits o Fibonacci
de 47 que igual a 2971215073.
n!
(n - k)! k!
2
3
4
5
1
1
3
6
10
1
4
10
1
5
A funo recursiva para calcular o coeficiente binomial, que se apresenta na Figura 4.12,
invoca-se recursivamente duas vezes e tem como condies de paragem os elementos
terminais, cujo valor 1. A primeira condio de teste evita que a funo entre num
processo recursivo infinito, caso a funo seja invocada para k menor do que n.
unsigned int COMBINACOES (int N, int K)
{
if (K > N) return 0;
/* invocao anormal (valor devolvido 0) */
if ((K == 0) || (K == N)) return 1;
/* condies de paragem */
return COMBINACOES (N-1, K) + COMBINACOES (N-1, K-1);
}
CAPTULO 4 : RECURSIVIDADE
Enquanto que a soluo recursiva calcula 2C(n,k)1 termos, a soluo dinmica calcula
aproximadamente, por defeito, NK termos. Os maiores coeficientes binomiais que se
conseguem calcular em aritmtica inteira de 32 bits so o C(35,16) e o C(35,19), que so
iguais a 4059928950.
10
A Figura 4.14 apresenta a funo de clculo das permutaes. Assume-se que LISTA uma
cadeia de N caracteres terminada com o carcter nulo, de maneira a simplificar a sua escrita
no monitor. Para trocar os caracteres utiliza-se a funo TROCA.
void TROCA (char *CAR_I, char *CAR_J)
{
char TEMP;
TEMP = *CAR_I; *CAR_I = *CAR_J; *CAR_J = TEMP;
}
void PERMUTACOES (char LISTA[], int I, int N)
{
int J;
if (I == N)
/* condio de paragem
printf ("%s\n", LISTA[J]);
/* imprimir a permutao gerada
else for (J = I; J <= N; J++)
/* para todos os caracteres
{
TROCA (&LISTA[I], &LISTA[J]); /* por o carcter direita
PERMUTACOES (LISTA, I+1, N);/* permutar os n-1 caracteres
TROCA (&LISTA[J], &LISTA[I]);/* repor o carcter no stio
}
*/
*/
*/
*/
*/
*/
printf ("Permutaes\n\n");
PERMUTACOES (LISTA, 0, K-1);
return 0;
}
void TROCA (char *CAR_I, char *CAR_J)
...
void PERMUTACOES (char LISTA[], unsigned int I, unsigned int N)
...
11
CAPTULO 4 : RECURSIVIDADE
A Figura 4.17 apresenta a funo e a Figura 4.18 apresenta a aplicao da funo sobre uma
matriz quadrada de 44 e as sucessivas matrizes, equivalentes para efeito do clculo do
determinante, que vo sendo obtidas aps cada invocao recursiva. Quando se atinge a
matriz de um s elemento, a matriz original foi completamente transformada numa matriz
diagonal. Agora o determinante calculado no retorno das sucessivas invocaes da funo
recursiva e igual ao produto dos elementos na diagonal. Logo, o valor do determinante
2.0 -1.0 -1.5 5.0, ou seja, igual a 15.
12
#define NMAX 10
...
double CALC_DETERMINANTE (double MATRIZ[][NMAX], unsigned int N)
{
int COL_AUX, NC, NL, UE = N-1; double ELEMENTO;
if (N == 1) return MATRIZ[0][0];
/* condio de paragem */
else
{
COL_AUX = UE;
/* procurar coluna com ltimo elemento 0 */
while ( (COL_AUX >= 0) && (MATRIZ[UE][COL_AUX] == 0) )
COL_AUX--;
if (COL_AUX >= 0)
/* se existir tal coluna */
{
if (COL_AUX != UE)
/* se no for a ltima coluna */
for (NL = 0; NL < N; NL++)
/* trocar as colunas */
{
ELEMENTO = MATRIZ[NL][UE];
MATRIZ[NL][UE] = MATRIZ[NL][COL_AUX];
MATRIZ[NL][COL_AUX] = -ELEMENTO;
}
/* dividir a coluna N-1 pelo ltimo elemento pondo-o em evidncia */
for (NL = 0; NL < UE; NL++)
MATRIZ[NL][UE] /= MATRIZ[UE][UE];
/* subtrair todas as colunas menos a ltima pela ltima coluna */
/* multiplicada pelo ltimo elemento da coluna a processar */
for (NC = 0; NC < UE; NC++)
for (NL = 0; NL < UE; NL++)
MATRIZ[NL][NC] -= MATRIZ[NL][UE] * MATRIZ[UE][NC];
/* invocao recursiva para a matriz de dimenso N-1 */
return MATRIZ[UE][UE] * CALC_DETERMINANTE (MATRIZ, N-1);
}
else return 0;
}
}
Figura 4.17 - Funo recursiva que calcula o determinante de uma matriz quadrada.
3.0
4.0
2.0
5.0
4.0
2.0
2.0
1.0
1.0
0.0
3.0
5.0
2.0
3.0
2.0
2.0
-2.5 2.5
4.5 -1.5
0.0
0.0
0.0
0.0
0.0
0.0 -1.0
0.0
0.0
0.0
2.0
0.0
=>
=>
0.0
4.0 -0.5
0.5
0.0
0.0
2.0
5.0 0.0
0.0 -1.5
0.0
0.0
0.0
0.0
0.0
0.0 -1.0
0.0
0.0
0.0
2.0
0.0
Figura 4.18 - Execuo da funo recursiva que calcula o determinante de uma matriz quadrada.
13
CAPTULO 4 : RECURSIVIDADE
Torre A
Torre B
Torre C
Torre A
Torre B
Torre C
Torre A
Torre B
Torre C
Torre A
Torre B
Torre C
A soluo para este problema trivial caso o nmero de discos seja um. Nesse caso basta
mud-lo da Torre A para a Torre B. No entanto se tivermos mais do que um disco a
soluo no bvia e at bastante dispendiosa medida que o nmero de discos
aumenta. A soluo que se apresenta na Figura 4.20 a soluo recursiva que resolve o
puzzle para o caso de N discos colocados na Torre A. Consiste em diminuir a
complexidade do problema at situao em que temos apenas um disco e para o qual
conhecemos a soluo.
14
Assim para o caso apresentado na Figura 4.19 em que temos quatro discos na Torre A, a
soluo passa por movimentar os trs discos de cima da Torre A para a Torre C, depois
mudar o quarto disco da Torre A para a Torre B e finalmente mudar os trs discos que se
encontram na Torre C para a Torre B.
nome: Torres de Hanoi (N, TORRE_A, TORRE_B, TORRE_C)
Procedimento
begin
if (N = 1)
then Mover o disco da TORRE_A para a TORRE_B
else begin
Torres de Hanoi (N-1, TORRE_A, TORRE_C, TORRE_B);
Torres de Hanoi (1, TORRE_A, TORRE_B, TORRE_C);
Torres de Hanoi (N-1, TORRE_C, TORRE_B, TORRE_A);
end
end
Obviamente que esta soluo funciona se for aplicada recursivamente, porque, resolver o
problema de mudar os trs primeiros discos da Torre A para a Torre C, resolve-se da
mesma forma, s que agora a torre de chegada a Torre C, a torre auxiliar a Torre B e o
nmero de discos a mudar menos um do que no problema inicial. A soluo passa por
mudar os dois discos de cima da Torre A para a Torre B, depois mudar o terceiro disco da
Torre A para a Torre C e finalmente mudar os dois discos que se encontram na Torre B
para a Torre C. Assim estamos perante o problema de mudar dois discos da Torre A para a
Torre B, o que implica mudar o primeiro disco da Torre A para a Torre C, o segundo disco
da Torre A para a Torre B e finalmente o primeiro disco da Torre C para a Torre B.
Aps termos mudado os trs primeiros discos da Torre A para a Torre C e termos atingido
a situao apresentada na terceira linha da Figura 4.19, ento para mudar os trs discos que
se encontram na Torre C para a Torre B aplica-se de novo a soluo recursiva, mas agora a
torre de partida a Torre C, a torre de chegada a Torre B e a torre auxiliar a Torre A.
A Figura 4.21 e a Figura 4.22 apresentam o programa que simula as Torres de Hani. Para
implementar as trs torres utilizam-se trs agregados, sendo os discos representados por
nmeros inteiros de 1 a N. O programa principal l do teclado o nmero de discos, valida o
seu valor, constri a situao inicial, imprime-a no monitor e depois invoca a funo para
simular a mudana dos discos.
A funo INICIALIZAR coloca os N discos na Torre A e nenhum disco na Torre B e na
Torre C. A funo MUDAR_DISCOS implementa o algoritmo recursivo e aps cada
mudana de um disco invoca a funo IMPRIMIR. A funo IMPRIMIR vai imprimir no
monitor o estado das torres, no incio e aps cada mudana de um disco. A funo no tem
parmetros, porque utiliza variveis globais. Uma vez que o algoritmo recursivo, em cada
nova invocao as torres vo mudando de posio, pelo que, a nica forma de poder
imprimir o estado das torres A, B e C, usando variveis globais para as torres e para os
contadores do nmero de discos armazenados em cada torre. Para declarar variveis
globais, elas tm de ser declaradas antes da funo main com o qualificativo static.
15
CAPTULO 4 : RECURSIVIDADE
#include <stdio.h>
#define D_MAX 10
/* agregados para as torres */
static int TORREA[D_MAX], TORREB[D_MAX], TORREC[D_MAX];
static int NDA, NDB, NDC;
/* nmero de discos de cada torre */
void INICIALIZAR (int, int [], int *, int [], int *, int [], int *);
void IMPRIMIR (void);
void MUDAR_DISCOS (int, int [], int *, int [], int *, int [], int *);
int main (void)
{
int NDISCOS;
do
{
printf ("Numero de discos = "); scanf ("%d", &NDISCOS);
} while ( (NDISCOS <= 0) || (NDISCOS > DISCOS_MAX) );
INICIALIZAR (NDISCOS, TORREA, &NDA, TORREB, &NDB, TORREC, &NDC);
printf ("---------------------------------\n");
printf ("|
Torres de Hanoi
|\n");
printf ("|
Numero de discos = %2d
|\n", NDISCOS);
printf ("---------------------------------\n");
printf ("| TORRE A
TORRE B
TORRE C |\n");
printf ("---------------------------------\n");
IMPRIMIR ();
MUDAR_DISCOS (NDISCOS, TORREA, &NDA, TORREB, &NDB, TORREC, &NDC);
return 0;
}
void INICIALIZAR (int ND, int TA[], int *NDA, int TB[], int *NDB,\
int TC[], int *NDC);
{
int I;
for (I = 0; I < D_MAX; I++)
{
TA[I] = 0; TB[I] = 0; TC[I] = 0;
/* limpar os agregados */
}
for (I = 0; I < ND; I++) TA[I] = ND - I;
/* discos na Torre A */
*NDA = ND;
*NDB = 0;
*NDC = 0;
}
void IMPRIMIR (void)
{
int I, CMAX = NDA;
if (NDB > CMAX) CMAX = NDB;
if (NDC > CMAX) CMAX = NDC;
for (I = CMAX; I > 0; I--)
{
if (NDA >= I) printf ("%10d", TORREA[I-1]);
else printf ("%10c", ' ');
if (NDB >= I) printf ("%10d", TORREB[I-1]);
else printf ("%10c", ' ');
if (NDC >= I) printf ("%10d", TORREC[I-1]);
else printf ("%10c", ' ');
printf ("\n");
}
printf ("---------------------------------\n"); scanf ("%*c");
}
16
void MUDAR_DISCOS (int ND, int TA[], int *NDA, int TB[], int *NDB,\
int TC[], int *NDC);
{
if (ND == 1)
/* condio de paragem */
{
/* mudar o disco da Torre A para a Torre B */
(*NDB)++; TB[*NDB-1] = TA[*NDA-1]; (*NDA)--;
IMPRIMIR ();
}
else
{
/* mudar os N-1 discos de cima da Torre A para a Torre C */
MUDAR_DISCOS (ND-1, TA, NDA, TC, NDC, TB, NDB);
/* mudar o ltimo disco da Torre A para a Torre B */
(*NDB)++; TB[*NDB-1] = TA[*NDA-1]; (*NDA)--;
IMPRIMIR ();
/* mudar os N-1 discos da Torre C para a Torre B */
MUDAR_DISCOS (ND-1, TC, NDC, TB, NDB, TA, NDA);
}
}
17
CAPTULO 4 : RECURSIVIDADE
4.10 Exerccios
1. Pretende-se escrever um programa que imprima no monitor, uma tabela em que se
compara os valores da funo co-seno calculada pela expanso em srie de Taylor para 5,
10 e 15 termos e pela funo matemtica cos. Os valores inicial e final da tabela, bem como
o nmero de elementos da tabela so lidos do teclado. O termo geral da expanso em srie
de Taylor da funo co-seno dado pela seguinte expresso esquerda, sendo apresentada
direita a sua expanso para os primeiros cinco termos da srie.
N 1
coseno( x, N )
2n
1n u x
2n
n 0
coseno( x,5 )
1
x2
x4
x6
x8
...
2!
4!
6!
8!
18
m, se n 0
Ackermann(m, n)
n 1, se m 0
Ackermann(m - 1,1), se n 0
Ackermann(m - 1, Ackermann(m, n - 1)) , com n ! 0 e m ! 0
Captulo 5
MEMRIAS
Sumrio
Neste captulo comeamos por introduzir o paradigma da programao modular e a
construo de mdulos na linguagem C, apresentando as suas caractersticas e a
necessidade de criao de mdulos genricos. A ttulo de exemplo, desenvolvemos um
exemplo de um mdulo abstracto com mltipla instanciao para operaes sobre nmeros
complexos.
Seguidamente mostramos a organizao da Memria de Acesso Aleatrio (RAM), da
Memria Fila (Queue/FIFO), da Memria Pilha (Stack/LIFO) e da Memria Associativa
(CAM) e os seus ficheiros de interface.
Depois de descrevermos as particularidades e limitaes dos tipos de implementao de
memrias, fazemos uma abordagem s estruturas de dados que servem de suporte
implementao esttica e semiesttica da Memria de Acesso Aleatrio, e implementao
esttica, semiesttica e dinmica da Memria Fila, da Memria Pilha e da Memria
Associativa.
Finalmente, apresentamos as funes da biblioteca de execuo ANSI stdlib que permitem a
atribuio e libertao de memria, durante a execuo do programa.
CAPTULO 5 : MEMRIAS
CAPTULO 5 : MEMRIAS
N_ELEMENTOS
100
/* nmero de elementos */
...
TIPO_ELEMENTO;
#endif
CAPTULO 5 : MEMRIAS
O mdulo tambm fornece uma funo que permite libertar a memria atribuda para um
nmero complexo, quando ele no mais preciso, ou seja, uma funo para apagar o
nmero complexo. Para evitar divises por zero, preciso uma funo que verifica se um
nmero complexo o complexo nulo. Existem ainda funes para extrair a parte real e a
parte imaginria do nmero complexo.
/* Ficheiro de interface do mdulo de nmeros complexos */
/* Nome: complexo.h */
#ifndef _COMPLEXO
#define _COMPLEXO
typedef
struct
complexo
*PtComplexo;
CAPTULO 5 : MEMRIAS
struct complexo
{
double Real; double Imag;
};
/* Funo que cria e inicializa um complexo na forma R+jI */
PtComplexo Inicializar_Complexo (double R, double I)
{
PtComplexo PC = (PtComplexo) malloc (sizeof (struct complexo));
PC->Real = R;
PC->Imag = I;
return PC;
}
/* Funo que l do teclado um complexo na forma R+jI */
void Ler_Complexo (PtComplexo PC)
{
if (PC == NULL) return;
printf ("Parte Real "); scanf ("%lf", &PC->Real);
printf ("Parte Imaginria "); scanf ("%lf", &PC->Imag);
}
/* Funo que escreve no monitor um complexo na forma R+jI */
void Escrever_Complexo (PtComplexo PC)
{
if (PC != NULL) printf ("%f +j %f\n", PC->Real, PC->Imag);
}
/* Funo que soma dois nmeros complexos */
PtComplexo Somar_Complexos (PtComplexo PC1, PtComplexo PC2)
{
PtComplexo PC = (PtComplexo) malloc (sizeof (struct complexo));
PC->Real = PC1->Real + PC2->Real;
PC->Imag = PC1->Imag + PC2->Imag;
return PC;
}
/* Funo que subtrai dois nmeros complexos */
PtComplexo Subtrair_Complexos (PtComplexo PC1, PtComplexo PC2)
{
PtComplexo PC = (PtComplexo) malloc (sizeof (struct complexo));
PC->Real = PC1->Real - PC2->Real;
PC->Imag = PC1->Imag - PC2->Imag;
return PC;
}
10
11
CAPTULO 5 : MEMRIAS
12
OK
NULL_PTR
NULL_SIZE
NO_MEM
RAM_EMPTY
RAM_FULL
RAM_EXISTS
NO_RAM
0
1
2
3
4
5
6
7
/*
/*
/*
/*
/*
/*
/*
/*
13
CAPTULO 5 : MEMRIAS
14
OK
NULL_PTR
NULL_SIZE
NO_MEM
FIFO_EMPTY
FIFO_FULL
FIFO_EXISTS
NO_FIFO
0
1
2
3
4
5
6
7
/*
/*
/*
/*
/*
/*
/*
/*
15
CAPTULO 5 : MEMRIAS
OK
NULL_PTR
NULL_SIZE
NO_MEM
STACK_EMPTY
STACK_FULL
STACK_EXISTS
NO_STACK
0
1
2
3
4
5
6
7
/*
/*
/*
/*
/*
/*
/*
/*
sz
bytes.
Valores
de
16
Como a memria tem que estar sempre ordenada pela chave de pesquisa, as operaes de
colocao e de remoo de elementos podem implicar deslocamentos dos elementos da
memria para permitir a colocao do elemento na memria ou para compensar a
eliminao do elemento da memria. O facto da memria estar sempre ordenada, permite
tambm implementar de forma eficiente operaes de leitura de informao, que so
normalmente muito importantes neste tipo de memria. Essas operaes so a leitura do
primeiro elemento da memria que tem uma determinada chave, que vamos designar por
CAM_Read_First, e a leitura de elementos sucessivos com a mesma chave, que vamos
designar por CAM_Read_Next.
/******************** Interface do Mdulo CAM ********************/
#ifndef _CAM
#define _CAM
/******************** Definio de Constantes ********************/
#define
#define
#define
#define
#define
#define
#define
#define
#define
#define
OK
NULL_PTR
NULL_SIZE
NO_MEM
CAM_EMPTY
CAM_FULL
CAM_EXISTS
NO_CAM
NO_KEY
NO_FUNC
0
1
2
3
4
5
6
7
8
9
/*
/*
/*
/*
/*
/*
/*
/*
/*
/*
17
CAPTULO 5 : MEMRIAS
18
RAM[1]
RAM[2]
...
RAM[I]
...
RAM[N-1]
ltimo elemento
RAM_Read (I)
RAM_Write (I)
RAM esttica
RAM[1]
RAM[2]
RAM semiesttica
...
RAM[I]
...
RAM[N-1]
19
CAPTULO 5 : MEMRIAS
Para implementar uma memria de acesso aleatrio dinmica seria preciso recorrer a uma
lista ligada de elementos. S que depois a memria s poderia ser acedida sequencialmente
a partir do elemento inicial e perder-se-ia o acesso indexado, pelo que, a memria deixaria
ento de ter as caractersticas de acesso aleatrio. Portanto, no existe implementao
dinmica da memria de acesso aleatrio.
FIFO[0]
FIFO[1]
FIFO[2]
...
FIFO[I]
...
FIFO[N-1]
FILA esttica
cabea
da fila
cauda
da fila
FIFO_Out
FIFO_In
20
cabea da fila
FIFO[0]
FIFO[1]
FIFO[2]
cauda da fila
...
FIFO[I]
...
FIFO[N-1]
FILA semiesttica
FIFO_Out
FIFO_In
A implementao dinmica, que se apresenta na Figura 5.17, baseada numa lista ligada de
elementos. Uma lista ligada uma estrutura constituda por elementos, a que vamos chamar
ns, ligados atravs de ponteiros. Cada n da lista ligada constitudo por dois ponteiros.
Uma para o elemento que armazena a informao e outro para o n seguinte da lista.
Repare que o ltimo n da lista aponta para NULL, para servir de indicador de finalizao
da fila. A memria para os ns e para os elementos da lista atribuda, quando um
elemento colocado na fila e libertada quando um elemento retirado da fila. Os
indicadores de cabea e cauda da fila so ponteiros. A cabea da fila aponta sempre para o
elemento mais antigo que se encontra na fila e que o primeiro a ser retirado. A cauda da
fila aponta sempre para o elemento mais recente que se encontra na fila e frente do qual
um novo elemento colocado. Quando so ambos ponteiros nulos, sinal que a fila est
vazia. Uma fila dinmica nunca est cheia. Quando muito, pode no existir memria para
continuar a acrescentar-lhe mais elementos.
cabea
da fila
PtSeg
PtSeg
PtSeg
PtSeg
PtEle
PtEle
PtEle
PtEle
Elemento
Elemento
Elemento
Elemento
FIFO_Out
FILA dinmica
cauda
da fila
FIFO_In
No caso das implementaes semiesttica e dinmica possvel criar uma fila abstracta.
Para isso, necessrio providenciar a funo de criao da fila FIFO_Create que
concretiza o tipo de elementos, atravs da especificao do seu tamanho em bytes, e a
funo de destruio da fila FIFO_Destroy que alm de libertar toda a memria dinmica
atribuda, coloca o indicador de tamanho dos elementos de novo a zero e recoloca os
indicadores de cabea e cauda da fila nas condies iniciais.
21
CAPTULO 5 : MEMRIAS
STACK_Push
STACK_Pop
topo da
pilha
Ponteiro
Elemento STACK[N-1]
Elemento
...
Ponteiro
Elemento
STACK[I]
Ponteiro
Elemento
...
Ponteiro
Elemento
STACK[2]
Ponteiro
Elemento
STACK[1]
Ponteiro
Elemento
STACK[0]
Ponteiro
PILHA semiesttica
topo da
pilha
STACK_Push
Elemento
STACK_Pop
Elemento
Elemento
A implementao dinmica de uma pilha, que se apresenta na Figura 5.19, baseada numa
lista ligada de elementos. Mas, enquanto que na fila cada n da lista ligada aponta para o n
seguinte, na pilha cada n aponta para o n anterior, sendo que o primeiro n da lista
aponta para NULL, para servir de indicador de finalizao da pilha. A memria para os ns
e para os elementos da lista atribuda, quando um elemento colocado na pilha e
libertada quando um elemento retirado da pilha. O indicador de topo da pilha um
ponteiro. O topo da pilha aponta sempre para o elemento mais recente que se encontra na
pilha, que o primeiro elemento a ser retirado e frente do qual um novo elemento
colocado. Quando o topo da pilha um ponteiro nulo, sinal que a pilha est vazia. Uma
pilha dinmica nunca est cheia. Quando muito, pode no existir memria para continuar a
acrescentar-lhe mais elementos.
22
PILHA dinmica
topo da
pilha
STACK_Push
PtEle
Elemento
STACK_Pop
PtAnt
PtEle
Elemento
PtAnt
PtEle
Elemento
PtAnt
No caso das implementaes semiesttica e dinmica possvel criar uma pilha abstracta.
Para isso, necessrio providenciar a funo de criao da pilha STACK_Create que
concretiza o tipo de elementos, atravs da especificao do seu tamanho em bytes, e a
funo de destruio da pilha STACK_Destroy que alm de libertar a memria dinmica
atribuda, coloca o indicador de tamanho dos elementos de novo a zero e recoloca o
indicador de topo da pilha na condio inicial.
23
CAPTULO 5 : MEMRIAS
CAM[0]
CAM[1]
CAM[2]
CAM[3]
CHAVE 1
CHAVE 2
CHAVE 4
CHAVE 6
pesquisa
binria
...
CAM[N-1]
CAM_In (CHAVE)
CAM_Out (CHAVE)
ltimo
elemento
CAM esttica
ltimo elemento
CAM[0]
CAM[1]
CAM[2]
CAM[3]
...
CAM[N-1]
CHAVE 1
CHAVE 2
CHAVE 4
CHAVE 6
binria
CAM_In (CHAVE)
CAM_Out (CHAVE)
CAM semiesttica
cabea
da CAM
PtSeg
PtSeg
PtAnt
PtAnt
PtEle
PtEle
24
PtSeg
PtAnt
PtEle
PtSeg
PtAnt
PtEle
CHAVE 1
CHAVE 2
CHAVE 4
CHAVE 6
Elemento
Elemento
Elemento
Elemento
pesquisa
sequencial
CAM_In (CHAVE)
CAM_Out (CHAVE)
Para optimizarmos a pesquisa pode-se optar por outra estrutura de dados que implemente
uma pesquisa ainda mais eficiente que a pesquisa binria, mas, que seja ainda uma estrutura
de dados em que os elementos possam ser colocados e retirados sem custos de
deslocamentos de elementos. Tal estrutura de dados, que se apresenta na Figura 5.22,
uma tabela de disperso (hashing table ).
CAM_In (CHAVE)
CAM_Out (CHAVE)
CAM[0] Ponteiro
PtSeg
PtEle
PtSeg
PtEle
CAM[1] Ponteiro
.
.
.
.
.
.
CAM[N-2] Ponteiro
CAM[N-1] Ponteiro
CHAVE 1
CHAVE 41
Elemento
Elemento
2 pesquisa sequencial
PtSeg
PtEle
CHAVE 2
CAM semiesttica/dinmica
Elemento
25
CAPTULO 5 : MEMRIAS
A situao de coliso acontece quando dadas duas chaves diferentes, a funo de disperso
calcula a mesma a posio de colocao. O que compromete a implementao da memria,
uma vez que, no se pode colocar dois elementos na mesma posio da tabela. Portanto,
tem que se resolver o problema das colises. Uma maneira passa por reduzir o seu nmero,
utilizando uma funo de disperso que assegure uma boa disperso.
Mas, independentemente da funo usada vo existir sempre colises. Portanto, tem de ser
criada uma estrutura de dados que permita colocar mais do que um elemento na mesma
posio da tabela. Uma soluo possvel consiste em criar uma estrutura semiesttica em
que cada elemento do agregado um ponteiro que aponta para uma lista ligada de
elementos. Temos assim uma tabela de listas ligadas.
Esta implementao semiesttica, sob o ponto de vista da estrutura de suporte, mas
dinmica sob o ponto de vista da colocao e remoo dos elementos. Permite resolver o
problema das colises e permite ainda a existncia na memria associativa de elementos
distintos com a mesma chave. Elementos com a mesma chave vo ficar na mesma posio
da tabela e podem ser colocados na lista ligada por ordem cronolgica da sua colocao na
memria associativa, se por exemplo, a lista for implementada como uma fila.
Para pesquisar a existncia de um elemento nesta implementao da memria associativa,
seja para colocar um novo elemento, seja para retirar um elemento j existente, aplica-se a
funo de disperso para a chave do elemento e obtm-se a posio da tabela onde o
elemento deve estar colocado. Depois utiliza-se a pesquisa sequencial para analisar a lista
ligada de elementos at detectar a posio de colocao ou remoo do elemento com a
chave pretendida. Como o nmero de elementos existente na lista ligada pequena, a
pesquisa sequencial aceitvel. Por outro lado, nesta implementao a leitura sucessiva de
elementos com a mesma chave aplicada facilmente.
No entanto, a grande limitao da tabela de disperso tem a ver com o facto da estrutura de
suporte ser um agregado, que uma estrutura de dados esttica. Se a memria associativa
necessitar de crescer esta soluo no a mais adequada. Portanto, necessria uma
estrutura de dados dinmica que permita colocar e retirar elementos de forma eficiente,
como na lista biligada, mas que suporte tambm uma pesquisa eficiente, como o caso da
pesquisa binria. Tal estrutura de dados a rvore binria de pesquisa. A Figura 5.23
apresenta esta implementao dinmica hierrquica.
Uma rvore uma estrutura de dados constituda por uma coleco de ns. Esta coleco
pode ser nula, ou constituda por um n inicial, que se designa por raiz da rvore, e zero ou
mais subrvores. Portanto, uma estrutura de dados com uma organizao recursiva, em
que cada n tambm uma rvore. Uma rvore diz-se binria se cada n no tiver mais do
que duas subrvores, a subrvore da esquerda e a subrvore da direita. Numa rvore binria
de pesquisa, um elemento colocado na rvore de maneira que, os elementos da sua
subrvore da esquerda tm uma chave menor do que a sua chave e os elementos da sua
subrvore da direita tm uma chave maior do que a sua chave.
Para implementar uma rvore binria, cada n da rvore constitudo por trs ponteiros.
Um para o elemento que armazena a informao e os outros dois para os ns seguintes da
rvore, ou seja, para as subrvores da esquerda e da direita. Quando o n seguinte no
existe, ento o respectivo ponteiro aponta para NULL, para servir de indicador de
inexistncia da subrvore.
PtEle
PtEsq
PtDir
CHAVE 4
PtEsq
PtEle
Elemento
PtDir
CHAVE 2
PtEsq
PtEle
PtDir
Elemento
PtEsq
PtEle
PtDir
CAM_In (CHAVE)
CAM_Out (CHAVE)
raiz
da CAM
pesquisa binria
CHAVE 6
PtEsq
PtEle
PtDir
Elemento
PtEsq
PtEle
CHAVE 1
CHAVE 3
CHAVE 8
Elemento
Elemento
Elemento
PtDir
26
27
CAPTULO 5 : MEMRIAS
28
A soluo correcta passa por criar o agregado dinamicamente, recorrendo funo calloc,
assim que se saiba o nmero de nmeros contido no ficheiro. Para tal declara-se o ponteiro
PARINT que vai receber o endereo devolvido pela funo, que o endereo inicial do
bloco de memria atribudo para o agregado. Devido dualidade ponteiro agregado, o
ponteiro PARINT utilizado para aceder aos elementos do agregado, tal como se fosse um
agregado. O programa antes de terminar, deve libertar a memria atribuda dinamicamente,
usando para o efeito a funo free.
#include <stdio.h>
#include <stdlib.h>
int main (int argc, char *argv[])
{
FILE *FP; int *PARINT, N, I;
if ( argc < 2 )
/* o nmero de argumentos suficiente? */
{
fprintf (stderr, "Uso: %s nome do ficheiro\n", argv[0]);
exit (EXIT_FAILURE);
}
/* abertura do ficheiro de entrada cujo nome argv[1] */
if ( (FP = fopen (argv[1], "r")) == NULL )
{
fprintf (stderr, "No foi possvel abrir o ficheiro %s\n"\
, argv[1]);
exit (EXIT_FAILURE);
}
/* leitura do nmero de nmeros inteiros armazenado no ficheiro */
fscanf (FP, "%d", &N);
/* atribuio de memria para um agregado de N inteiros */
if ( (PARINT = (int *) calloc (N, sizeof (int))) == NULL )
{
fprintf (stderr, "No foi possvel atribuir o agregado\n");
fclose (FP);
/* fecho do ficheiro de entrada */
exit (EXIT_FAILURE);
}
for (I = 0; I < N; I++)
/* leitura do ficheiro para o agregado */
fscanf (FP, "%d", &PARINT[I]);
/* fecho do ficheiro de entrada */
fclose (FP);
/* processamento do agregado */
...
Captulo 6
PESQUISA E ORDENAO
Sumrio
Uma das tarefas mais habituais na programao a pesquisa de informao, o que exige o
desenvolvimento de algoritmos eficientes de pesquisa. Neste captulo introduzimos os
mtodos de pesquisa sequencial, de pesquisa binria e de pesquisa por tabela.
Apresentamos vrios algoritmos que utilizam o mtodo de pesquisa sequencial e o
algoritmo de pesquisa binria, nas verses iterativa e recursiva.
Por outro lado a pesquisa fortemente condicionada pela organizao da informao, e por
conseguinte, a ordenao uma tarefa ainda mais frequente. Apresentamos as classes mais
simples dos algoritmos de ordenao, que so, a ordenao por seleco, a ordenao por
troca e a ordenao por insero. Para cada uma delas expomos os algoritmos mais
simples, que apesar de no serem os mais eficientes, so contudo suficientemente rpidos
para pequenos agregados e apropriados para mostrar as caractersticas dos princpios de
ordenao. Explicamos os algoritmos de ordenao Sequencial ( Sequential ), Seleco
( Selection ), Bolha ( Bubble ), Concha ( Shell ), Crivo ( Shaker ) e Insero ( Insertion ),
mostramos a sua execuo com um pequeno agregado e fazemos a comparao do
desempenho de cada um deles. Apresentamos tambm o algoritmo de ordenao Fuso de
Listas ( Merge List ) que muito verstil, porque pode ser aplicado, quer na ordenao de
agregados, quer na ordenao de ficheiros. Apresentamos ainda os algoritmos de
ordenao recursivos Fuso ( Merge ) e Rpido ( Quick ), que so muito eficientes.
Finalmente, explicamos como generalizar os algoritmos de ordenao e como contabilizar
as instrues de comparao e de atribuies de elementos, de modo a poder fazer testes
de desempenho dos algoritmos.
6.1 Pesquisa
Uma das tarefas mais comuns no dia a dia a pesquisa de informao. Mas, a pesquisa
depende muito da forma como a informao est organizada. Se a informao estiver
completamente desordenada no temos outra alternativa que no seja analisar toda a
informao por ordem, seja ela do princpio para o fim ou vice-versa, at encontrar o que
pretendemos. Este processo de pesquisa normalmente lento. Imagine, por exemplo,
procurar a nota de um teste numa pauta no ordenada, no caso de uma disciplina com
algumas centenas de alunos. Mas, se a informao estiver ordenada por uma ordem, seja ela
crescente ou decrescente no caso de informao numrica, ascendente ou descendente no
caso de informao textual, como por exemplo a mesma pauta listada por ordem do
nmero mecanogrfico, ento possvel fazer uma procura mais ou menos selectiva,
partindo a informao a procurar em intervalos sucessivos cada vez menores, evitando
assim analisar informao irrelevante e acelerar o processo de pesquisa. E, se a informao
alm de estar ordenada por ordem, estiver ainda composta com entradas especficas, como
por exemplo, uma pauta de uma disciplina com alunos de vrios cursos, que se apresenta
dividida em pautas por cursos, cada uma delas ordenada por nmero mecanogrfico, ento
possvel fazer uma procura dirigida pauta do respectivo curso e depois fazer uma
procura em funo do nmero mecanogrfico, optimizando ainda mais o processo de
pesquisa. Portanto, o mtodo de pesquisa inevitavelmente dependente da forma como a
informao est organizada e apresentada. Quanto mais ordenada estiver a informao,
mais eficiente poder ser o mtodo de pesquisa.
Tal como no dia a dia, a pesquisa de informao tambm uma tarefa trivial em
programao. Pesquisar um agregado procura da localizao de um determinado valor, ou
de uma determinada caracterstica acerca dos seus valores, uma tarefa muito frequente e
simples. Mas, computacionalmente dispendiosa porque, o agregado pode ser constitudo
por centenas ou mesmo milhares de elementos. Pelo que, exige o desenvolvimento de
algoritmos eficientes.
A maneira mais simples de pesquisar um agregado a pesquisa sequencial
( sequencial search ), tambm chamada de pesquisa linear. Consiste em analisar todos os
elementos do agregado de maneira metdica. A pesquisa comea no elemento inicial do
agregado e avana elemento a elemento at encontrar o valor procurado, ou at atingir o
elemento final do agregado. Este mtodo de pesquisa normalmente demorado,
dependente do tamanho do agregado, mas, no depende do arranjo interno dos elementos
no agregado. Independentemente da forma como a informao est armazenada no
agregado, o valor procurado ser encontrado, caso exista no agregado.
No entanto, se tivermos informao priori sobre os elementos do agregado possvel
acelerar a pesquisa. Se por exemplo, os elementos estiverem ordenados por ordem
crescente ou decrescente, ento possvel fazer uma pesquisa binria ( binary search ),
tambm chamada de pesquisa logartmica. Este tipo de pesquisa comea por seleccionar
o elemento central do agregado e compara-o com o valor procurado. Se o elemento
escolhido for menor ento podemos excluir a primeira metade do agregado e analisamos
apenas a segunda metade. Caso contrrio, ento podemos excluir a segunda metade do
agregado e analisamos apenas a primeira metade. Em cada passo da pesquisa, o nmero de
elementos do agregado que tm de ser analisados reduzido a metade, pelo que, este
mtodo de pesquisa mais eficiente. O processo repetido at que o elemento
seleccionado seja o valor procurado, ou at que o nmero de elementos que tm de ser
analisados seja reduzido a zero, o que significa, que o valor procurado no existe no
agregado.
Se os elementos do agregado, em vez de estarem colocados em posies sucessivas do
agregado, estiverem colocados em posies predeterminadas do agregado, posies essas
que so determinadas por uma funo de disperso ( hashing function ), ento possvel fazer
uma pesquisa por tabela ( table search ), tambm chamada de pesquisa por disperso
( hashing ). Uma funo de disperso uma funo aritmtica que determina a posio de
colocao do elemento no agregado, tendo em considerao o valor do elemento, ou no
caso de um elemento estruturado, usando um campo do elemento como chave de
colocao no agregado. No entanto, normalmente uma funo de disperso produz
situaes de coliso. A situao de coliso acontece quando dados dois valores diferentes, a
funo de disperso calcula a mesma a posio de colocao. Portanto, o agregado tem de
permitir colocar mais do que um elemento numa mesma posio. Uma soluo possvel
consiste em criar uma estrutura semiesttica em que cada elemento do agregado um
ponteiro que aponta para uma lista ligada de elementos. Temos assim um agregado de listas
ligadas. Este modelo de estrutura de dados, que se designa por uma tabela de disperso
com encadeamento de elementos, pesquisado em dois tempos. Para procurar um
elemento, primeiro usada a funo de disperso para, dado o valor do elemento desejado,
calcular a sua posio de colocao no agregado e aceder directamente a essa posio.
Depois utiliza-se a pesquisa sequencial para analisar a lista ligada de elementos at detectar
o elemento com o valor pretendido. Este mtodo de pesquisa ser abordado mais tarde a
propsito da implementao de memrias associativas.
Para os algoritmos apresentados neste captulo vamos considerar que a estrutura de dados a
pesquisar ou ordenar, um agregado de elementos inteiros de nome seq, com capacidade
para armazenar NMAX elementos, tendo no entanto apenas nelem elementos teis.
Tendo em considerao este agregado de elementos inteiros, vamos analisar vrios
algoritmos que utilizam a pesquisa sequencial e o algoritmo de pesquisa binria nas suas
verses iterativa e recursiva. Vamos considerar que as funes s so invocadas para
agregados que contm de facto elementos com informao, ou seja, nelem maior do que
zero, pelo que, os algoritmos podem ser simplificados, uma vez que no tm que se
preocupar com esta situao anmala.
Figura 6.4 - Funo que determina o elemento do agregado com o primeiro valor que serve.
Figura 6.5 - Funo que determina o elemento do agregado com o melhor valor que serve.
Figura 6.6 - Funo que determina o elemento do agregado com o pior valor que serve.
Posio
Inicial de
Pesquisa
20
0
3 330 25 22 24 15 32 42
1
Primeiro
Valor<=40
Menor
Valor
Valor
55
10
11
12
13
14
Pior
Valor<=40
Maior
Valor
16
17
18
19
Melhor
Valor<=40
/* pesquisa falhada */
Figura 6.8 - Funo de pesquisa binria que procura um valor no agregado (verso iterativa).
posio mnima seria o elemento de ndice 10. Agora que estamos a analisar a primeira
metade do agregado, calcula-se a nova posio mdia, que o elemento de ndice 4. Como
o valor procurado maior do que valor armazenado no elemento mdio, que 26, ento
isso significa que ele se encontra na segunda metade do intervalo em anlise, pelo que, a
posio mnima passa para o elemento direita da posio mdia, ou seja, para o elemento
de ndice 5. Agora, a nova posio mdia passa a ser o elemento de ndice 6. Como o valor
procurado ainda maior do que valor armazenado no elemento mdio, que 30, ento isso
significa que ele se encontra na segunda metade do intervalo em anlise, pelo que, a posio
mnima passa para o elemento direita da posio mdia, ou seja, para o elemento de
ndice 7. A nova posio mdia passa agora a ser o elemento de ndice 7, cujo valor o
valor procurado, pelo que, a pesquisa termina com sucesso e devolve o ndice 7. Para
obtermos este resultado, foi preciso analisar quatro elementos do agregado enquanto que a
pesquisa sequencial necessitaria de analisar oito elementos.
MIN
MED
MIN
MED
10
11
12
13
14
15
16
17
18
19
MAX
MIN MED
MAX
10
11
12
13
14
15
16
17
18
19
MAX
10
11
12
13
14
15
16
17
18
19
MIN MAX
10
11
12
13
14
15
16
17
18
19
MED
MAX MIN
MED
10
11
12
13
14
15
16
17
18
19
Se por exemplo, o valor procurado fosse 40, que maior do que 34, ento a nova posio
mnima passaria para o elemento direita da posio mdia, ou seja, para o elemento de
ndice 8, exactamente igual ao valor da posio mxima. Consequentemente, a nova
posio mdia passaria a ser o elemento de ndice 8, cujo valor 42, pelo que, a posio
mxima passaria para o elemento esquerda da posio mdia, ou seja, para o elemento de
ndice 7 e portanto, as posies mnima e mxima trocavam de posio parando o ciclo de
pesquisa. Como o valor armazenado no elemento cujo ndice a posio mdia final, que
o elemento de ndice 8, diferente do valor procurado, ento a pesquisa terminaria sem
sucesso e devolveria o ndice 1 como sinal de pesquisa falhada. Neste caso a funo
analisava cinco elementos do agregado enquanto que a pesquisa sequencial necessitaria de
analisar os vinte elementos do agregado para chegar ao mesmo resultado.
Esta estratgia de pesquisa tambm pode ser implementada de forma recursiva, sendo que
cada nova pesquisa analisa uma das metades do agregado anteriormente analisado at que o
valor seja encontrado ou at no existirem mais elementos para analisar. A verso recursiva
apresentada na Figura 6.10. Compare-a com a verso iterativa que foi apresentada na
Figura 6.8. A funo comea por testar a situao de paragem no caso da pesquisa sem
sucesso, o que acontece quando as posies inicial e final trocam de posio. Nesse caso, a
funo devolve o ndice 1 como sinal de pesquisa falhada. Caso tal no se verifique,
calcula o ndice do elemento central do agregado em anlise. Se o elemento for o valor
procurado, estamos perante a situao de paragem com sucesso e a funo devolve o ndice
do elemento central do agregado. Caso contrrio, em funo da comparao do valor
procurado com o valor do elemento central, a funo invoca-se recursivamente de forma
alternativa para a primeira metade ou para a segunda metade do agregado.
int ProcuraBinariaRecursiva (int seq[], unsigned int inicio,\
unsigned int fim, int valor)
{
unsigned int medio;
/* condio de paragem no caso de pesquisa sem sucesso */
if (inicio > fim) return -1;
medio = (inicio + fim) / 2;
Figura 6.10 - Funo de pesquisa binria que procura um valor no agregado (verso recursiva).
10
6.2 Ordenao
A ordenao o processo de organizar um conjunto de objectos segundo uma determinada
ordem. Como j foi dito anteriormente, se a informao estiver ordenada possvel utilizar
algoritmos de pesquisa mais eficientes, como por exemplo a pesquisa binria ou a pesquisa
por tabela. Pelo que, a ordenao uma tarefa muito importante no processamento de
dados e feita para facilitar a pesquisa.
Os algoritmos de ordenao so classificados em dois tipos. A ordenao de informao
armazenada em agregados designa-se por ordenao interna. Enquanto que, a ordenao
de informao armazenada em ficheiros, designa-se por ordenao externa. Neste
captulo vamos apresentar alguns algoritmos de ordenao interna, se bem que alguns
tambm possam ser utilizados para ordenao externa.
Existem algoritmos de ordenao muito eficientes para ordenar agregados de grandes
dimenses, mas so normalmente complexos. Pelo contrrio, os algoritmos de ordenao
que vamos apresentar so simples, mas apropriados para mostrar as caractersticas dos
princpios de ordenao. So normalmente pouco eficientes, mas so suficientemente
rpidos para agregados de pequenas dimenses. Os algoritmos de ordenao que ordenam
os elementos do agregado, no prprio agregado, fazendo para o efeito um rearranjo interno
dos seus elementos, enquadram-se numa das trs seguintes categorias: ordenao por
seleco; ordenao por troca; e ordenao por insero.
11
12
330
25
15
42
32
55
145
330
209
25
42
15
32
55
145
I
2
NC=9 NT=3
depois
da
segunda
passagem
I
2
330
NC=8 NT=4
depois
da
terceira
passagem
NC=7 NT=4
depois
da
quarta
passagem
NC=6 NT=4
depois
da
quinta
passagem
NC=5 NT=3
depois
da
sexta
passagem
NC=4 NT=2
depois
da
stima
passagem
NC=3 NT=2
depois
da
oitava
passagem
NC=2 NT=2
depois
da
nona
passagem
NC=1 NT=1
TOTAL
NC = 45
NT = 25
15
209
42
25
32
15
55
145
J
330
209
42
32
25
55
145
J
I
2
15
25
330
209
42
32
55
145
J
I
2
15
25
32
330
209
42
55
145
J
I
2
15
25
32
42
330
209
55
145
J
I
2
15
25
32
42
55
330
209
145
J
I
2
15
25
32
42
55
145
330
209
I
2
15
25
32
42
55
145
209
330
J
15
25
32
42
55
145
209
330
13
/* trocar os elementos */
}
}
209
depois
da
primeira
passagem
330
25
15
330
depois
da
segunda
passagem
I
2
25
15
25
15
depois
da
terceira
passagem
NC=7 NT=1
MIN
15
25
depois
da
stima
passagem
NC=3 NT=1
depois
da
oitava
passagem
NC=2 NT=1
depois
da
nona
passagem
NC=1 NT=0
TOTAL
NC = 45
NT = 7
55
145
42
209
32
55
145
MIN
depois
da
sexta
passagem
NC=4 NT=1
32
NC=8 NT=1
2
MIN
depois
da
quinta
passagem
NC=5 NT=1
42
NC=9 NT=1
depois
da
quarta
passagem
NC=6 NT=0
14
42
209
32
330
55
145
J
42
209
32
330
55
145
J
I
2
15
25
42
MIN
209
15
25
32
330
55
MIN
209
42
330
55
15
25
32
MIN
42
209
330
55
15
15
25
25
32
32
42
42
55
55
145
J
I
8
145
J
145
J
I
2
32
MIN
330
209
145
MIN
145
209
330
I
2
15
15
25
25
32
32
42
42
55
55
145
145
209
330
MIN
209
330
15
do
{
ntrocas = 0;
/* inicializar o contador de trocas */
for (indi = nelem-1; indi >= indinicial; indi--)
if (seq[indi-1] > seq[indi])
{
Swap (&seq[indi], &seq[indi-1]);
/* trocar os elementos */
ntrocas++;
}
indinicial++;
/* actualizar o limite superior de ordenao */
} while (ntrocas && indinicial < nelem);
}
209
depois
da
primeira
passagem
209
depois
da
quarta
passagem
NC=6 NT=2
depois
da
quinta
passagem
NC=5 NT=2
depois
da
sexta
passagem
NC=4 NT=2
depois
da
stima
passagem
NC=3 NT=2
depois
da
oitava
passagem
NC=2 NT=2
depois
da
nona
passagem
NC=1 NT=0
TOTAL
NC = 45
NT = 25
15
42
32
55
145
330
25
15
42
32
55
145
I
INICIAL
209
330
25
15
42
32
55
145
NC=8 NT=5
depois
da
terceira
passagem
NC=7 NT=4
25
INICIAL
NC=9 NT=6
depois
da
segunda
passagem
330
16
I
INICIAL
15
209
330
25
32
42
55
145
I
INICIAL
15
25
209
330
32
42
55
145
I
INICIAL
15
25
32
209
330
42
55
145
I
INICIAL
15
25
32
42
209
330
55
145
I
INICIAL
15
25
32
42
55
209
330
145
I
INICIAL
15
25
32
42
55
145
209
330
I
INICIAL
15
25
32
42
55
145
209
330
I
INICIAL
15
25
32
42
55
145
209
330
17
Para um agregado com N elementos, este algoritmo faz no melhor caso apenas uma
passagem, fazendo N1 comparaes e faz no pior caso N1 passagens, fazendo (N2N)/2
comparaes. Em mdia faz aproximadamente N2/3 comparaes. O nmero de trocas se
bem que dependente do grau de desordenao dos elementos, no pior caso pode atingir as
(N2N)/2 trocas. Portanto, este algoritmo tem uma eficincia de comparao de ordem
O(N2), uma eficincia de trocas de ordem O(N2), pelo que, pertence classe O(N2).
Como este algoritmo tem a capacidade de aps cada passagem determinar se o agregado
est ou no ordenado, ento indicado para ordenar agregados que estejam parcialmente
desordenados. No entanto, caso o agregado esteja muito desordenado, ele o pior dos
algoritmos de ordenao, uma vez que faz muitas trocas. De maneira a diminuir o nmero
de trocas, Donald L. Shell criou uma variante deste algoritmo, que em vez de comparar
elementos adjacentes, compara elementos distanciados de um incremento que vai sendo
progressivamente diminudo, at que nas ltimas passagens compara elementos adjacentes.
A Figura 6.18 apresenta o algoritmo de Ordenao Concha ( Shell Sort ) para a ordenao
crescente do agregado. Existe a varivel auxiliar incremento que representa a distncia de
comparao e cujo valor inicial metade do comprimento do agregado. Para os ltimos
elementos do agregado, mais concretamente para os nelemincremento ltimos elementos,
compara-se o elemento de ndice indi, com o elemento distanciado incremento elementos,
ou seja com o elemento de ndice indiincremento, e caso o valor seja menor trocam-se os
elementos. Deste modo os elementos de menor valor vo sendo deslocados em direco
parte inicial do agregado. Em cada passagem, contabiliza-se o nmero de trocas efectuadas,
e quando uma passagem no tiver efectuado qualquer troca, isso sinal de que os
elementos que esto separados da distncia de comparao, j esto ordenados e a distncia
de comparao reduzida, sendo dividida ao meio. O algoritmo repetido at que a ltima
distncia de comparao usada seja igual a um. Quando numa passagem com distncia de
comparao unitria, no se efectuar qualquer troca de elementos, ento sinal que o
agregado est ordenado. Ao contrrio dos outros algoritmos de ordenao, at esta
passagem final no h garantia que algum elemento do agregado j esteja ordenado. A srie
de incrementos utilizada a que foi proposta pelo Shell.
void Shell_Sort (int seq[], unsigned int nelem)
{
unsigned int indi, ntrocas, incremento;
/* 1 verso */
18
330
25
15
42
32
55
145
15
42
209
330
25
55
145
INCREMENTO=5
2
32
NC=5 NT=3
depois
da
segunda
passagem
I
INCREMENTO=5
2
32
15
42
NC=5 NT=0
209
330
25
55
depois
da
terceira
passagem
NC=8 NT=4
INCREMENTO=2
depois
da
quarta
passagem
NC=8 NT=1
INCREMENTO=2
depois
da
quinta
passagem
NC=8 NT=0
INCREMENTO=2
depois
da
sexta
passagem
NC=9 NT=3
INCREMENTO=1
depois
da
stima
passagem
NC=9 NT=0
INCREMENTO=1
TOTAL
NC = 52
NT = 11
15
32
42
25
55
145
330
15
15
25
42
32
55
145
330
15
25
42
32
55
145
330
25
32
42
55
145
209
330
I
15
25
32
42
55
145
209
209
I
209
I
209
I
145
330
I
15
25
32
42
55
145
209
330
Normalmente, o algoritmo Concha mais eficiente do que o algoritmo Bolha, uma vez
que, as primeiras passagens analisam apenas parte dos elementos do agregado e portanto,
fazem poucas trocas, mas trocam elementos que esto muito fora de stio. Quando as
ltimas passagens, que analisam os elementos adjacentes, so efectuadas, ento o agregado
j se encontra parcialmente ordenado, pelo que, so necessrias poucas passagens e poucas
trocas para acabar a ordenao. No entanto, para passar de incremento em incremento esta
verso do algoritmo exige uma passagem sem trocas.
19
Existe uma implementao alternativa deste algoritmo, que se apresenta na Figura 6.20, que
no necessita de fazer esta passagem extra, para determinar a passagem ao incremento
seguinte. Utiliza uma tcnica de insero de elementos em vez de troca de elementos. Pega
no agregado constitudo pelos elementos que esto distncia de comparao e para cada
um desses elementos faz a sua insero na posio correcta, de maneira que este agregado
fique ordenado. Depois passa distncia de comparao seguinte. Quando a distncia de
comparao unitria, ento estamos perante o algoritmo de Insero, que se apresenta na
Figura 6.23. Para ordenar o mesmo agregado, esta verso faz um total de 29 comparaes e
de 55 instrues de atribuio, ou seja, cpias de elementos, que correspondem a
aproximadamente 18 trocas. Se por um lado esta verso mais eficiente nas comparaes,
menos 23 o que d uma eficincia na ordem dos 50%. Por outro lado, ela menos eficiente
nas trocas, com aproximadamente mais 7 trocas para trocar os 11 elementos do agregado
que esto fora do stio, ou seja, existe uma ineficincia na ordem dos 70%. Esta ineficincia
caracterstica da tcnica de insero.
void Shell_Sort (int seq[], unsigned int nelem)
{
unsigned int indi, indj, incremento; int temp;
/* 2 verso */
20
21
25
15
209
330
55
145
25
15
42
32
55
145
I
INICIAL
2
209
25
FINAL
15
42
32
55
145
330
UT
INICIAL
TOTAL
NC = 39
NT = 25
depois
da
terceira
passagem
NC=7 NT=4
depois
da
sexta
passagem
NC=4 NT=0
32
UT
depois
da
quinta
passagem
NC=5 NT=2
FINAL
NC=8 NT=7
depois
da
quarta
passagem
NC=6 NT=6
42
INICIAL
NC=9 NT=6
depois
da
segunda
passagem
330
209
FINAL
25
15
42
32
55
145
330
UT
I
INICIAL
25
15
FINAL
42
32
55
145
209
330
UT
INICIAL
2
15
25
FINAL
32
42
55
145
209
330
209
330
209
330
UT
I
INICIAL
15
25
32
FINAL
42
55
145
UT
I
FINAL
2
15
INICIAL
25
32
42
55
145
22
}
}
Para comparar este algoritmo com os anteriores, temos que ter em considerao, que para
ordenar cada elemento, so precisas duas instrues de atribuio. Uma para copiar o
elemento para uma varivel temporria e outra para o inserir na posio definitiva. E o
deslocamento de cada elemento custa ainda uma instruo de atribuio. Pelo que, o
nmero de instrues de atribuio NA = ND + 2 * (N1). Como cada instruo de
atribuio equivalente, a um tero das instrues de atribuio que so necessrias para
efectuar a troca de dois elementos do agregado, nos algoritmos anteriores baseados em
trocas, ento temos que NT = ( ND + 2 * (N1) )/3.
A Figura 6.24 apresenta a execuo do algoritmo para um agregado com 10 elementos. O
algoritmo faz um total de 34 comparaes e de 43 instrues de atribuio para efectuar os
23
25
15
42
32
55
145
25
15
42
32
55
145
I
209
330
PI
NC=1 ND=0
ordenar
o
terceiro
elemento
330
PI
209
330
25
15
42
32
55
145
NC=3 ND=2
25
209
330
15
42
32
55
145
ordenar
o
quarto
elemento
PI
42
32
55
145
32
55
145
NC=4 ND=3
ordenar
o
quinto
elemento
25
209
330
15
15
25
209 330
42
PI
15
25
209 330
42
32
55
145
NC=3 ND=2
15
25
42
330
32
55
145
ordenar
o
sexto
elemento
PI
15
25
42
209
330
32
55
145
NC=6 ND=5
15
25
42
209
330
32
55
145
ordenar
o
stimo
elemento
NC=4 ND=3
ordenar
o
oitavo
elemento
NC=7 ND=6
ordenar
o
nono
elemento
NC=3 ND=2
ordenar
o
dcimo
elemento
209
PI
15
25
42
209
330
32
55
145
15
25
32
42
209
330
55
145
PI
15
25
32
42
209
330
55
145
15
25
32
42
209
330
55
145
PI
15
25
32
42
209
330
55
145
15
25
32
42
55
209
330
145
PI
15
25
32
42
55
209
330
145
NC=3 ND=2
15
25
32
42
55
145
209
330
TOTAL
NC = 34
NA = 43
15
25
32
42
55
145
209
330
24
Para um agregado com N elementos, este algoritmo faz no pior caso (N2N)/2
comparaes e deslocamentos, mas, em termos mdios faz aproximadamente N 2/4
comparaes e deslocamentos, pelo que, pertence classe O(N 2). Se o agregado estiver
parcialmente desordenado este o algoritmo de ordenao mais indicado.
Para a escolha da verso do algoritmo Concha, devemos optar pela primeira verso se a
funo de comparao dos elementos do agregado for simples, por exemplo uma
comparao numrica e se os elementos do agregado forem estruturas pesadas, ou seja,
com muitos bytes, com vista a minorar o nmero de trocas. Se pelo contrrio, a funo de
comparao dos elementos do agregado for pesada, por exemplo uma comparao
alfanumrica com muitos caracteres e se os elementos do agregado forem tipos simples,
estruturas pequenas, ou ponteiros, ento devemos optar pela segunda verso com vista a
minorar o nmero de comparaes. Estas consideraes aplicam-se igualmente escolha
de qualquer algoritmo de ordenao.
Para a anlise dos algoritmos devemos ter em conta que o pior caso na ordenao de um
agregado acontece quando o agregado est invertido em relao ordenao pretendida,
enquanto que o melhor caso acontece quando o agregado est ordenado de acordo com a
ordenao pretendida. Para avaliar o caso mdio deve-se utilizar agregados gerados
aleatoriamente e fazer uma estimativa mdia.
25
Este algoritmo muito verstil e eficiente, uma vez que, pode ser generalizado para fazer a
fuso de mais do que dois agregados. No caso da existncia de elementos repetidos nos
agregados de entrada, podemos considerar solues alternativas, em que a ocorrncia de
elementos repetidos eliminada ou mantida. Tambm pode ser utilizado como algoritmo
de ordenao externa para fundir dois ou mais ficheiros. Para executar a fuso de dois
agregados j ordenados, com N/2 elementos cada, este algoritmo faz no melhor caso N/2
comparaes e no pior caso N1 comparaes.
26
J
32
42
209
330
15
25
55
145
25
55
145
25
55
145
C
K
depois
de copiar
o primeiro
elemento
do
agregado B
depois
de copiar
o primeiro
elemento
do
agregado A
depois
de copiar
mais dois
elementos
do
agregado B
depois
de copiar
mais dois
elementos
do
agregado A
depois
de esgotar
os
elementos
do
agregado B
depois
de copiar
os
restantes
elementos
do
agregado A
I
A
J
32
42
209
330
15
K
I
A
32
J
42
209
330
15
K
I
A
32
42
209
15
25
330
15
25
55
145
K
I
32
42
209
330
15
25
32
42
15
25
55
145
K
I
32
42
209
330
15
25
32
42
2
55
15
25
55
145
145
K
32
42
209
330
15
25
32
42
2
55
15
145
25
209
55
145
330
27
/* condio de paragem */
/* partio do agregado */
/* elemento da 1 parte */
/* elemento da 2 parte */
28
29
209
209
209
209
209
209
25
15
TOTAL
NC = 21
NA = 68
330
330
330
330
25
25
25
25
15
15
15
15
42
42
42
42
330
25
32
42
209
15
42
25
42
32
55
55
55
145
145
145
32
330
145
32
15
55
32
32
330
330
209
32
55
32
145
32
55
55
145
145
d
209 330
Se o nmero de elementos do agregado for menor do que dois, ento o agregado est
automaticamente ordenado. Se existirem apenas dois elementos, eles so ordenados atravs
de uma simples comparao e eventual troca dos dois elementos. Quando o agregado tem
mais do que dois elementos, escolhido o elemento de ndice mdio para servir de pivot.
Os elementos das extremidades e o pivot so trocados para ficarem por ordem crescente.
Se s existirem trs elementos, ento esta operao constituda por trs comparaes e
eventuais trs trocas, ordena o agregado. Seno, h que assegurar que todos os elementos
esquerda do pivot so menores do que ele e que todos os elementos sua direita so
maiores do que ele. Os elementos que se encontrem fora do stio so trocados da parte
direita para a parte esquerda e vice-versa, ou ento so trocados com o pivot provocando
que ele se desloque mais para a esquerda ou mais para a direita do que a posio mdia
inicialmente calculada. Pelo que, quando o processamento se separao terminar no h
garantia que o agregado est partido em duas metades com um nmero semelhante de
elementos. Aps este processamento, o algoritmo invocado recursivamente para o
agregado constitudo pelos elementos esquerda do pivot e para o agregado constitudo
30
/* trocar os elementos */
31
330
25
15
42
32
55
145
209
330
25
15
42
32
55
145
145
330
209
aps processamento
42
55
25
15
I
42
55
25
32
15
I
8
32
209
330
209
330
aps processamento
15
2
I
15
25
32
55
I
25
42
R
42
55
aps processamento
2
I
8
R
TOTAL
NC = 28
NT = 19
25
15
R
15
25
15
25
15
25
32
42
55
15
25
32
42
55
145
32
Com o objectivo de diminuir o nmero de trocas, existe uma verso optimizada, que se
apresenta na Figura 6.31 e que frequentemente apresentada na literatura. A optimizao
consiste em evitar as trocas que envolvem o pivot e o deslocam da posio inicial. Aps a
escolha do pivot ele escondido na penltima posio do agregado. Os elementos so
analisados e trocados at serem todos comparados com o pivot. Quando a anlise do
agregado terminar, ou seja, quando indi for maior ou igual do que indj, o pivot colocado
no stio, por troca com o elemento que est na posio mais esquerda do agregado que
maior do que ele, ou seja, com o elemento que est na posio indi. Esta verso do
algoritmo faz um total de 30 comparaes e de 17 trocas de elementos. Quando
comparado com a primeira verso temos mais 2 comparaes, mas menos 2 trocas.
void Quick_Sort (int seq[], unsigned int inicio, unsigned int fim)
{
unsigned int medio, nelem = fim-inicio+1, indi, indj;
if (nelem <= 1) return;
/* o agregado tem no mximo 1 elemento */
if (nelem == 2)
/* o agregado s tem 2 elementos */
{
if (seq[inicio] > seq[fim]) Swap (&seq[inicio], &seq[fim]);
return;
}
medio = (inicio + fim) / 2;
else break;
}
/* recuperar o pivot para a posio mdia do agregado */
medio = indi;
Swap (&seq[medio], &seq[fim-1]);
/* invocao recursiva para a parte esquerda do agregado */
Quick_Sort (seq, inicio, medio-1);
/* invocao recursiva para a parte direita do agregado */
Quick_Sort (seq, medio+1, fim);
}
Para executar a ordenao de um agregado com N elementos, este algoritmo faz no pior
caso (N2N)/2 comparaes. No entanto, no caso mdio faz aproximadamente
1.4(N+1) log2 N comparaes, pelo que, considera-se que pertence classe O(N log2 N).
33
34
35
A partir desta definio podem-se declarar variveis do tipo PtFComp e atribuir-lhe uma
qualquer funo, desde que seja uma funo inteira com dois parmetros de entrada do
tipo TElem. A Figura 6.34 apresenta um exemplo da passagem de um parmetro de
entrada deste tipo para uma funo de ordenao. A Figura 6.35 apresenta a utilizao de
uma varivel deste tipo para ser colocada a apontar para diferentes funes ao longo da
execuo do programa, de forma a parametrizar a ordenao de um agregado. Tal como
nos agregados, o nome de uma funo tambm um ponteiro para a funo, pelo que, a
instruo de atribuio fcomp = CompNome; coloca o ponteiro para funo fcomp a
apontar para a funo CompNome.
A funo de troca de elementos do agregado que foi apresentada na Figura 6.11, tem de ser
alterada de maneira a poder trocar dois elementos do tipo TElem. A Figura 6.33 apresenta
a nova verso que vamos designar por SwapElementos.
void SwapElementos (TElem *x, TElem *y)
{
TElem temp;
temp = *x; *x = *y; *y = temp;
}
Figura 6.33 - Funo para trocar dois elementos de um agregado de elementos do tipo TElem.
A Figura 6.35 apresenta a utilizao sucessiva deste algoritmo de ordenao, para ordenar
um agregado de elementos do tipo TElem. Antes de cada invocao, atribudo ao
ponteiro fcomp a funo de comparao necessria para obter a ordenao desejada e
depois a funo de ordenao invocada indicando tambm o tipo de ordenao
pretendido. Para aumentar a legibilidade do programa, definimos as constantes simblicas
CRESCENTE e DECRESCENTE.
Como exerccio de treino escreva a funo Display, cuja funcionalidade imprimir no
monitor a informao relativa a um agregado de estruturas do tipo TElem, com o
prottipo Display (TElem seq[], unsigned int nelem); e teste o programa.
36
#include <stdio.h>
#include <stdlib.h>
#include "elemento.h"
#define CRESCENTE 1
#define DECRESCENTE -1
void SwapElementos (TElem *, TElem *);
void Sequential_Sort (TElem [], unsigned int, PtFComp, int);
void Display (TElem [], unsigned int);
int main (void)
{
TElem pintores[] = {
{
{
{
{
{
1,
2,
3,
4,
5,
};
int nelem = sizeof (pintores) / sizeof (pintores[0]);
/* ponteiro para a funo de comparao inicializado a NULL */
PtFComp fcomp = NULL;
fcomp = CompNome;
/* ordenao alfabtica ascendente */
Sequential_Sort (pintores, nelem, fcomp, CRESCENTE);
printf ("Ordenao Alfabtica Ascendente\n");
Display (pintores, nelem);
fcomp = CompData;
/* ordenao cronolgica decrescente */
Sequential_Sort (pintores, nelem, fcomp, DECRESCENTE);
printf ("Ordenao Cronolgica Decrescente\n");
Display (pintores, nelem);
fcomp = CompNRegisto;
/* ordenao crescente por registo */
Sequential_Sort (pintores, nelem, fcomp, CRESCENTE);
printf ("Ordenao Numrica Crescente\n");
Display (pintores, nelem);
return EXIT_SUCCESS;
}
...
37
funes tm o parmetro de entrada modo de tipo inteiro, que indica o modo de actuao
sobre a varivel contadora.
De maneira a aumentar a legibilidade das funes aconselhvel utilizar constantes
simblicas que representam o modo de actuao sobre a varivel contadora. Temos as trs
constantes simblicas seguintes: REP para reportar o valor da varivel contadora; INIC
para inicializar a varivel contadora; e NORM para realizar a operao da funo e para
incrementar o valor da varivel contadora.
Para calcular o nmero de instrues de atribuio utiliza-se uma varivel de durao
permanente na funo SwapCount, tal como se mostra na Figura 6.36. No modo NORM a
funo troca os dois elementos e incrementa o valor da varivel contadora de trs unidades,
para contabilizar as trs instrues de atribuio.
unsigned int SwapCount (TElem *x, TElem *y, int modo)
{
static unsigned int cont;
/* varivel contadora */
TElem temp;
if (modo == REP) return cont;
else if (modo == INIC) cont = 0;
else if (modo == NORM)
{
temp = *x; *x = *y; *y = temp;
/* efectuar a troca */
cont += 3; /* contagem das 3 instrues de atribuio */
}
return 0;
}
Figura 6.36 - Funo para trocar elementos do agregado com contabilizao de atribuies.
Figura 6.37 - Funo para comparar elementos do agregado com contabilizao de comparaes.
38
Como exerccio de treino altere o programa apresentado na Figura 6.35 de modo a utilizar
estas novas verses das funes SwapCount, CCount e Sequential_Sort. Acrescente ainda
ao programa a impresso no monitor do nmero de comparaes e do nmero de trocas,
que so um tero das instrues de atribuio, depois de cada ordenao.
6.7 Exerccios
1. Pretende-se escrever uma funo de ordenao Fuso de Listas que contemple a
situao da existncia de elementos repetidos nos agregados de entrada, e que nessa
situao copie apenas um dos elementos repetidos para o agregado de sada.
Captulo 7
FILAS E PILHAS
Sumrio
Este captulo dedicado s estruturas de dados lineares que so as filas e as pilhas.
Apresentamos as implementaes esttica e semiesttica baseadas em agregados e a
implementao dinmica baseada em listas ligadas. Mostramos exemplos de aplicao que
utilizam filas e pilhas como elementos de armazenamento e que tiram partido da sua
organizao interna para a resoluo de problemas. Finalmente apresentamos uma
implementao abstracta, dinmica e com capacidade de mltipla instanciao.
7.1 Introduo
As filas e as pilhas so estruturas de dados lineares que tm implementaes em tudo
semelhantes, sendo que diferem apenas na prioridade da retirada de informao. Enquanto
que uma fila implementa a poltica do primeiro a chegar primeiro a sair ( first in first out ), a
pilha implementa a poltica do ltimo a chegar primeiro a sair ( last in first out ).
As filas so muito usadas nos sistemas operativos para funcionarem como reas de
armazenamento de informao que deve ser processada por ordem de chegada, como por
exemplo, o atendimento de pedidos de utilizao de uma impressora de rede que recebe
ficheiros para imprimir, enviados por diferentes computadores instalados na rede. Para
sincronizar a interaco entre processos concorrentes que executam a diferentes
velocidades de processamento. So tambm utilizadas para simular modelos que descrevem
o comportamento de situaes reais de atendimento de pedidos que so processados por
ordem de chegada, como por exemplo, simular o atendimento de uma fila de espera num
qualquer servio do dia a dia.
As pilhas so usadas para gerir algoritmos em que existem processos que invocam
subprocessos do mesmo tipo, como o caso dos algoritmos recursivos. Para processar
estruturas imbricadas, ou seja, estruturas que contm outras estruturas do mesmo tipo
dentro delas, como por exemplo, expresses aritmticas que so compostas por
subexpresses aritmticas do mesmo tipo. Assim uma pilha pode ser usada para verificar o
balanceamento dos parnteses numa expresso aritmtica, que pode conter vrios nveis de
parnteses, que podem ser de tipos diferentes como parnteses curvos, rectos e chavetas.
Podem ser usadas para fazer a avaliao de expresses, que contm expresses internas que
tm de ser avaliadas previamente, antes do clculo da expresso final. Por exemplo, para
calcular uma expresso em notao polaca, que utiliza os smbolos das operaes depois
dos operadores. Para fazer a anlise sintctica durante a compilao de um programa, onde
existem ciclos repetitivos dentro de ciclos repetitivos, blocos dentro de blocos e portanto,
existem estruturas imbricadas que tm de estar balanceadas e correctamente imbricadas.
Vamos apresentar as implementaes esttica, semiesttica e dinmica de filas e de pilhas,
considerando que estamos perante estruturas de dados concretas, cujo tipo dos elementos
de armazenamento concretizado pelo utilizador atravs de um ficheiro de interface, que
vamos designar por elemento.h.
Este ficheiro de interface, que se apresenta na Figura 7.1, define a constante que
parametriza a dimenso da estrutura de dados de suporte, necessrio apenas no caso das
implementaes esttica e semiesttica, bem como o tipo de dados do elemento
constituinte da memria. No caso da implementao dinmica, este ficheiro precisa apenas
de definir o tipo de dados do elemento constituinte da memria. Assim o utilizador do
mdulo, pode concretiz-lo para uma estrutura de dados que corresponda s suas
necessidades, sem ter a necessidade de reprogramar o ficheiro de implementao do
mdulo. Em relao criao de um mdulo abstracto, esta soluo exige a recompilao
do mdulo, sempre que este ficheiro modificado.
N_ELEMENTOS
100
...
TElem;
#endif
7.2 Filas
Uma memria fila ( queue/FIFO ) uma memria em que s possvel processar a
informao pela ordem de chegada, da que, tambm seja apelidada de memria do
primeiro a chegar primeiro a sair. Numa memria fila, o posicionamento para a colocao
de um novo elemento na fila, que vamos designar por Fifo_In, a cauda da fila (fifo tail ),
e o posicionamento para a remoo de um elemento da fila, que vamos designar por
Fifo_Out, a cabea da fila (fifo head ).
/* Definio de Constantes */
#define
#define
#define
#define
OK
NULL_PTR
FIFO_EMPTY
FIFO_FULL
0
1
4
5
/*
/*
/*
/*
A Figura 7.3 mostra o estado inicial da fila. Por uma questo de implementao, vamos
considerar que a cauda da fila indica sempre a primeira posio livre para a prxima
operao de colocao de um elemento na fila, pelo que, inicialmente aponta para a posio
0 do agregado, enquanto que a cabea da fila indica sempre a posio da prxima operao
de remoo de um elemento da fila, pelo que, inicialmente aponta para a posio N do
agregado, ou seja, a posio depois do fim do agregado, como indicao de fila vazia. A
Figura 7.3 mostra tambm a colocao do primeiro elemento na fila e o estado aps a
operao. A cabea da fila vai ficar a apontar para o elemento acabado de colocar, que a
posio 0 do agregado, e, fica nesta posio enquanto este elemento no for retirado da fila.
A cauda da fila deslocada para a posio 1 do agregado, que a primeira posio livre
para colocar o prximo elemento.
FIFO[0]
...
FIFO[N-1]
cauda
da fila
cabea
da fila
estado inicial
FIFO[0]
...
FIFO[N-1]
cabea
da fila
cauda
da fila
Sempre que se coloca um elemento na fila, a cauda da fila deslocada para o elemento
seguinte da fila. Existem duas situaes distintas. Na primeira situao, que se apresenta na
Figura 7.4, a posio seguinte da fila est ainda dentro da dimenso do agregado, pelo que,
a cauda continua atrs da cabea.
FIFO[0]
...
FIFO[N-1]
cabea
da fila
FIFO[0]
cauda
da fila
colocao de um elemento
...
FIFO[N-1]
cabea
da fila
cauda
da fila
Na segunda situao, que se apresenta na Figura 7.5, a posio seguinte da fila est fora da
dimenso do agregado, pelo que, a cauda da fila passa para o incio do agregado e fica
frente da cabea. O que parece uma situao anmala quando comparada com o
funcionamento de uma fila de espera no dia a dia, em que a cabea est sempre frente da
cauda. Mas, preciso ter em considerao que estamos perante uma fila circular. Para
conseguir este efeito circular da cauda da fila utiliza-se o operador mdulo, para calcular
sempre um valor que est entre 0 e N_ELEMENTOS1. Em alternativa utilizao
circular do agregado, a fila poderia ser implementada de forma linear, o que implicaria
deslocar todos os elementos da fila, para o incio do agregado, sempre que se retirasse um
elemento da fila. Mas, tal implementao seria muito ineficiente para filas grandes.
FIFO[0]
...
FIFO[N-1]
colocao de um elemento
FIFO[0]
cabea
da fila
cauda
da fila
...
FIFO[N-1]
cauda
da fila
cabea
da fila
A Figura 7.6 apresenta a situao de colocao do ltimo elemento na fila. Nesta situao a
cauda da fila fica a apontar para o mesmo elemento que a cabea da fila, o que significa que
a fila ficou cheia. Enquanto este estado durar, no possvel colocar mais elementos na fila.
FIFO[0]
...
FIFO[N-1]
cauda
da fila
cabea
da fila
FIFO[0]
...
FIFO[N-1]
cabea da fila
FILA
CHEIA
cauda da fila
Sempre que um elemento retirado da fila, o indicador de cabea da fila deslocado para o
elemento seguinte. Como j foi referido, esta implementao no a forma habitual de
funcionamento de um fila, onde sempre que o elemento da cabea da fila sai da fila, toda a
fila deslocada para a frente. Existem duas situaes distintas quando se retira um
elemento da fila.
Na primeira situao, que se apresenta na Figura 7.7, a posio seguinte da fila est ainda
dentro da dimenso do agregado, pelo que, a cabea ainda no deu a volta ao agregado.
Neste caso ainda est atrs da cauda, porque a cauda j excedeu o fim do agregado.
FIFO[0]
...
FIFO[N-1]
cauda
da fila
FIFO[0]
cabea
da fila
remoo de um elemento
...
FIFO[N-1]
cauda
da fila
cabea
da fila
Na segunda situao, que se apresenta na Figura 7.8, a posio seguinte da fila est fora da
dimenso do agregado, pelo que, a cabea passa para o incio do agregado e neste caso fica
de novo frente da cauda. Para conseguir este efeito circular da cabea da fila utiliza-se o
operador mdulo, para calcular sempre um valor que est entre 0 e N_ELEMENTOS1.
FIFO[0]
...
FIFO[N-1]
remoo de um elemento
cauda
da fila
FIFO[0]
cabea
da fila
...
FIFO[N-1]
cabea
da fila
cauda
da fila
A Figura 7.9 apresenta a situao de remoo do ltimo elemento da fila. Nesta situao a
cabea da fila fica a apontar para o mesmo elemento que a cauda da fila, o que significa que
a fila ficou vazia. Enquanto este estado durar, no possvel retirar mais elementos da fila.
Nesta situao, a cabea da fila deve ficar a apontar para a posio N do agregado, ou seja,
a posio depois do fim do agregado, como indicao de fila vazia. Assim, fica reposta o
estado inicial da fila, com a diferena que neste caso, a cauda da fila no est a apontar para
a posio 0 do agregado. Quando se colocar um elemento na fila, na posio apontada pela
cauda da fila, a cabea da fila ser colocada de novo a apontar para o elemento acabado de
colocar.
...
FIFO[0]
FIFO[N-1]
cabea
da fila
cauda
da fila
FIFO[0]
...
FIFO[N-1]
FILA
VAZIA
cauda da fila
cabea da fila
cabea
da fila
/* rea de armazenamento */
/* cabea da fila */
/* cauda da fila */
/* Definio de Constantes */
#define
#define
#define
#define
#define
OK
NULL_PTR
NO_MEM
FIFO_EMPTY
FIFO_FULL
0
1
3
4
5
/*
/*
/*
/*
/*
/* rea de armazenamento */
/* cabea da fila */
/* cauda da fila */
10
/* Definio de Constantes */
#define
#define
#define
#define
OK
NULL_PTR
NO_MEM
FIFO_EMPTY
0
1
3
4
/*
/*
/*
/*
11
cabea
da fila
cabea
da fila
cauda
da fila
estado inicial
PtSeg
PtEle
cauda
da fila
colocao
do primeiro
elemento
Elemento
1
A Figura 7.15 apresenta a colocao de mais um elemento na fila. Neste caso, este
elemento tem de ser ligado fila, pelo que, o n do elemento que est apontado pela cauda
e que aponta para NULL, posto a apontar para o novo n, que passa agora a ser o ltimo
elemento da fila. A cauda da fila actualizada e a cabea da fila continua inalterada.
cabea
da fila
PtSeg
PtSeg
PtEle
PtEle
Elemento
1
Elemento
2
cauda
da fila
cabea
da fila
PtSeg
PtSeg
colocao
de um
elemento
PtSeg
PtEle
PtEle
PtEle
Elemento
1
Elemento
2
Elemento
3
cauda
da fila
cabea
da fila
PtSeg
12
PtSeg
PtSeg
PtEle
PtEle
PtEle
Elemento
1
Elemento
2
Elemento
3
cauda
da fila
cabea
da fila
remoo
de um
elemento
PtSeg
PtSeg
PtEle
PtEle
Elemento
2
Elemento
3
cauda
da fila
PtSeg
PtEle
cauda
da fila
Elemento
3
cabea
da fila
cauda
da fila
remoo
do ltimo
elemento
FILA
VAZIA
13
/* cabea da fila */
/* cauda da fila */
14
7.3 Pilhas
Uma memria pilha ( stack/LIFO ) uma memria em que s possvel processar a
informao pela ordem inversa ordem de chegada. Da que, tambm seja apelidada de
memria do ltimo a chegar primeiro a sair. Numa memria pilha, o posicionamento para
a colocao de um elemento na pilha, que vamos designar por Stack_Push, e o
posicionamento para a remoo de um elemento da pilha, que vamos designar por
Stack_Pop, o topo da pilha (top of the stack).
/* Definio de Constantes */
#define
#define
#define
#define
OK
NULL_PTR
STACK_EMPTY
STACK_FULL
0
1
4
5
/*
/*
/*
/*
15
colocao do
primeiro elemento
STACK[N-1] Elemento
STACK[N-1] Elemento
.
.
.
Elemento
.
.
.
Elemento
STACK[1]
Elemento
STACK[0]
Elemento
topo da
pilha
Elemento
Elemento
STACK[1]
Elemento
STACK[0]
Elemento
topo da
pilha
Sempre que se coloca um elemento na pilha, o topo da pilha deslocada para a posio
seguinte da pilha, depois da cpia do elemento para a pilha. A Figura 7.21 apresenta a
situao limite de utilizao da pilha, quando se coloca o ltimo elemento na pilha. Nesta
situao o topo da pilha fica a apontar para a posio do agregado de ndice N, o que
significa que a pilha ficou cheia. Enquanto este estado durar, no possvel colocar mais
elementos na pilha.
colocao do ltimo elemento
STACK[N-1] Elemento
.
.
.
Elemento
Elemento
topo da
pilha
PILHA
CHEIA
topo da
pilha
STACK[N-1] Elemento
.
.
.
Elemento
Elemento
STACK[1]
Elemento
STACK[1]
Elemento
STACK[0]
Elemento
STACK[0]
Elemento
16
Sempre que um elemento retirado da pilha, primeiro preciso deslocar o topo da pilha
para a posio anterior da pilha, onde est armazenado o ltimo elemento da pilha. S
depois que o elemento copiado para fora da pilha.
A Figura 7.22 apresenta a situao de remoo de um elemento, quando existe mais do que
um elemento na pilha. O topo da pilha deslocado para a posio anterior e a informao
armazenada no elemento retirado da pilha.
remoo de um elemento
STACK[N-1] Elemento
STACK[N-1] Elemento
.
.
.
Elemento
topo da
pilha
Elemento
.
.
.
Elemento
Elemento
STACK[1]
Elemento
STACK[1]
Elemento
STACK[0]
Elemento
STACK[0]
Elemento
topo da
pilha
.
.
.
STACK[N-1] Elemento
Elemento
.
.
.
Elemento
STACK[1]
Elemento
STACK[0]
Elemento
topo da
pilha
Elemento
Elemento
STACK[1]
Elemento
STACK[0]
Elemento
PILHA
VAZIA
topo da
pilha
17
/* rea de armazenamento */
/* topo da pilha */
18
/* Definio de Constantes */
#define
#define
#define
#define
#define
OK
NULL_PTR
NO_MEM
STACK_EMPTY
STACK_FULL
0
1
3
4
5
/*
/*
/*
/*
/*
/* rea de armazenamento */
/* topo da pilha */
19
/* Definio de Constantes */
#define
#define
#define
#define
OK
NULL_PTR
NO_MEM
STACK_EMPTY
0
1
3
4
/*
/*
/*
/*
estado inicial
20
colocao do
primeiro elemento
topo da
pilha
topo da
pilha
PtEle
Elemento
1
PtAnt
Figura 7.28 - Situao inicial da pilha e aps a colocao do primeiro elemento na pilha.
topo da
pilha
PtEle
Elemento
2
PtAnt
topo da
pilha
PtEle
Elemento
1
PtEle
PtAnt
Elemento
1
PtAnt
PtEle
Elemento
2
PtAnt
PtEle
PtAnt
Elemento
1
topo da
pilha
PtEle
PtAnt
Elemento
1
21
A Figura 7.31 mostra a remoo do ltimo elemento da pilha. Como este ltimo n aponta
para NULL, ao ser atribudo o valor NULL ao topo da pilha isso sinal de que a pilha
ficou vazia, e a pilha fica num estado igual ao estado inicial.
remoo do ltimo elemento
topo da
pilha
PtEle
Elemento
1
topo da
pilha
PILHA
VAZIA
PtAnt
22
/* topo da pilha */
23
curvos. Por vezes para melhor identificar os vrios nveis de subexpresses, utilizam-se
parnteses curvos, rectos e chavetas, tal se mostra na seguinte expresso.
{ A + B * [ C / (F+A) + (C+D) * (EF) ] } / [ (A+B) * C ]
Para que uma expresso aritmtica possa ser correctamente calculada necessrio assegurar
o correcto balanceamento dos parnteses, como o caso da expresso anterior. Uma
expresso com um balanceamento correcto de parnteses, tem tantos parnteses a abrir, ou
seja, parnteses esquerdos, como parnteses a fechar, ou seja, parnteses direitos. Mas
tambm preciso assegurar que um nvel interno de parnteses fechado antes que um
nvel externo e com um parntese direito equivalente ao parntese esquerdo. Pelo que, a
anlise no pode apenas limitar-se a contar o nmero de parnteses direitos e esquerdos de
cada tipo. Por exemplo, a expresso seguinte tem o mesmo nmero de parnteses direitos e
esquerdos e no est balanceada, porque a chaveta fechada antes do parntese recto.
{ A + B * [ C / (F+A) + (C+D) * (EF) } ] / [ (A+B) * C ]
Uma vez que necessrio assegurar que cada parntese direito equivalente ao ltimo
parntese esquerdo, esta anlise pode ser feita usando uma pilha O algoritmo o seguinte.
Sempre que aparece um parntese esquerdo na expresso, este colocado na pilha.
Quando aparece um parntese direito, tira-se da pilha o ltimo parntese esquerdo l
colocado e verifica-se se so equivalentes. Caso o teste seja positivo ento este nvel interno
de parnteses est balanceado e ambos os parnteses so descartados. Caso os parnteses
no sejam equivalentes, ento o processo interrompido porque este nvel interno de
parnteses no est balanceado. Se por acaso no existir um parntese esquerdo na pilha,
ento sinal que existem mais parnteses direitos que esquerdos at esta posio da
expresso, pelo que, a expresso tambm no est balanceada. Quando a anlise da
expresso terminar preciso verificar se a pilha est vazia. Porque, caso no esteja vazia,
ento sinal que existem mais parnteses esquerdos que direitos na expresso, pelo que, a
expresso tambm no est balanceada.
A Figura 7.34 apresenta o programa que implementa este algoritmo. De maneira a
estruturar melhor o programa, foi implementada uma funo inteira que compara o
parntese esquerdo com o parntese direito e que devolve 1, caso eles sejam equivalentes e
0 no caso contrrio. Neste caso a pilha utilizada a implementao esttica, mas podia ser
qualquer outra implementao. Para utilizar a pilha, primeiro preciso editar o ficheiro de
interface elemento.h, e definir o nmero de elementos da estrutura de dados da pilha com
um valor suficiente para armazenar os parnteses da expresso, que no pior caso podem ser
tantos quantos o nmero de caracteres da expresso e definir tambm o tipo dos elementos
da pilha como sendo do tipo char. Depois o mdulo compilado, com a opo c, para
ficar concretizado para uma pilha de caracteres. Finalmente, o programa compilado,
indicando no comando de compilao o ficheiro objecto do mdulo pilha_est.o. A Figura
7.33 apresenta o resultado de execuo do programa para vrias expresses.
Expresso algbrica -> {A+B*[C/(F+A)+(C+D)*(EF)]}/[(A+B)*C]
Expresso com parnteses balanceados
Expresso algbrica -> {A+B*[C/(F+A)+(C+D)*(EF)}]/[(A+B)*C]
Parnteses [ e } discordantes
Expresso algbrica -> {A+B*[C/(F+A)+(C+D)*(EF)]}/[(A+B)*C]}
Mais parnteses direitos do que esquerdos
Expresso algbrica -> {A+B*[C/(F+A)+(C+D)*(EF)]}/[(A+B)*C
Mais parnteses esquerdos do que direitos
24
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "pilha_est.h" /* ficheiro de interface do mdulo da pilha */
int parenteses_equivalentes (char, char);
int main (void)
{
char exp[81],
cpilha;
int nc,
c,
st;
*/
*/
*/
*/
*/
Figura 7.34 - Programa que verifica o balanceamento dos parnteses numa expresso.
25
26
27
A funo de colocao de elementos na fila Fifo_In comea por assegurar que a fila existe
e depois comporta-se tal como na implementao dinmica. Mas, a cpia do elemento para
a fila feita pela funo memcpy indicando o ponteiro para o elemento a colocar na fila, o
ponteiro para a cauda da fila e o nmero de bytes a copiar, que est armazenado no campo
que especifica o tamanho dos elementos da fila. Desta maneira possvel manipular os
elementos da fila sem que se saiba de que tipo eles so.
A funo de remoo de elementos da fila Fifo_Out comea por assegurar que a fila existe
e depois comporta-se tal como na implementao dinmica. Tal como na funo Fifo_In,
a cpia do elemento da fila feita pela funo memcpy.
/****************** Interface da FILA Abstracta ******************/
/* Nome : fila_abs.h */
#ifndef _FILA_ABSTRACTA
#define _FILA_ABSTRACTA
typedef struct fifo *PtFifo;
/* Definio de Constantes */
#define
#define
#define
#define
#define
#define
OK
NULL_PTR
NULL_SIZE
NO_MEM
FIFO_EMPTY
NO_FIFO
0
1
2
3
4
7
/*
/*
/*
/*
/*
/*
Devolve
28
struct fifo
{
unsigned int size;/* tamanho em nmero de bytes de cada elemento */
PtNo head;
/* cabea da fila */
PtNo tail;
/* cauda da fila */
};
/* Definio das Funes */
PtFifo Fifo_Create (unsigned int sz)
{
PtFifo fifo;
if (sz == 0) return NULL;
if ((fifo = (PtFifo) malloc (sizeof (struct fifo))) == NULL)
return NULL;
fifo->size = sz;
fifo->head = NULL;
fifo->tail = NULL;
return fifo;
}
int Fifo_Destroy (PtFifo *fila)
{
PtFifo fifo = *fila; PtNo tmp;
if (fifo == NULL) return NO_FIFO;
while ( fifo->head != NULL )
{
tmp = fifo->head;
fifo->head = fifo->head->pseg;
free (tmp->pelemento);
free (tmp);
}
free (fifo);
*fila = NULL;
return OK;
}
Figura 7.37 - Ficheiro de implementao da fila abstracta com mltipla instanciao (1 parte).
29
Figura 7.38 - Ficheiro de implementao da fila abstracta com mltipla instanciao (2 parte).
30
OK
NULL_PTR
NULL_SIZE
NO_MEM
STACK_EMPTY
NO_STACK
0
1
2
3
4
7
/*
/*
/*
/*
/*
/*
Devolve
31
struct stack
{
unsigned int size;/* tamanho em nmero de bytes de cada elemento */
PtNo top;
/* topo da pilha */
};
/* Definio das Funes */
PtStack Stack_Create (unsigned int sz)
{
PtStack stack;
if (sz == 0) return NULL;
if ((stack = (PtStack) malloc (sizeof (struct stack))) == NULL)
return NULL;
stack->size = sz;
stack->top = NULL;
return stack;
}
int Stack_Destroy (PtStack *pilha)
{
PtStack stack = *pilha; PtNo tmp;
if (stack == NULL) return NO_STACK;
while (stack->top != NULL)
{
tmp = stack->top;
stack->top = stack->top->pant;
free (tmp->pelemento);
free (tmp);
}
free (stack);
*pilha = NULL;
return OK;
}
Figura 7.40 - Ficheiro de implementao da pilha abstracta com mltipla instanciao (1 parte).
32
Figura 7.41 - Ficheiro de implementao da pilha abstracta com mltipla instanciao (2 parte).
33
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "pilha_abs.h" /* ficheiro de interface do mdulo da pilha */
int main (void)
{
char exp[81], cpilha;
int nc, c, st;
PtStack pilha1 = NULL, pilha2 = NULL;
pilha1 = Stack_Create (sizeof (char));
pilha2 = Stack_Create (sizeof (char));
printf ("Texto de entrada -> ");
nc = strlen (exp);
/* Criao da pilha 1 */
/* Criao da pilha 2 */
/* Destruio da pilha 1 */
/* Destruio da pilha 2 */
return EXIT_SUCCESS;
}