Programação Orientada à Procedimentos II Programação Orientada à Procedimentos I Processamento Paralelo Lógica de Programação Introdução à Computação Informática Básica

 

 

Informática Básica
Introdução à Computação
Lógica de Programação
Programação Orientada à Procedimentos I
Programação Orientada à Procedimentos II

Processamento Paralelo

Aula 1

Aula 2

Aula 3



CAPÍTULO I

INTRODUÇÃO


1.1 Noções de Lógica

1.1.1 O que é Lógica?

O uso corriqueiro da palavra lógica está normalmente relacionado à coerência e racionalidade. Freqüentemente associa-se lógica apenas à matemática, não percebendo sua aplicabilidade e relação com as demais ciências. Podemos relacionar a lógica com a "correção do pensamento", pois uma de suas preocupações é determinar quais operações são válidas e quais não são, fazendo análises das formas e leis do pensamento. Como filosofia, ela procura saber por que pensamos assim e não de outro jeito. Com arte ou técnica, ela nos ensina a usar corretamente as leis do pensamento, Poderíamos dizer também que a lógica é a "arte de bem pensar", que é a "ciência das formas do pensamento". Visto que a forma mais complexa do pensamento é o raciocínio, a lógica estuda a "correção do raciocínio". Podemos ainda dizer que a lógica tem em vista a "ordem da razão". Isto dá a entender que a nossa razão pode funcionar desordenadamente. Por isso a lógica estuda e ensina a colocar "ordem no pensamento".

Exemplos:
a) Todo mamífero é um animal.
Todo cavalo é um mamífero.
Portanto, todo cavalo é um animal.

b) Kaiton é país do planeta Stix.
Todos os Xinpins são de Kaiton.
Logo, todos os Xinpins são Stixianos.

Esses exemplos ilustram silogismos, que no estudo da Lógica Proposicional (ou Cálculo Sentencial) representam um argumento composto por duas premissas e uma conclusão; e está estabelecendo uma relação, que pode ser válida ou não. Este é um dos objetivos da lógica, o estudo de técnicas de formalização, dedução e análise que permitam verificar a validade de argumentos. No caso dos exemplos, ambos são válidos. Devemos ressaltar que, apesar da aparente coerência de um encadeamento lógico, este pode ser válido ou não em sua estrutura. Neste sentido, a lógica também objetiva a criação de uma representação mais formal, que se contrapõe à linguagem natural, que é suscetível a argumentações informais.

1.1.2 Existe Lógica no Dia-a-Dia?

Sempre que pensamos, a lógica ou a ilógica necessariamente nos acompanha. Quando falamos ou escrevemos estamos expressando nosso pensamento, logo, precisamos usar de lógica nessas atividades. Podemos perceber a importância da lógica em nossa vida, não só na teoria, como na prática, já que, quando queremos pensar, falar, escrever ou agir corretamente, precisamos colocar "ordem no pensamento", isto é, utilizar lógica.

Exemplos:
a) A gaveta está fechada.
A caneta está dentro da gaveta.
Precisamos primeiro abrir a gaveta para depois pegar a caneta.

b) Anacleto é mais velho que Felisberto.
Felisberto é mais velho que Marivaldo.
Portanto, Anacleto é mais velho que Marivaldo.

1.1.3 Mas e a Lógica de Programação?

Significa o uso correto das leis do pensamento, da "ordem da razão" e de processos de raciocínio e simbolização formais na programação de computadores, objetivando racionalidade e o desenvolvimento de técnicas que cooperem para a produção de soluções logicamente válidas e coerentes, que resolvam com qualidade os problemas que se deseja programar. O raciocínio é algo abstrato, intangível. Os seres humanos têm a capacidade de expressá-lo através da palavra falada ou escrita, que por sua vez se baseia em um determinado idioma, que segue uma série de padrões (gramática). Um mesmo raciocínio pode ser expresso em qualquer um dos inúmeros idiomas existentes, mas continuará representando o mesmo raciocínio, usando apenas outra convenção. Algo similar ocorre com a Lógica de Programação, que pode ser concebida pela mente treinada e pode ser representada em qualquer uma das inúmeras linguagens de programação existentes. Estas, por sua vez, são muito atreladas a uma grande diversidade de detalhes computacionais, que pouco têm a ver com o raciocínio original. Para escapar dessa torre de Babel e, ao mesmo tempo, representar mais fielmente o raciocínio da Lógica de Programação, utilizamos os Algoritmos.

