Escolar Documentos
Profissional Documentos
Cultura Documentos
ALGORITMOS
Sumário
Comecemos com um problema simples. Pretende-se escrever um programa que dada uma
distância expressa em milhas, que é lida do teclado, converte-a para quilómetros e escreve-a
no monitor. A especificação completa é apresentada na Figura 2.1. A partir do enunciado,
deduzimos que temos apenas uma variável de entrada MILHAS, que é a distância em
milhas a ser convertida. Uma distância é obviamente uma quantidade física real não
negativa. Também temos apenas uma variável de saída QUILOMETROS, que é a distância
calculada em quilómetros através da fórmula de conversão.
Neste caso temos três variáveis de entrada, que são os coeficientes A, B e C da equação de
2º grau, sendo que A tem de ser forçosamente diferente de zero para que a equação seja
realmente de 2º grau. As variáveis de saída são duas, X1 e X2, mas como as raízes da
equação de 2º grau, no caso geral, são números complexos, então temos quatro variáveis de
saída. X1R e X2R são as partes reais de X1 e X2 respectivamente, enquanto X1I e X2I são
as partes imaginárias de X1 e X2 respectivamente. A solução passa por aplicar a fórmula
resolvente, em que se começa por calcular o descriminante B2-4AC. Se o valor for positivo
ou nulo, então temos soluções reais, ou seja, soluções complexas com parte imaginária igual
a zero. Senão temos soluções complexas conjugadas.
3 CAPÍTULO 2 : ALGORITMOS
− B + ∆ − B − ∆
∆ ≥ 0 ⇒ X1R = e X1I = 0 ; X 2R = e X 2I = 0
2× A 2× A
− B − ∆ − B − ∆
∆ < 0 ⇒ X1R = e X1I = ; X 2R = e X 2I = −
2× A 2× A 2× A 2× A
Um algoritmo deve contemplar alternativas para toda a gama de valores das variáveis de
entrada, o que implica uma descrição algébrica da solução do problema. Só tendo em
consideração todas as possibilidades dos valores que se apresentam à entrada do problema
é que se consegue construir uma solução eficaz para o problema. Entenda-se por solução
eficaz, uma solução que num intervalo de tempo finito e independentemente dos dados
colocados à entrada, atinge sempre um resultado. Quer seja, o cálculo dos dados de saída,
quer seja, a indicação da impossibilidade de efectuar tal cálculo.
Também foi referido que o algoritmo deve considerar sempre a resolução de classes de
problemas e não de problemas particulares. Deve também fazê-lo de forma eficiente, ou
seja, da maneira mais objectiva e com o menor custo possível. O custo pode ser medido
em número de instruções do processador. Finalmente, o algoritmo deve terminar, ou seja,
deve atingir a situação em que não existem mais instruções para executar.
Numa definição mais formal, podemos dizer que um algoritmo é uma descrição detalhada e
rigorosa da solução do problema, ou seja, uma sequência finita de instruções bem definidas
que num número finito de passos, resolve de forma eficaz e eficiente o problema.
Uma descrição detalhada e rigorosa implica que cada uma das operações tem que ser
enunciada sem ambiguidade. Costuma-se começar por usar uma linguagem natural, por
exemplo o português, e depois à medida que a descrição se torna mais detalhada, passa-se
para uma linguagem formalmente mais simples, mas mais rigorosa, baseada na notação
matemática. Esta linguagem designa-se por pseudocódigo.
Quando se diz que um algoritmo é uma sequência finita de instruções, estamos a supor que
o conjunto de operações descrito no algoritmo é realizado segundo uma ordem
PROGRAMAÇÃO ESTRUTURAS DE DADOS E ALGORITMOS EM PASCAL 4
A sequência de operações tem necessariamente que ser concluída num número finito de
passos, sob pena da solução, que ela representa, não ser realizável num computador. A
sequência de operações também tem que produzir sempre resultados, ou seja, calcular
valores, senão é inútil.
Uma decomposição hierárquica, começa sempre com uma solução inicial, onde são
tomadas as decisões mais importantes, e depois, continua-se a refinar a solução, explorando
eventualmente várias soluções alternativas, à medida que vamos quebrando a complexidade
do problema.
Vamos começar por apresentar os algoritmos em linguagem natural dos problemas, cujas
especificações completas, foram apresentadas anteriormente.
A Figura 2.3 apresenta as três operações necessárias para resolver o problema da conversão
de distâncias de milhas para quilómetros, cuja especificação completa foi apresentada na
Figura 2.1.
5 CAPÍTULO 2 : ALGORITMOS
Primeiro é preciso ler do teclado um valor não negativo que representa a distância em
milhas que se pretende converter, daí a designação de leitura com validação. Depois é
preciso efectuar a conversão, usando para o efeito a fórmula de conversão. Finalmente o
valor calculado é escrito no monitor. Para separar as operações usamos o separador do
Pascal que é o ponto e vírgula (;).
Qualquer um dos algoritmos apresentados ainda está longe do algoritmo desejável para
passar à fase de codificação em Pascal. Por exemplo, a primeira operação de leitura com
validação tem que ser melhor explicitada. O que é validar uma variável? E a parte de
conversão ou de cálculo deve ser explicitada através duma solução algébrica. No entanto,
esta primeira descrição algorítmica, ou descrição algorítmica de primeiro nível, dá-nos já
uma ideia das operações fundamentais do programa. Agora temos que pegar em cada uma
das operações deste primeiro nível e refiná-las, usando para o efeito uma linguagem
algébrica e pseudocódigo, obtendo assim algoritmos mais rigorosos de segundo nível.
Cada componente da descrição algorítmica deve ser composta por (ver Figura 2.5): um
título; o tipo de operação, função ou procedimento, quando se implementa o componente
como um subprograma, caso contrário é omitido; a descrição das variáveis de entrada e de
saída; e a sequência de operações entre begin e end. A descrição das variáveis de entrada e
de saída pode ser omitida se forem devidamente descritas na especificação completa.
begin
decomposição da operação em termos de operações mais simples
end
begin
sequência de operações simples
end
if ( condição booleana )
then operação simples ou composta;
─────────────────────────────────────────────────────────────────────
if ( condição booleana )
then operação simples ou composta
else operação simples ou composta;
Figura 2.9 - Definição da operação repetitiva cujo número de iterações é previamente conhecido.
A operação simples ou composta que constitui o núcleo repetitivo da operação for é
efectuada um número de vezes igual à distância positiva, mais um, entre o valor inicial e o
valor final. O sentido de variação é crescente para a operação for … to … do, e,
decrescente, para a operação for … downto … do. A variável contadora vai
sucessivamente assumindo, em cada iteração, um dos valores do intervalo especificado,
começando pelo valor inicial e terminando no valor final. As operações que formam o
núcleo repetitivo, podem referenciar esta variável, mas em nenhum caso podem modificar
o seu valor.
repeat
sequência de operações simples
until ( condição booleana );
─────────────────────────────────────────────────────────────────────
while ( condição booleana ) do
operação simples ou composta;
Figura 2.10 - Definição das operações repetitivas cujo número de iterações é previamente
desconhecido.
PROGRAMAÇÃO ESTRUTURAS DE DADOS E ALGORITMOS EM PASCAL 8
Para obter uma leitura com validação, é necessário repetir a leitura do valor introduzido
pelo teclado, ou seja da variável de entrada MILHAS, até que o valor lido seja maior ou
igual a zero. Para tal efeito, utiliza-se a instrução repeat ... until a abraçar as instruções de
escrita da mensagem e de leitura da variável. Para criar uma melhor interface entre o
programa e o ser humano que vai utilizar o programa, todos os valores de entrada a ser
inseridos pelo teclado, devem ser precedidos por uma mensagem a informar o utilizador.
Essa mensagem deve ser sucinta e elucidativa dos valores que o programa está à espera. Na
escrita dos valores de saída, para além da escolha de um formato de saída adequado para
cada valor, estes também devem ser acompanhados por uma mensagem, também ela
sucinta e suficientemente explicativa da informação que está a ser escrita no monitor. Por
agora, vamos omitir os formatos de escrita dos valores de saída. Na conversão da distância,
aquando do cálculo da variável de saída QUILOMETROS, usa-se o operador de atribuição
do Pascal := em vez do operador = usado na expressão matemática da especificação
completa. Atribuir um valor a uma variável, significa armazenar um valor na célula de
memória usada para guardar a variável.
nome: Determinação das raízes da equação (A, B, C, X1R, X1I ,X2R, X2I)
begin
DELTA := sqr (B) - 4 * A * C;
if (DELTA >= 0.0)
then begin
X1R := (-B + sqrt (DELTA)) / (2 * A);
X1I := 0.0;
X2R := (-B - sqrt (DELTA)) / (2 * A);
X2I := 0.0;
end
else begin
X1R := -B / (2 * A);
X1I := sqrt (-DELTA) / (2 * A);
X2R := X1R;
X2I := -X1I;
end
end
Como estes algoritmos de segundo nível já estão muito próximos do Pascal, já podemos
fazer o algoritmo completo do problema. Nesta fase inicial, em que ainda não foram
apresentados os subprogramas, o algoritmo final do problema consiste na fusão dos
algoritmos parcelares, eliminando obviamente as instruções begin e end que encapsulam
cada algoritmo parcelar. A Figura 2.14 apresenta o algoritmo final no caso do problema da
conversão de distâncias. Compare-o com os algoritmos parcelares da Figura 2.12. Repare
que se separa as partes importantes do programa com um comentário, que é nada mais,
nada menos, que os nomes da operações do algoritmo de primeiro nível.
end
O enunciado diz-nos que vai existir um processo de leitura que terminará apenas quando
for introduzido um valor de paragem, neste caso o zero. Logo, não podemos saber à
partida quantos valores vão de facto ser lidos. Pelo que, não podemos declarar variáveis de
entrada para armazenar toda a informação lida. A solução passa então por ler um valor para
a variável de entrada e processá-lo de imediato, de modo a poder reutilizar a variável.
Como para calcularmos a média de um conjunto de números, temos que somá-los e contá-
los primeiro, então o que temos de fazer é ir somando e contando os números lidos, com
excepção do número de paragem, durante o processo repetitivo de leitura. Quando a leitura
terminar, e partindo do princípio que foram de facto lidos números para além do número
de paragem, poderemos então calcular a média e imprimi-la no monitor. As Figura 2.15 e
Figura 2.16 apresentam respectivamente a especificação completa e o algoritmo de
primeiro nível em linguagem natural.
Este tipo de variável designa-se por variável acumuladora, e para que funcione bem temos
que assegurar que o valor inicialmente armazenado na variável é zero. Pelo que, temos de a
inicializar a zero, e como ela é real usamos o valor 0.0, para aumentar a legibilidade do
programa. Para contarmos os números lidos temos que pegar na variável N e acumular
uma unidade por cada número lido, o que se designa por incrementar a variável.
Obviamente, esta variável também precisa de ser inicializada a zero. Quando o valor de
paragem é lido, temos que proteger a operação de contagem para não fazermos uma
contagem incorrecta. Já que, este valor destina-se apenas a parar a leitura e não deve
interferir no cálculo da média. A protecção da operação de soma acumulada é desnecessária
neste caso, porque o zero é o elemento neutro da adição, mas é feita apenas por uma
questão de eficiência, evitando uma adição inútil. Para proteger estas duas instruções usa-se
a operação decisória binária if. O operador desigualdade em Pascal é <>. A Figura 2.18
apresenta o algoritmo final em pseudocódigo.
(* Impressão da média *)
writeln ('Média dos números lidos = ' , MEDIA);
end
else writeln ('Não foi introduzido qualquer número');
end
NLB
altura ( NL )
NCB
largura ( NC )
repeat
write ('Largura do triângulo? ');
readln (NC);
until (NC > 0) and (NC <= NCMAX);
end
Na operação de leitura, a validação das dimensões do triângulo, implica que têm de ser
maiores do que zero, e (and) não podem exceder as dimensões máximas da janela de
visualização. É preciso ter em consideração que, quando pretendemos usar uma condição
booleana composta, cada uma das condições booleanas simples, tem que ser
obrigatoriamente inserida entre parêntesis curvos. A caracterização espacial da figura
implica o cálculo da centragem vertical NLB e da centragem horizontal NCB. A impressão
da figura, ou seja, do triângulo e do espaço envolvente, implica escrever NLB linhas em
branco, seguido do triângulo, seguido de NLB linhas em branco. A operação de impressão
das linhas em branco é a mesma, independentemente do sítio onde vai ser utilizada, antes
ou depois do triângulo.
As operações que se encontram definidas em linguagem natural, ainda não são operações
elementares e por isso serão objecto de uma análise mais detalhada num terceiro nível
hierárquico, que é apresentado na Figura 2.23.
Para escrever NLB linhas em branco, temos que mudar de linha, ou seja, fazer writeln,
num ciclo repetitivo de tipo for. Para desenhar o triângulo temos que escrever as suas
linhas num processo repetitivo. Para cada linha, temos que calcular o número de asteriscos
NAST, escrever NCB espaços antes de começarmos a escrever os NAST asteriscos e
finalmente muda-se de linha. Repare que não é preciso escrever outra vez NCB espaços
depois dos asteriscos para centrar horizontalmente o triângulo. Vamos agora analisar na
Figura 2.24 estas operações do quarto nível hierárquico.
17 CAPÍTULO 2 : ALGORITMOS
A parte difícil do algoritmo é o cálculo do número de asteriscos de cada linha. Para isso
temos que recorrer à equação da recta, definida pelos dois pontos (coluna 1, linha 1) e
(coluna NC, linha NL). O que dá o declive (NL - 1) / (NC - 1). De notar que nesta figura o
eixo dos x é ao longo das colunas da janela e o eixo dos y é ao longo das linhas da janela.
Aplicando a equação da recta (y = mx + b) e sendo que o y é o número da linha de
impressão, ou seja, LINHA, e o x é o número de asteriscos a calcular, ou seja, NAST, então
obtemos a expressão NAST = (LINHA - 1) / (NL - 1) * (NC - 1) + 1. Como o número de
caracteres a escrever no monitor tem que ser obrigatoriamente um número inteiro, este
valor é arredondado. Para isso usa-se a função round. Com estes algoritmos de quarto
nível, já podemos escrever o algoritmo completo em pseudocódigo do terceiro nível da
impressão do triângulo, que é apresentado na Figura 2.25.
(* Mudar de linha *)
writeln;
end
end
end
(* Impressão do triângulo *)
for LINHA := 1 to NL do
begin
(* Mudar de linha *)
writeln;
end
end
repeat
write ('Altura do triângulo? ');
readln (NL);
until (NL > 0) and (NL <= NLMAX);
repeat
write ('Largura do triângulo? ');
readln (NC);
until (NC > 0) and (NC <= NCMAX);
(* Impressão da figura *)
(* Impressão do triângulo *)
for LINHA := 1 to NL do
begin
(* Impressão de espaços *)
for NCAR := 1 to NCB do write (' ');
(* Impressão de asteriscos *)
for NCAR := 1 to NAST do write ('*');
(* Mudar de linha *)
writeln;
end
end
Vamos analisar este problema sobre o ponto de vista dos valores de entrada. Primeiro
temos a leitura do comprimento das calhas de perfil, seguida da lista de secções que
pretendemos cortar. Como não sabemos à partida quantos são os cortes a efectuar,
portanto, não podemos declarar variáveis de entrada para armazenar toda a informação
lida. Vamos assumir por uma questão de simplificação que os tamanhos das secções a
cortar são dados em centímetros e são valores exactos, ou seja, valores inteiros. Como um
comprimento é um valor positivo não nulo, então podemos usar o valor zero, como
terminador da leitura. Estamos perante um problema que se enquadra no modelo de
decomposição algorítmica que classificámos de modelo básico modificado à entrada.
Vamos agora analisar este problema sobre o ponto de vista dos valores de saída. Para cada
calha de perfil, temos que indicar os cortes a efectuar, sendo obviamente indiferente a
ordem desses mesmos cortes, e a sobra que não vai ser usada. Como à partida também não
sabemos quantas calhas de perfil vamos usar, então também não podemos declarar
variáveis de saída para armazenar toda a informação calculada. Estamos perante um
problema que se enquadra no modelo de decomposição algorítmica que classificámos de
modelo básico modificado à saída. E, ainda existem dois valores de saída que são
calculados após todo o processamento, e que são o número de calhas que foram usadas e a
eficiência.
21 CAPÍTULO 2 : ALGORITMOS
Este algoritmo tem no entanto um problema. É que não aproveita ao máximo as calhas de
perfil, já que se o corte seguinte não puder ser efectuado na calha em uso, então o resto da
calha é desperdiçado. Mas na prática, quando isto acontece, o senhor João vai procurar na
lista se existe mais à frente um corte que ainda dê para aproveitar o resto da calha. Só que a
nossa solução para computador não contempla tal facto. Porquê? Porque o programa não
sabe em avanço os valores seguintes da lista e portanto, não pode tentar aproveitar a sobra
da calha. Isto deve-se ao facto de até agora considerarmos que um programa só pode ler e
armazenar um valor de cada vez numa variável. Ou seja, estamos a considerar que uma
variável representa apenas uma célula de memória e portanto não pode armazenar mais do
que um valor.
Precisamos então de poder ler do teclado uma lista de valores e armazená-los numa única
variável, para serem posteriormente utilizados num ciclo repetitivo. A esse tipo de variáveis
dá-se o nome de array. Um array é um agregado de células de memória contíguas, que
apesar de ser identificada por um único nome, pode armazenar um número, definido pelo
programador, de valores diferentes simultaneamente. Mas, permitindo o acesso individual a
cada célula de memória, ou seja, ao valor armazenado em cada elemento da variável array.
Para aceder a cada elemento individual, usa-se o nome da variável seguido do índice do
elemento entre parêntesis rectos.
Com este tipo de variáveis, então já podemos ler do teclado os valores das secções que se
pretendem cortar e por conseguinte separar a leitura do processamento. Podemos assim
passar a ter uma decomposição algorítmica do tipo básico, sob o ponto de vista da entrada
de dados. Substituindo na especificação completa do problema, a variável de entrada
CORTE pela variável LISTACORTES de tipo array, podemos então reescrever o algoritmo
principal, ou de primeiro nível hierárquico, tal como é apresentado na Figura 2.34. A
operação de leitura passa a trazer para o programa, para posterior utilização nas operações
seguintes, o comprimento da calha COMPCALHA, as secções que pretendemos cortar
LISTACORTES, bem como o número de secções a cortar N.
Figura 2.34 - Nova versão do algoritmo em linguagem natural do problema dos alumínios.
Agora a leitura de todos os dados de entrada é feita numa única operação, como é
apresentada na Figura 2.35. Uma variável de tipo array, tem sempre associado um número
máximo de elementos que pode armazenar e que foi definido pelo programador. Neste
caso, este valor é representado pela constante LMAX. A leitura dos valores é feita um a
um, indicando para o efeito o índice do elemento que está a ser lido. Vamos para já
considerar que o índice do primeiro elemento é 1. A leitura termina com a introdução do
valor zero, significando que não se pretende introduzir mais dados, tal como na versão
anterior. Ou (or), quando se esgota a capacidade de armazenamento da variável. Daí a
existência das duas condições do ciclo repetitivo. Quando a leitura terminar, é preciso
calcular o número de elementos da variável que contêm informação útil, ou seja, o número
de valores lidos, menos o último, caso ele seja o terminador.
PROGRAMAÇÃO ESTRUTURAS DE DADOS E ALGORITMOS EM PASCAL 26
Este contador do número de elementos efectivos do array é um valor numérico inteiro que
é obviamente, maior ou igual a zero e menor ou igual a LMAX. Posteriormente, esta
variável N permite-nos controlar o acesso aos elementos úteis do array.
N := 0;
repeat
N := N + 1;
write ('Comprimento da secção a ser cortada? ');
readln (LISTACORTES[N]);
until (LISTACORTES[N] = 0) or (N = LMAX);
if (LISTACORTES[N] = 0) then N := N – 1;
end
de a melhor que serve (best fit). A terceira, consiste em aproveitar ao mínimo a sobra e
portanto, procurar no resto da lista a menor secção que ainda pode ser cortada a partir da
sobra, com o objectivo de maximizar o desperdício, para permitir ainda aproveitar a sobra
restante. Este algoritmo tem o nome de a pior que serve (worst fit). É preciso ter em
consideração, que o algoritmo first fit é dependente da ordem dos valores da lista, enquanto
que os outros dois não o são, pelo que, devem produzir melhores resultados. Os algoritmos
best fit e worst fit classificam-se como algoritmos heurísticos.
Do lado da saída do programa, não podemos fazer praticamente nada para separar o
processamento da escrita dos resultados. Agora, o que podemos fazer é criar de facto um
relatório da simulação do programa. Na actual versão, os resultados são escritos no
monitor, o que pode ser um problema. Como a janela de visualização é limitada e o
relatório pode exceder o número máximo de linhas de visualização, então parte do relatório
pode ficar irremediavelmente perdido e obrigar a nova simulação. A solução passa por
enviar o relatório para a memória de massa através da utilização de um ficheiro de texto.
Depois este ficheiro pode ser lido, analisado e eventualmente enviado para uma impressora
usando um dos editores de texto disponibilizados pelo sistema operativo.
Esta solução, com saída para ficheiro e a análise dos três algoritmos de pesquisa sequencial
mencionados, será objecto de estudo mais tarde, quando se estudar as variáveis
estruturadas de tipo array e os ficheiros de texto na linguagem Pascal.
2.7 Exercícios
Para os exercícios descritos a seguir, aplique a análise algorítmica que foi empregue neste
capítulo. Comece por fazer a especificação completa do problema e o algoritmo de
primeiro nível, com as operações principais em linguagem natural. Depois, pegue em cada
uma dessas operações e detalhe os seus algoritmos em pseudocódigo. Finalmente, junte os
algoritmos das operações e faça o algoritmo final do problema em pseudocódigo.
1. Pretende-se escrever um programa que dada uma temperatura em graus Celsius, que é
lida do teclado, converte-a para graus Fahrenheit e escreve-a no monitor. A fórmula de
conversão é F = 1.8 * C + 32.