1.2 O que é um Algoritmo?


O objetivo principal do estudo da Lógica de Programação é a construção de algoritmos coerentes e válidos. Mas o que é um algoritmo? Um algoritmo pode ser definido como uma seqüência de passos que visam atingir um objetivo bem definido. Na medida em que precisamos especificar uma seqüência de passos, precisamos utilizar ordem, ou seja, "pensar com ordem", portanto, precisamos utilizar lógica. Apesar do nome pouco usual, algoritmos são comuns em nosso cotidiano, como, por exemplo, uma receita de bolo. Nela está descrita uma série de ingredientes necessários e uma seqüência de diversos passos (ações) que devem ser fielmente cumpridos para que se consiga fazer o alimento desejado, conforme se esperava antes do início das atividades (objetivo bem definido). Quando elaboramos um algoritmo devemos especificar ações claras e precisas, que a partir de um estado inicial, após um período de tempo finito, produzem um estado final previsível e bem definido. Isto significa que o algoritmo fixa um padrão de comportamento a ser seguido, uma norma de execução a ser trilhada, com vistas a alcançar, como resultado final, a solução de um problema, garantindo que sempre que executado, sob as mesmas condições, produza o mesmo resultado.

1.3 Algoritmizando a Lógica

1.3.1 Porque é Importante Construir um Algoritmo?

Um algoritmo tem por objetivo representar mais fielmente o raciocínio envolvido na Lógica de Programação e, dessa forma, permite-nos abstrair de uma série de detalhes computacionais, que podem ser acrescentados mais tarde. Assim, podemos focalizar nossa atenção naquilo que é importante: a lógica da construção de algoritmos. Outra importância da construção dos algoritmos é que, uma vez concebida uma solução algorítmica para um problema, esta pode ser traduzida para qualquer linguagem de programação e ser agregada das funcionalidades disponíveis nos diversos ambientes; costumamos denominar esse processo de codificação.

1.3.2 Vamos a Um Exemplo

Podemos escrever um primeiro algoritmo de exemplo, utilizando português coloquial, que descreva o comportamento na resolução de uma determinada atividade, como por exemplo, a troca de uma lâmpada. Apesar de aparentemente óbvia demais, muitas vezes realizamos esse tipo de atividade inconscientemente, sem percebermos seus pequenos detalhes que são as ações que nos levam a alcançar o objetivo proposto. Vejamos este primeiro algoritmo, descrito passo a passo;

Algoritmo 1.1

· pegar uma escada;
· posicionar a escada embaixo da lâmpada;
· buscar uma lâmpada nova;
· subir na escada;
· retirar a lâmpada velha;
· colocar a lâmpada nova.

lnvoluntariamente, já seguimos uma determinada seqüência de ações que representadas nesse algoritmo, fazem com que ele seja seguido naturalmente por qualquer pessoa, estabelecendo um padrão de comportamento, pois qualquer pessoa agiria da mesma maneira. A seqüenciação é uma convenção com o objetivo de reger o fluxo de execução do algoritmo, determinando qual a primeira ação a ser executada e qual ação vem a seguir. Nesse caso, a seqüência é linear, de cima para baixo, assim como é a seqüência pela qual lemos um texto, de cima para baixo e da esquerda para direita. Reexaminando o algoritmo anterior, notamos que ele tem um objetivo bem definido; trocar uma lâmpada. Porém, e se a lâmpada não estivesse queimada? A execução das ações conduziria a uma troca, independentemente de a lâmpada estar ou não queimada. Porque não foi prevista essa possibilidade em sua construção. Para solucionar essa necessidade, podemos efetuar um teste a fim de verificar se a lâmpada está ou não queimada. Uma solução para esse novo algoritmo seria:

Algoritmo 1.2

· pegar uma escada;
· posicionar a escada embaixo da lâmpada;
· buscar uma lâmpada nova;
· acionar o interruptor;
· se a lâmpada não acender, então
	. subir na escada;
	. retirar a lâmpada queimada;
	. colocar a lâmpada nova.

Agora estamos ligando algumas ações à condição lâmpada não acender, ou seja, se esta condição for verdadeira (lâmpada queimada) efetuaremos a troca da lâmpada, seguindo as próximas ações:

. subir na escada;
. retirar a lâmpada queimada;
. colocar a lâmpada nova.

Se a condição lâmpada não acender for falsa (a lâmpada está funcionando), as ações relativas à troca da lâmpada não serão executadas e a lâmpada (que está em bom estado) não será trocada. O que ocorreu nesse algoritmo foi a inclusão de um teste seletivo, através de uma condição, que determina qual ou quais ações serão executadas (note que anteriormente, no Algoritmo 1.1, todas as ações eram executadas), dependendo do resultado da inspeção da condição resultar em verdadeiro ou falso. Esse algoritmo está correto, visto que atinge seu objetivo, porém, pode ser melhorado, uma vez que buscamos uma escada e uma lâmpada sem saber se serão necessárias. Mudemos então o teste condicional se a lâmpada não acender para o início da seqüência de ações:

Algoritmo 1.3

· acionar o interruptor;
· se a lâmpada não acender, então
   . pegar uma escada;
   . posicionar a escada embaixo da lâmpada;
   . buscar uma lâmpada nova;
   . acionar o interruptor;
   . subir na escada;
   . retirar a lâmpada queimada;
   . colocar a lâmpada nova.

Observe que, agora, a ação acionar o interruptor é a primeira do algoritmo e a condição lâmpada não acender já é avaliada. Neste caso, pegar urna escada até colocar a lâmpada nova dependem da lâmpada estar efetivamente queimada. Há muitas form de resolver um problema, afinal cada pessoa pensa e age de maneira diferente, cada indivíduo tem uma heurística própria. Isto significa que, para este mesmo problema de trocar lâmpadas, poderíamos ter diversas soluções diferentes e corretas (se atingissem o resultado desejado de efetuar a troca), portanto, o bom senso e a prática de lógica de programação é que indicarão qual a solução mais adequada, que com menos esforço e maior objetividade produz o resultado almejado. A solução apresentada no Algoritmo 1.3 é aparentemente adequada, porém, não prevê a possibilidade de a lâmpada nova não funcionar e, portanto, não atingir o objetivo nesta situação específica. Podemos fazer um refinamento, uma melhoria no algoritmo, de tal modo que se troque a lâmpada diversas vezes, se necessário, até que funcione. Uma solução seria:

Algoritmo 1.4

· acionar o interruptor;
· se a lâmpada não acender, então
   . pegar uma escada;
   . posicionar a escada embaixo da lâmpada; . buscar uma lâmpada nova;
   . acionar o interruptor;
   . subir na escada;
   . retirar a lâmpada queimada;
   . colocar a lâmpada nova;
   . se a lâmpada não acender, então
      . retirar a lâmpada queimada;
      . colocar outra lâmpada nova;
      . se a lâmpada não acender, então
         · retirar a lâmpada queimada;
         · colocar outra lâmpada nova;
         · se a lâmpada não acender, então
            . retirar a lâmpada queimada;
            . colocar outra lâmpada nova;
                ....
                Até quando?

Notamos que o Algoritmo 1.4 não está terminado, falta especificar até quando será feito o teste da lâmpada. As ações cessarão quando conseguirmos colocar uma lâmpada que acenda; caso contrário, ficaremos testando indefinidamente (note que o interruptor continua acionado!). Esta solução está mais próxima do objetivo, pois garante que a lâmpada acenda novamente, ou melhor, que seja trocada com êxito, porém, temos o problema de não saber o número exato de testes das lâmpadas. Observemos que o teste da lâmpada nova é efetuado por um mesmo conjunto de ações:

· se a lâmpada não acender, então
   . retirar a lâmpada queimada;
   . colocar uma lâmpada nova.

Portanto, em vez de reescrevermos várias vezes este conjunto de ações, podemos alterar o fluxo seqüencial de execução de forma que, após executada a ação colocar outra lâmpada nova voltemos a executar o teste se a lâmpada não acender fazendo com que essas ações sejam executadas o número de vezes necessário sem termos de reescrevê-las. Precisamos, então, expressar essa repetição da ação sem repetir o texto que representa a ação, assim como determinar um limite para tal repetição, com o objetivo de garantir uma condição de parada, ou seja, que seja cessada a atividade de testar a lâmpada nova quando esta já estiver acesa. Uma solução seria:

· enquanto a lâmpada não acender, faça
. retirar a lâmpada queimada;
. colocar uma lâmpada nova;

A condição lâmpada não acender permaneceu, e estabelecemos um fluxo repetitivo que será finalizado assim que a condição de parada for falsa, ou seja, assim que a lâmpada acender. Percebemos que o número de repetições é indefinido, porém é finito, e que depende apenas da condição estabelecida, o que leva a repetir as ações até alcançar o objetivo: trocar a lâmpada queimada por uma que funcione. Este novo algoritmo ficaria:
Algoritmo 1.5

· acionar o interruptor;
· se a lâmpada não acender, então
   . pegar uma escada;
   . posicionar a escada embaixo da lâmpada;
   . buscar uma lâmpada nova;
   . acionar o interruptor;
   . subir na escada;
   . retirar a lâmpada queimada;
   . colocar uma lâmpada nova;
   . enquanto a lâmpada não acender, faça
     . retirar a lâmpada queimada;
     . colocar uma lâmpada nova;

Até agora estamos efetuando a troca de uma única lâmpada na verdade, estamos testando um soquete (acionado por um interruptor), trocando tantas lâmpadas quantas forem necessárias para assegurar que o conjunto funcione. O que faríamos se tivéssemos mais soquetes a testar, por exemplo, 10 soquetes? A solução aparentemente mais óbvia seria repetir o algoritmo de uma única lâmpada para os 10 soquetes existentes, ficando algo como:

Algoritmo 1.6

· acionar o interruptor do primeiro soquete
· se a lâmpada não acender, então
   . pegar uma escada;
   . posicionar a escada embaixo da lâmpada;
   . buscar uma lâmpada nova;
   . acionar o interruptor;
   . subir na escada;
   . retirar a lâmpada queimada;
   . colocar uma lâmpada nova;
   . enquanto a lâmpada não acender, faça
      . retirar a lâmpada queimada;
      . colocar uma lâmpada nova;
      · acionar o interruptor do segundo soquete;
      · se a lâmpada não acender, então
         . pegar uma escada;
         . posicionar a escada embaixo da lâmpada;
              ....
         · acionar o interruptor do terceiro soquete;
         · se a lâmpada não acender, então
                 ....
            · acionar o interruptor do quarto soquete;
                  ...
               · acionar o interruptor do décimo soquete;
                  ...

Observamos que o Algoritmo 1.6 é apenas um conjunto de 10 repetições do Algoritmo 1.5. uma vez para cada soquete, havendo a repetição de um mesmo conjunto de ações por um número definido de vezes: 10. Como o conjunto de ações que foram repetidas é exatamente igual, poderíamos alterar o fluxo seqüencial de execução de modo a fazer com que este voltasse a executar o conjunto de ações relativas a um único soquete (Algoritmo 1.5) tantas vezes quantas fossem desejadas. Uma solução para 10 soquetes seria:

Algoritmo 1.7

· ir até o interruptor do primeiro soquete;
· enquanto a quantidade de soquetes testados for menor que dez, faça
. acionar o interruptor;
. se a lâmpada não acender, então
   . pegar uma escada;
   . posicionar a escada embaixo da lâmpada;
   . buscar uma lâmpada nova;
   . acionar o interruptor;
   . subir na escada;
   . retirar a lâmpada queimada;
   . colocar uma lâmpada nova;
   . enquanto a lâmpada não acender, faça
      · retirar a lâmpada queimada;
      · colocar uma lâmpada nova;
      · ir até o interruptor do próximo soquete;

Quando a condição quantidade de soquetes testados for menor que dez for verdadeira, as ações responsáveis pela troca ou não de um único soquete serão executadas. Caso a condição de parada seja falsa, ou seja, todos os dez soquetes já tiverem sido testados, nada mais será executado. Todo o exemplo foi desenvolvido a partir do problema de descrevermos os passos necessários para efetuar a troca de uma lâmpada, ou seja, construir um algoritmo para esse fim. Inicialmente, tínhamos um pequeno conjunto de ações que deveriam ser executadas, todas passo a passo, uma após a outra, compondo uma ordem seqüencial de execução, a estrutura seqüencial. Notamos que nem sempre todas as ações previstas deveriam ser executadas. Tal circunstância sugeriu que um determinado conjunto de ações fosse evitado, selecionando conforme o resultado de uma determinada condição. Construímos assim, uma estrutura seletiva através de um teste condicional que permitia ou não que o fluxo de execução passasse por um determinado conjunto de ações. Quando nos deparamos com a inviabilidade da aplicação da estrutura de seleção para a verificação do êxito na troca da lâmpada, precisamos repetir um mesmo trecho.