ATENÇÃO! Este documento é apenas um rascunho da obra em desenvolvimento! CAP241 – Computação Aplicada I Parte 1. Introdução a Lógica de Programação Dr. Gilberto Ribeiro de Queiroz Notas de Aula URL do documento original: <http://www.dpi.inpe.br/~gribeiro> INPE São José dos Campos 2016 LISTA DE FIGURAS Pág. Figura 1.1 – Algoritmo MDC em Linguagem Natural. .................................................................................8 Figura 1.2 – Algoritmo que escreve a sequência de Fibonacci. ..................................................................11 Figura 1.3 – Segunda versão do algoritmo de Fibonacci.............................................................................12 Figura 1.4 – Primeiro refinamento do algoritmo de Fibonacci. ..................................................................12 Figura 1.5 – Versão final do algoritmo de Fibonacci após os refinamentos. ..............................................13 Figura 1.6 – Algoritmo de Fibonacci na forma de diagrama de blocos. .....................................................16 Figura 1.7 – Simbologia em um diagrama de blocos. .................................................................................17 Figura 1.8 – Algoritmo de Fibonacci na forma de diagrama de blocos. .....................................................18 Figura 1.9 – Exemplo de instrução MIPS instruções. .................................................................................19 Figura 1.10 – Exemplo de instrução MIPS instruções. ...............................................................................19 Figura 1.13 - Programa em Assembler: x = 9 + 71. ....................................................................................20 Figura 1.14 – Algoritmo MDC expresso em C++. ......................................................................................21 Figura 1.15 – Fluxo de compilação de um programa C++. .........................................................................22 Figura 1.16 – Exemplo de código em ALGOL 60. .....................................................................................24 Figura 1.17 –Algumas Linguagens de Programação. ..................................................................................25 Figura 1.18 – Relação entre variáveis, memória e representação do tipo de dados. ...................................28 Figura 1.19 – Declaração da variável v. ......................................................................................................29 Figura 1.20 – (a) Equação geral de uma reta; (b) Reta definida pela expressão. ........................................31 Figura 1.21 – Exemplos de comandos de atribuição. ..................................................................................36 Figura 1.22 – Exemplos de chamada de função. .........................................................................................37 Figura 1.23 – Uso de comentários ...............................................................................................................39 Figura 1.24 – Comando condicional simples. .............................................................................................41 Figura 1.25 – Fluxo de execução de um programa com a presença de um único comando if. ...................42 Figura 1.26 – Exemplo de teste condicional simples. .................................................................................42 Figura 1.27 – Comando condicional simples com um bloco de comandos.................................................43 Figura 1.28 – Exemplo de teste condicional com blocos de comandos. .....................................................43 Figura 1.29 – Estrutura condicional composta. ...........................................................................................44 Figura 1.30 – Ideia da estrutura condicional composta. ..............................................................................44 Figura 1.31 – Exemplo de uso da estrutura condicional composta. ............................................................45 Figura 1.32 – Comandos condicionais aninhados. ......................................................................................46 Figura 1.33 – Comandos condicionais aninhados. ......................................................................................47 Figura 1.34 – Somátórios. ............................................................................................................................48 Figura 1.35 – Laços de repetição com interrupção no início .......................................................................48 Figura 1.36 – Somatório dos números entre [1:5]. ......................................................................................49 Figura 1.37 – Laços de repetição com interrupção no final. .......................................................................50 Figura 1.38 – Somatório dos números entre [1:5]. ......................................................................................51 Figura 1.39 – Laços de repetição com variável de controle. .......................................................................52 Figura 1.40 – Somatório dos números entre [1:5] com laço do tipo “para”. ...............................................52 Figura 1.41 – Laços de repetição com variável de controle sobre um conjunto. ........................................53 Figura 1.42 – Esquema de um vetor de números inteiros............................................................................55 Figura 1.43 – Calculando o número de alunos com nota acima da média. .................................................56 Figura 1.44 – Tabela de notas de alunos......................................................................................................57 Figura 1.45 – Procedimento de transposição de uma matriz. ......................................................................59 Figura 1.46 – Função de transposição de uma matriz. ................................................................................60 Figura 1.47 – Função iterativa para geração do n-th termo da série de Fibonacci. .....................................61 Figura 1.48 – Função recursiva para geração do n-th termo da série de Fibonacci. ...................................62 Figura 1.49 – Pontos de intersecção entre dois conjunto de segmentos de reta. .........................................63 Figura 1.50 – Algoritmo para computação dos pontos de intersecção entre dois conjuntos de segmentos de reta. ..............................................................................................................................................................64 ii Figura 1.51 – Implementação do algoritmo para computação dos pontos de intersecção entre segmentos de dois conjuntos apresentado na Figura 3.2 em C++. ................................................................................65 iii LISTA DE TABELAS Pág. Tabela 1.1 – Operadores aritméticos. ..........................................................................................................30 Tabela 1.2 – Operadores relacionais. ...........................................................................................................32 Tabela 1.3 – Operadores lógicos. ................................................................................................................34 Tabela 1.4 – Precedência dos operadores. ...................................................................................................35 Tabela 1.5 – Funções sobre uma string. ......................................................................................................38 iv SUMÁRIO Pág. 1 Introdução à Lógica de Programação .....................................................................................................7 1.1. Algoritmos ..........................................................................................................................................8 1.2. Representação de Algoritmos ...........................................................................................................10 1.2.1. Pseudocódigo .................................................................................................................................10 1.2.2. Diagrama de Blocos .......................................................................................................................14 1.2.3. Diagrama de Chapin ......................................................................................................................18 1.2.4. Linguagens de Programação ..........................................................................................................19 1.2.5. Considerações sobre estilo de algoritmos ......................................................................................25 1.3. Técnicas Básicas para Desenvolvimento de Algoritmos ..................................................................27 1.3.1. Constantes ......................................................................................................................................27 1.4. Variáveis ...........................................................................................................................................28 1.5. Declaração de Variáveis ...................................................................................................................29 1.6. Expressões.........................................................................................................................................30 1.6.1. Expressões e Operações Aritméticas .............................................................................................30 1.6.2. Expressões Lógicas ........................................................................................................................32 1.6.3. Precedência dos Operadores ..........................................................................................................35 1.7. Atribuição .........................................................................................................................................36 1.8. Invocando Funções ...........................................................................................................................37 1.9. Comentários ......................................................................................................................................39 1.10. Comandos de Entrada e Saída (E/S) ...............................................................................................40 1.11. Estrutura Condicional .....................................................................................................................41 1.11.1. Esrutura condicional simples .......................................................................................................41 1.11.2. Estrutura condicional composta ...................................................................................................44 1.11.3. Comandos condicionais aninhados (ou encadeados) ...................................................................46 1.12. Repetições .......................................................................................................................................48 1.12.1. Laços de repetição com interrupção no início .............................................................................48 1.12.2. Laços de repetição com interrupção ao final da primeira iteração ..............................................50 1.12.3. Repetição com variável de controle .............................................................................................52 1.12.4. Interrompendo o laço de repetição ...............................................................................................53 1.12.5. Desviando a sequência de laço de repetição ................................................................................53 1.13. Vetores ............................................................................................................................................54 1.14. Matrizes (Arrays) ............................................................................................................................57 1.15. Estruturas ou Registros ...................................................................................................................58 1.16. Criando Procedimentos e Funções ..................................................................................................59 1.17. Recursividade ..................................................................................................................................61 1.18. Modelo de Programação .................................................................................................................63 v 1.19. Instruções sobre ambiente de programação ....................................................................................68 1.20. Considerações Finais do Capítulo ...................................................................................................69 1.21. Referências Bibliográficas do Capítulo ..........................................................................................70 1.22. Exercícios Propostos .......................................................................................................................72 1.23. Exercícios Especiais........................................................................................................................74 vi 1 Introdução à Lógica de Programação “Programconstructionconsistsofasequenceofrefinementsteps.” Niklaus Wirth (Wirth, 1971b) Um computador é uma ferramenta para solução de problemas envolvendo processamento de dados. Um programa é formado por uma sequência de instruções que diz ao computador como realizar uma determinada tarefa. Essas instruções dizem, por exemplo, como o programa deve interagir com o usuário, como ele deve utilizar o hardware, e como operar os dados para realizar computações. Quando um computador segue as instruções de um programa, dizemos que ele está executando o programa. Os programas são escritos por programadores. A atividade de programar consiste na habilidade de dizer ao computador como ele deve realizar um determinado trabalho. Para isso, precisamos de uma forma de expressar nossos programas. As Linguagens de Programação servem a esse propósito. Porém, para conseguir escrever programas em alguma linguagem é preciso conhecer um pouco de lógica de programação. Este capítulo apresenta uma introdução aos principais conceitos e técnicas envolvidos na atividade de programação de computadores. 1.1. Algoritmos O termo algoritmo é usado na Ciência da Computação para descrever métodos finitos, determinísticos e efetivos, que são adequados à implementação na forma de programas de computador (Sedgewick e Wayne, 2011). Para entender melhor a definição acima, vamos descrever um algoritmo para computar o máximo divisor comum entre dois números inteiros positivos quaisquer: ComputaroMDCentredoisnúmerosinteirosnãonegativos,peq: • Seqfor0,oresultadoseráp; • Casocontrário,obterorestodadivisãorentrepeq. • OMDCentreqerseráoresultado. Figura 1.1 – Algoritmo MDC em Linguagem Natural. Fonte: Adaptado de Sedgewick e Wayne (2011). Observe que o algoritmo acima é formado pelo encadeamento de uma série de instruções ou ações, que seguidas nos levarão à solucionar nosso problema: computar o MDC entre dois números inteiros positivos. Para verificar nosso algoritmo, vamos tomar como valores de p e q, respectivamente, os números 21 e 4. Nesse caso nosso algoritmo se comporta da seguinte forma: Inicialmente, verificamos se o valor de q é maior do que 0. Neste caso, como o valor de q é 4, vamos a próxima instrução. Calculamos o resto da divisão entre 21 e 4 obtemos o valor 1. Usando nosso algoritmo novamente, temos agora que computar o MDC(4, 1). Verificamos agora se o valor de q é maior do que 0. Neste caso, como o valor de q é 1, vamos a próxima instrução. Calculando o resto da divisão entre 4 e 1 obtemos o valor 0. Usando nosso algoritmo novamente, temos agora que computar o MDC(1, 0). Agora, na primeira instrução, o valor de q é 0. Neste caso, o resultado é então o valor 1. Agora, vamos tomar como valores de p e q, respectivamente, os números 6 e 12. Nesse caso nosso algoritmo se comporta da seguinte forma: Inicialmente, verificamos se o valor de q é maior do que 0. Neste caso, como o valor de q é 12, vamos a próxima instrução. Calculamos o resto da divisão entre 6 e 12 obtemos o valor 6. Usando nosso algoritmo novamente, temos agora que computar o MDC(6, 6). Verificamos agora se o valor de q é maior do que 0. Neste caso, como o valor de q é 6, vamos a próxima instrução. Calculando o resto da divisão entre 6 e 6 obtemos o valor 0. Usando nosso algoritmo novamente, temos agora que computar o MDC(6, 0). Agora, na primeira instrução, o valor de q é 0. Neste caso, o resultado é então o valor 6. 1.2. Representação de Algoritmos Na prática, podemos representar um algoritmo de diversas formas. 1.2.1. Pseudocódigo A forma mais usual de representar uma algoritmo é através de um procedimento descrito em linguagem natural, que utiliza algumas convenções especiais, como estruturas de controle e palavras reservadas para representar comandos e operações. Esta forma é conhecida como Português Estruturado, Portugol ou pseudocódigo. Para entender como é esta representação, vamos escrever um algoritmo para computar a série de Fibonacci para valores inferiores a um certo valor v1. A sequência de Fibonacci é definida como tendo os dois primeiros termos o valor 1 e cada termo seguinte sendo igual à soma dos dois termos imediatamente anteriores. Portanto, a série de Fibonacci para valores inferiores a 100 é a seguinte: 1123581321345589 1 Para informações sobre algoritmos para computação da séries de Fibonacci veja a Wikibooks. A Figura 1.2 mostra uma primeira versão desse algoritmo: algoritmo leiav escrevasérie-fibonacci(v) fimalgoritmo Figura 1.2 – Algoritmo que escreve a sequência de Fibonacci. Fonte: Adaptada de Farrer et al. (1989) No algoritmo acima, as palavras algoritmo e fim algoritmo foram utilizadas para delimitar o início e o fim das instruções que compõem o algoritmo. Esta primeira versão é formada por duas ações (ou comandos): • A primeira realiza a leitura do valor V, para o qual iremos computar a séries de Fibonacci. Repare que utilizamos a palavra leia para indicar um comando, isto é, uma ação, que realiza a leitura de um valor numérico a partir de um dispositivo de entrada, como o teclado. • A segunda ação, escreve, em um dispositivo de saída, como a tela, a sequência de Fibonacci, supondo a existência de um comando de escrita chamado escreva, e um comando em que computa a série de Fibonacci. As palavras em azul e sublinhadas são chamadas de palavras reservadas, e fazem parte da nossa gramática para escrita de pseudocódigos. Obviamente, que o algoritmo acima ainda está longe de servir ao propósito que queremos, que é expressar uma forma geral de como computar a série de Fibonacci. Na construção de algoritmos, é comum utilizarmos uma técnica conhecida como refinamentos sucessivos (Wirth, 1971b): começamos o algoritmo com uma descrição de mais alto nível, contendo um conjunto de comandos que serão expandidos à medida que vamos detalhando esse algoritmo. Utilizando essa abordagem, podemos reescrever o algoritmo da Figura 1.2 da seguinte forma: algoritmo leiav processeos2primeirostermos processeostermosrestantes fimalgoritmo Figura 1.3 – Segunda versão do algoritmo de Fibonacci. Fonte: Adaptada de Farrer et al. (1989) Aplicando esta técnica ao comando “processe os 2 primeiros termos”, temos o algoritmo mostrado na Figura 1.4. algoritmo leiav atribua1aoprimeirotermo seeleformenorquev entãoescreva-o fimse atribua1aosegundotermo seeleformenorquev entãoescreva-o fimse processeostermosrestantes fimalgoritmo Figura 1.4 – Primeiro refinamento do algoritmo de Fibonacci. Fonte: Adaptada de Farrer et al. (1989) Na versão da Figura 1.4 já podemos perceber a inclusão de uma estrutura condicional, uma das estruturas de controle que determina a ordem em que os comandos devem ser executados. Agora, refinando o comando “processeostermosrestantes”, temos a versão final do algoritmo, conforme apresentado na Figura 1.5. algoritmo leiav atribua1aoprimeirotermo seeleformenorquev entãoescreva-o fimse atribua1aosegundotermo seeleformenorquev entãoescreva-o fimse repita calculeonovotermosomandoosdoisanteriores senovotermoformaiorouigualav entãointerrompa fimse escrevanovotermo fimrepita fimalgoritmo Figura 1.5 – Versão final do algoritmo de Fibonacci após os refinamentos. Fonte: Adaptada de Farrer et al. (1989) Nessa versão final, aparece uma segunda estrutura de controle, a estrutura de repetição, que faz com que os comandos dentro dela sejam executados repetidamente até que uma certa condição interrompa a repetição. Vamos ver como funciona nosso algoritmo para uma entrada de valor 100: Primeiro termo = 1 Como 1 < 100, escreve o valor 1 Segundo termo = 1 Como 1 < 100, escreve o valor 1 Calcula o novo termo, como sendo 1 + 1 = 2 Como 2 < 100, escreve o valor 2 Calcula o novo termo, como sendo 1 + 2 = 3 Como 3 < 100, escreve o valor 3 Calcula o novo termo, como sendo 2 + 3 = 5 Como 5 < 100, escreve o valor 5 Calcula o novo termo, como sendo 3 + 5 = 8 Como 8 < 100, escreve o valor 8 Calcula o novo termo, como sendo 5 + 8 = 13 Como 13 < 100, escreve o valor 13 Calcula o novo termo, como sendo 8 + 13 = 21 Como 21 < 100, escreve o valor 21 Calcula o novo termo, como sendo 13 + 21 = 34 Como 34 < 100, escreve o valor 34 Calcula o novo termo, como sendo 21 + 34 = 55 Como 55 < 100, escreve o valor 55 Calcula o novo termo, como sendo 34 + 55 = 89 Como 89 < 100, escreve o valor 89 Calcula o novo termo, como sendo 55 + 89 = 144 Como 144 não é menor que 100, interrompe Resultado final: 1123581321345589. 1.2.2. Diagrama de Blocos Outra forma que temos para representação de um algoritmo é através da representação gráfica em diagrama de blocos. Este tipo de diagrama é capaz de apresentar o fluxo de comandos de um algoritmo. O algoritmo da Figura 1.5, para computação da sequência de Fibonacci, é apresentado na Figura 1.6 na forma de um diagrama de blocos equivalente. Figura 1.6 – Algoritmo de Fibonacci na forma de diagrama de blocos. Fonte: Adaptada de Farrer et al. (1989) A simbologia do diagrama acima é mostrada na Figura 1.7. Terminal: usado para indicar início ou fim do algoritmo. E/S Dados: usado para indicar entrada ou saída de dados de forma genérica. Fluxo de comandos: indica qual a próxima instrução a ser executada. Processamento: usado para comandos a serem executados. indicar cálculos ou Comando de Decisão: usado para indicar desvios no fluxo de instruções do algoritmo dependendo da avaliação de uma certa condição. Saída em Vídeo: saída de dados no monitor. Figura 1.7 – Simbologia em um diagrama de blocos. 1.2.3. Diagrama de Chapin O diagrama de Chapin ou Nassi-Sneider é outra forma de expressão de algoritmos que utiliza quadros para fornecer uma visão hierárquica e mais estruturada de um algoritmo. A Figura 1.8 apresenta o algoritmo da Figura 1.5, para computação da sequência de Fibonacci, utilizando a notação de digrama de Chapin. Figura 1.8 – Algoritmo de Fibonacci na forma de diagrama de blocos. Fonte: Adaptada de Farrer et al. (1989) 1.2.4. Linguagens de Programação Todo processador ou família de processadores, possui seu próprio conjunto de instruções e operações. Essas instruções e operações são representadas por padrões de bits que correspondem a diferentes comandos de máquina, como carga de dados nos registradores, adição, subtração, multiplicação ou divisão de valores de dois registradores, ou instruções para transferência do fluxo de execução de um programa. Essas instruções permitem que o computador realize as atividades mais básicas possíveis. Veja o exemplo do formato das instruções MIPS (32-bits): Tipo R I J 31 opcode(6-bits) opcode(6-bits) opcode(6-bits) rs(5) rs(5) rt(5) rt(5) 0 shamt(5) funct(6) immediate(16) address(26) rd(5) Figura 1.9 – Exemplo de instrução MIPS2 instruções. Fonte: Adaptada de Wikipedia. Uma operação para adicionar o conteúdo dos registradores 1 e 2 e armazenamento no registrador 6 tem a seguinte forma: op 000000 rs 00001 rt 00010 rd 00110 shamt 00000 funct 100000 Figura 1.10 – Exemplo de instrução MIPS3 instruções. Fonte: Adaptada de Wikipedia. 2 3 https://en.wikipedia.org/wiki/MIPS_instruction_set https://en.wikipedia.org/wiki/MIPS_instruction_set Um programa, ou executável, é formado por um conjunto dessas instruções numa determinada ordem para realizar tarefas. Suponha por exemplo que você esteja colocando para rodar sua IDE de programação favorita. O Sistema Operacional irá copiar os bytes contendo as instruções do seu programa para a memória RAM do seu computador, e a CPU irá começar a executar seu programa a partir da primeira instrução. O grande problema com o código de máquina é que ele é bastante inconveniente para a escrita de programas diretamente por seres humanos. Por conta disso, foram desenvolvidas as Linguagens de Montagem (Assembly), que usam símbolos chamados mnemonic4 para representar cada instrução ou operação (load, store, jump). O programa responsável por converter o programa escrito na linguagem de montagem para o código de máquina é chamado de montador ou assembler. Uma linguagem de montagem contém o mesmo número de instruções que a linguagem de máquina associada, sendo porem mais legível. LDA#9;loadsthenumber9intotheaccumulator ADD#71;addsthenumber71tothecontentsoftheaccumulator=80 STO34;savetheaccumulatorresulttothememoryaddress34 Figura 1.13 - Programa em Assembler: x = 9 + 71. Fonte: Adaptada de Wikibooks. Escrever um programa diretamente em linguagem de montagem não é uma tarefa simples, pelo menos nos dias de hoje, onde temos grandes sistemas para construir. Além de ser de difícil compreensão, os processadores possuem um conjunto de instruções bem reduzidos e nossas necessidades ao criarmos programas são bem maiores. 4 https://en.wikipedia.org/wiki/Mnemonic Por conta dessas dificuldades, foram criadas as linguagens de alto 5 nível , nos anos 50, para fornecer abstrações de mais alto nível do que as instruções de máquina. Essas linguagens são mais próximas também das linguagens naturais. Portanto, linguagem de alto nível é outra forma que temos para escrevermos nossos algoritmos, como mostrado no código da Figura 1.14. unsignedintMDC(unsignedintp,unsignedintq) { if(q==0) returnp; unsignedintr=p%q; returnMDC(q,r); } Figura 1.14 – Algoritmo MDC expresso em C++. Fonte: Adaptada de Sedgewick e Wayne (2011). 5 https://en.wikipedia.org/wiki/High-level_programming_language As linguagens de alto nível também foram concebidas para evitar que um dado programa tivesse que ser reescrito toda vez que fosse ser executado em um arquitetura diferente. Para isso, foram criados os compiladores, que são responsáveis por traduzir o código na linguagem de alto nível em uma linguagem de montagem que posteriormente pode ser transformada em linguagem de máquina por um assembler. A Figura 1.15 mostra este fluxo para um compilador C++ (GNU g++). Figura 1.15 – Fluxo de compilação de um programa C++. Em 1953, John Backus desenvolveu uma linguagem chamada Speedcoding (Backus, 1954), considerada a primeira linguagem de alto nível, para ajudar no desenvolvimento de software no computador IBM 701. Ao contrário das linguagens de máquina da época, Speedcoding introduzia uma linguagem simbólica e tinha como objetivo ter uma expressividade mais próxima à linguagem natural. Essa linguagem introduziu suporte a aritmética em ponto flutuante. Tratava-se de uma linguagem interpretada, cujo desempenho estava longe do ideal: seu desempenho era de 10 a 20 vezes pior do que de um código escrito diretamente em código de máquina. O grande problema identificado por Backus na época, e que motivou o desenvolvimento das linguagens de programação de alto nível, era que grande parte dos custos e do tempo para solucionar problemas científicos e de engenharia com a ajuda de grandes computadores, era gasto, respectivamente, com a preparação do problema (2/3 custo) e com a escrita e debug do programa criado (90% do tempo) (Backus et al., 1957). Somente com o desenvolvimento da Linguagem FORTRAN (Backus et al., 1957) para o IBM 704, é que as linguagens de alto nível passaram a ganhar ampla aceitação. O IBM 704 introduziu um hardware com capacidade de realização de aritmética em ponto flutuante. FORTRAN possibilitou que os programadores especificassem procedimentos numéricos usando uma linguagem próxima à da matemática, e ainda assim ser capaz de gerar código de máquina eficiente no IBM 704. Essa foi a maneira encontrada por Backus para reduzir a codificação e atividades de debug para menos de 1/5 do trabalho. Na sequência, vieram as Linguagens ALGOL, acrônimo de ALGOrithmic Language. A primeira versão, ALGOL 58, que inicialmente seria batizada de IAL (International Algebraic Language), representa um marco histórico por ter sido desenvolvida em conjunto por cientistas americanos e europeus na tentativa de criar uma linguagem padronizada. ALGOL foi utilizada, principalmente, na comunidade científica para especificar os algoritmos apresentados em revistas e jornais, como a ACM. Essa família de linguagens formalizou diversos recursos disponíveis nas linguagens atuais, entre elas, o uso de estrutura de comandos em blocos, comandos condicionais, laços, procedimentos e funções, formas de passagem de parâmetro (valor, referência, nome), funções aninhadas, escopo léxico6. A Figura 1.16 mostra um exemplo de código em ALGO 60. procedureAbsmax(a)Size:(n,m)Result:(y)Subscripts:(i,k); valuen,m;arraya;integern,m,i,k;realy; commentTheabsolutegreatestelementofthematrixa,ofsizenbym, istransferredtoy,andthesubscriptsofthiselementtoiandk; begin integerp,q; y:=0;i:=k:=1; forp:=1step1untilndo forq:=1step1untilmdo ifabs(a[p,q])>ythen begin y:=abs(a[p,q]); i:=p;k:=q end endAbsmax Figura 1.16 – Exemplo de código em ALGOL 60. Fonte: Wikipedia 6 https://en.wikipedia.org/wiki/Scope_(computer_science)#Lexical_scoping Por conta da especificação da Linguagem ALGOL 58, Backus criou a notação BNF, refinada mais tarde por Peter Naur durante a especificação de ALGOL 60. ALGOL influenciou o desenvolvimento de diversas outras linguagens, como mostrado na Figura 1.17. Esta figura mostra apenas uma pequena lista das linguagens de programação que foram criadas ou que existem atualmente. Figura 1.17 –Algumas Linguagens de Programação. De tudo que conversamos nesta seção, o importante a ser compreendido é que um programa escrito em uma linguagem de programação é apenas uma forma de expressar um dado algoritmo. 1.2.5. Considerações sobre estilo de algoritmos Nossa noção de algoritmo será baseada em conceitos presentes nas linguagens imperativas7, isto é, linguagens cuja propriedade principal é a existência de comandos que podem produzir efeito colateral em um programa, como a alteração do valor de uma variável. Isto significa que alguns comandos podem alterar o estado da aplicação. As linguagens de programação declarativas, por outro lado, descrevem a lógica sem a necessidade de uso de instruções de controle de fluxo, em teoria, focando mais nos aspectos de especificação de como um programa deve atingir seus resultados. Esta discussão ficará mais clara quando falarmos dos paradigmas de programação e como as linguagens suportam esses paradigmas. 7 https://en.wikipedia.org/wiki/Imperative_programming 1.3. Técnicas Básicas para Desenvolvimento de Algoritmos Nesta seção iremos detalhar os principais conceitos envolvidos na escrita de um algoritmo. 1.3.1. Constantes Uma constante é um valor fixo que não se modifica ao longo de todo o tempo de execução de um programa. Em linguagens de programação, como C++, também chamamos essas constantes de literais. Uma constante pode ser um número, um valor lógico ou uma sequência de caracteres (string). As constantes numéricas são usadas para representar números inteiros ou reais, positivos ou negativos: • 9,-21,3.14,-2.73 As duas constantes lógicas possíveis são: verdadeiro efalso. As constantes string representam sequências de caracteres, formadas por letras e dígitos. Em geral representamos a sequência entre aspas duplas: • "Gilberto Ribeiro de Queiroz", "30/12/1976", "7654", "VERDADEIRO" 1.4. Variáveis Um programa, além de manipular constantes literais também manipula o que chamamos de variáveis. Cada variável corresponde a uma posição de memória cujo conteúdo pode variar ao longo do tempo de execução de um programa (Figura 1.18). Uma variável possui um nome usado como identificador e, em geral, é associada com um tipo de dado. O tipo de dados define os possíveis valores que podem estar associado a uma variável8. Figura 1.18 – Relação entre variáveis, memória e representação do tipo de dados. Cada linguagem de programação possui suas regras para nomenclatura das variáveis, inclusive para dizer se há diferenças entre variáveis com letras maiúsculas e minúsculas. 8 Tipagem é um conceito clássico de linguagens fortemente tipadas. Atualmente, existem inúmeras linguagens fracamente tipadas, em que uma variável pode alterar o tipo de valor associado. 1.5. Declaração de Variáveis Os nossos pseudocódigos deverão conter a declaração das variáveis, isto é, antes de usar uma variável, devemos especificar seu nome e o tipo de dados associado a ela. A declaração de variáveis terá a seguinte sintaxe: declareidentificadorestipo Retomando o algoritmo de Fibonacci (Figura 1.5), teríamos a seguinte declaração da variável v: algoritmo declarevnumérico leiav ... fimalgoritmo Figura 1.19 – Declaração da variável v. Após a declaração de uma variável, qualquer referencia a ela, ou melhor, ao seu identificador, implica na referencia ao conteúdo da posição de memória associada a ela. 1.6. Expressões Através da combinação das operações e operandos podemos criar expressões, como as expressões matemáticas convencionais. Uma expressão deve resultar em um valor de um certo tipo. 1.6.1. Expressões e Operações Aritméticas As operações aritméticas disponíveis para os tipos numéricos são muito semelhantes às que usamos na matemática. A Tabela 1.1 mostra os operadores aritméticos disponíveis na nossa linguagem (pseudocódigo). Tabela 1.1 – Operadores aritméticos. Operadores Expressões Resultado + (adição) 5.1+8 13.1 - (subtração) 3-2 1 * (multiplicação) 3*4.1 12.3 /(divisão) 6/4 1.5 n x (radiciação) xn(potenciação) 3 27 3 33 27 algorítmica Esses operadores podem ser combinados para escrita de expressões, como a forma geral de uma equação de reta: Ax+By+C=0 (equação geral de uma reta) A equação matemática acima pode ser reescrita como uma expressão envolvendo constantes e variáveis, como mostrado abaixo: 3*x-2*y–6 Figura 1.20 – (a) Equação geral de uma reta; (b) Reta definida pela expressão. Veja que na expressão, não é válido omitir o operador ‘*’ (multiplicação). Além disso, quando uma expressão contém mais de dois operadores, como no caso da equação acima, a ordem em que eles são aplicados é significante e, por isso, existe uma convenção bem definida da precedência de cada operador. No caso das operações aritméticas, a prioridade é a seguinte: 1. potenciação e radiciação; 2. multiplicação e divisão; 3. adição e subtração. Assim como na matemática, podemos usar parênteses para controlar essa prioridade. Além das regras de precedência, temos também a ordem de aplicação. Várias das expressões que montamos são infixas, isto é, temos um literal ou variável ou expressão, seguido do operador, seguido por outro literal ou variável ou expressão. Nas linguagens de programação, vários operadores possuem uma associatividade da esquerda para direita. 1.6.2. Expressões Lógicas Veremos que esse tipo de expressão é essencial para controlar o fluxo de execução de nossos programas, nos comandos condicionais e testes condicionais dos laços de repetição. 1.6.2.1. Operadores Relacionais Os operadores relacionais ou operadores de comparação permitem comparar dois valores e produzir um valor booleano (ou lógico) como resultado. Esses operadores são mostrados na Tabela 1.2. Tabela 1.2 – Operadores relacionais. Operadores Expressões 5==4 == (igual) 5==5 5!=4 != (diferente) 5!=5 5<4 < (menor que) 4<5 5<=4 <=(menor ou igual) 4<=5 5>4 > (maior que) 4>5 5>=4 >= (maior ou igual) 4>=5 Resultado falso verdadeiro verdadeiro falso falso verdadeiro falso verdadeiro verdadeiro falso verdadeiro falso Retomando nossa equação da reta poderíamos escrever uma expressão da seguinte forma: (3*x-2*y–6)>0 No caso dos operadores relacionais, a prioridade é a seguinte: 1. <, >, <=, >=; 2. ==, != 1.6.2.2. Operadores Lógicos Os operadores lógicos são baseados na álgebra de proposições, retornando o resultado de operações denominadas booleanas. Esses operadores são mostrados na Tabela 1.3. Tabela 1.3 – Operadores lógicos. Operadores não (negação) Valor Expressões Resultado a=verdadeiro; nãoa falso a=falso; nãoa verdadeiro aeb verdadeiro aeb falso aeb falso aeb falso aoub verdadeiro aoub verdadeiro aoub verdadeiro aoub falso a=verdadeiro; b=verdadeiro; a=verdadeiro; e (conjunção) b=falso; a=falso; b=verdadeiro; a=falso; b=falso; a=verdadeiro; b=verdadeiro; a=verdadeiro; ou (disjunção) b=falso; a=falso; b=verdadeiro; a=falso; b=falso; Com esses operadores, podemos construir expressões como: media>7emin(n1,n2)>5 No caso dos operadores lógicos, a prioridade é a seguinte: 1. Negação: não 2. Conjunção: e 3. Disjunção: ou 1.6.3. Precedência dos Operadores Tabela 1.4 – Precedência dos operadores. Nível Operador Descrição 1 não não lógico 2 +, - operadores unários que definem o sinal 3 n x ,xn radiciação, potenciação 4 *, / multiplicação, divisão 5 +, - adição, subtração 6 <, >, <=, >= operadores de comparação 7 ==, != igualdade e desigualdade 8 e e lógico 9 ou ou lógico 1.7. Atribuição A atribuição é um comando que associa um valor de um determinado tipo de dados a uma variável. Essa associação pode também ser o resultado de uma expressão. Portanto, este comando possui a seguinte forma: identificador←expressão Exemplo: algoritmo declarev1numérico declarev2numérico declaresomanumérico v1←10 v2←30 soma←v1+v2 v1←45 v2←76 soma←v1+v2 fimalgoritmo Figura 1.21 – Exemplos de comandos de atribuição. O valor da expressão avaliada deve ser compatível com o tipo de dado da variável na sua declaração. 1.8. Invocando Funções Apenas com o conjunto de operações básicas seria muito difícil expressarmos nossos algoritmos, mesmo em pseudocódigo. Por isso, várias funcionalidades que iremos incluir na escrita dos nossos programas pressupõe a existência de algumas funções e procedimentos auxiliares. Como por exemplo: • sin, cos, log, resto. Estas funcionalidades podem ser incluídas no nosso algoritmo na forma de chamada de uma função (function call), que é uma forma de desviar o fluxo de controle do nosso programa para uma outra parte que irá realizar uma determinada computação e depois irá retornar o fluxo de controle ao ponto onde foi chamada (ou invocada). A chamada de uma função é feita colocando-se o nome da função e a lista de argumentos que será passada à função, para que ela realize sua computação. Por exemplo: algoritmo declarev1numérico declarev2numérico declareresultadonumérico v1←10 v2←30 resultado←sen(v1) resultado←sen(v1+v2) resultado←sen(45) fimalgoritmo Figura 1.22 – Exemplos de chamada de função. Na nossa linguagem de pseudocódigo, não teremos a disposição operadores para string. Ao invés disso, teremos a lista de funções mostradas na Tabela 1.5. Tabela 1.5 – Funções sobre uma string. Operadores Valores Expressões Resultado concat(a,b) a←"nome:" b←"Gilberto" concat(a,b) "nome:Gilberto" sub (s, i, l) a←"Gilberto" sub(a,0,3) "Gil" cumprimento (s) a←"Gilberto" cumprimento (a) 8 1.9. Comentários Comentários são parte importante de qualquer programa, não sendo considerados instruções a serem executadas. Servem apenas ao proposito de documentar o código. algoritmo declarex,ynumérico{coordenadasdeumponto} declareresultadonumérico leiax leiay resultado←3*x-2*y–6{calculaseopontoencontra-sesob} {areta3x-2y–6} fimalgoritmo Figura 1.23 – Uso de comentários 1.10. Comandos de Entrada e Saída (E/S) A sintaxe destes comandos é a seguinte: leialistadevariáveis escrevaexpressão 1.11. Estrutura Condicional As estruturas condicionais ou comandos condicionais permitem alterar a sequência de execução de um programa dependendo do resultado de uma expressão lógica. 1.11.1. Esrutura condicional simples Um único comando se...então permite que nosso programa execute outro comando ou bloco de comandos (Figura 1.24), se uma dada condição for verdadeira, isto é, se a expressão lógica definida pela condição for avaliada como verdadeira. seexpressão-lógica entãocomando(s) Comando(s) executado(s) caso a condição seja verdadeira. fimse comando Figura 1.24 – Comando condicional simples. Na Figura 1.24, os comandos no bloco da palavra chave então só serão executados se a expressão lógica for avaliada como verdadeira. Neste caso, o comando fora da estrutura de controle poderá ser executado após a sequência de comandos delimitada pelo teste condicional, caso nenhum desses comandos termine o programa. Caso a expressão-booleana seja avaliada como falsa, apenas o comando fora da estrutura de controle será executado. A Figura 1.25 ilustra como é o fluxo do nosso programa dependendo da avaliação da expressão lógica do comando se. Figura 1.25 – Fluxo de execução de um programa com a presença de um único comando if. Um exemplo desse tipo de comando condicional é mostrado na Figura 1.26: 01 02 03 04 05 06 07 08 09 declareidadenumérico idade←39; seidade>=40 entãoescreva"nacasados40!" fimse seidade<40 entãoescreva"aindanãochegounos40!" fimse escreva"Qualqueridadeéboa,desdequebemaproveitada!" Figura 1.26 – Exemplo de teste condicional simples. Outra possibilidade, é a estrutura condicional ter uma sequência ou bloco de comandos (Figura 1.27). Figura 1.27 – Comando condicional simples com um bloco de comandos. Um exemplo desse tipo de comando condicional é mostrado abaixo (Figura 1.28): 01 02 03 04 05 06 07 08 declareidadenumérico idade←39; se(idade>38)e(idade<40) então escreva"quasenos40!" escreva"umaboaidade!" fimse escrevaidade Figura 1.28 – Exemplo de teste condicional com blocos de comandos. 1.11.2. Estrutura condicional composta Neste caso, além da sequência de comandos a ser executada caso a expressão lógica seja avaliada como verdadeira, temos também um conjunto de instruções que pode ser executado caso ela seja avaliada como falsa. seexpressão-lógica entãocomando(s) senãocomando(s) fimse comando(s) Figura 1.29 – Estrutura condicional composta. Essas estrutura condicionais são chamadas de comandos if-thenelse. Conforme mostrado na Figura 1.30, este tipo de comando nos permite escolher entre dois fluxos possíveis. Figura 1.30 – Ideia da estrutura condicional composta. Exemplo: algoritmo{resultadododesempenhodeumaaluno} declaren1,n2numérico declarenomestring leianome,n1,n2 se((n1+n2)/2)>7) entãoescrevanome,"aprovado!" senãoescrevanome,"reprovado!" fimse fimalgoritmo Figura 1.31 – Exemplo de uso da estrutura condicional composta. 1.11.3. Comandos condicionais aninhados (ou encadeados) Podemos colocar como comando a ser executado em uma estrutura condicional outro comando condicional. Ou seja, dentro de um bloco de um comando se...então...senão, podemos ter vários tipos de comandos, incluindo os comandos condicionais (Figura 1.32). seexpressão-lógica-1 então comando(s) seexpressão-lógica-2 comando condicional aninhado. entãocomando(s) fimse comando(s) senão comando(s) seexpressão-lógica-3 entãocomando(s) comando condicional aninhado. fimse comando(s) fimse comando; Figura 1.32 – Comandos condicionais aninhados. Exemplo: algoritmo{alunoaprovado?} declaren1,n2numérico declarenomestring leianome,n1,n2 se((n1+n2)/2)>6) então escreva"Oaluno:",nome,"obteveamédia." se(n1<4)ou(n2<4) então escreva"Masteráquefazerrecuperação!" senão escreva"Atingiutodososrequisitos.Parabéns!" fimse senão escreva"Aluno:",nome,"reprovado!" fimse fimalgoritmo Figura 1.33 – Comandos condicionais aninhados. 1.12. Repetições Muitas das computações que realizamos em um programa são inerentemente repetitivas. Nas linguagens imperativas, encontramos comandos específicos para isso, que são chamados de comandos de repetição ou loops ou estruturas de repetição. Através desses comandos, podemos realizar uma computação até que uma certa condição seja satisfeita. É muito comum utilizarmos laços quando estamos trabalhando com séries e fórmulas matemáticas (Figura 1.34), bem como estrutura matriciais. n ∑a i i=1 a= n∑ xy − ∑ x∑ y n∑ x 2 − (∑ x ) 2 2 ∑ y∑ x − ∑ x∑ xy b= n∑ x − (∑ x ) 2 2 Figura 1.34 – Somátórios. 1.12.1. Laços de repetição com interrupção no início Se a expressão lógica no início do laço for verdadeira, os comandos dentro da estrutura de repetição são executados de maneira sequencial. Ao final da execução dos comandos, internos ao laço, o fluxo de controle do programa volta ao início do laço para avaliar novamente a condição de repetição. Se ela for satisfeita novamente (verdadeira), executa outra vez o corpo do laço, até que a repetição seja interrompida quando a condição for falsa. comando(s) enquantoexpressão-lógicafaça comando(s) fimenquanto comando(s) Figura 1.35 – Laços de repetição com interrupção no início Exemplo: algoritmo declaresoma,numnumérico soma←0 num←1 enquantonum<=5faça soma←soma+num num←num+1 fimenquanto escrevasoma fimalgoritmo Figura 1.36 – Somatório dos números entre [1:5]. 1.12.2. Laços de repetição com interrupção ao final da primeira iteração Explicação. comando(s) faça comando(s) enquantoexpressão-lógica comando(s) Figura 1.37 – Laços de repetição com interrupção no final. Exemplo: algoritmo declaresoma,numnumérico soma←0 num←1 faça soma←soma+num num←num+1 enquantonum<5 escrevasoma fimalgoritmo Figura 1.38 – Somatório dos números entre [1:5]. 1.12.3. Repetição com variável de controle Este tipo de laço é muito comum quando fazemos algoritmos que manipulam matrizes9. paravdev1atévn[passop]faça comando(s) fimpara Figura 1.39 – Laços de repetição com variável de controle. Exemplo: algoritmo declaresomanumérico soma←0 paraide1até5faça soma←soma+i fimpara escrevasoma fimalgoritmo Figura 1.40 – Somatório dos números entre [1:5] com laço do tipo “para”. 9 https://pt.wikipedia.org/wiki/Estrutura_de_repetição Outra variação deste tipo de laço é a seguinte: paracadaitemdeconjuntofaça comando(s) fimpara Figura 1.41 – Laços de repetição com variável de controle sobre um conjunto. Veremos exemplos de uso deste tipo de laço quando falarmos de vetores, matrizes, listas. 1.12.4. Interrompendo o laço de repetição Podemos utilizar em nossos algoritmos a instrução interrompa, que faz com que o fluxo de execução de um laço seja quebrado, isto é, desviado para a instrução seguinte ao laço. Em geral, podemos colocar esta instrução dentro de um teste condicional no corpo do laço. 1.12.5. Desviando a sequência de laço de repetição Outra instrução útil é a continue, que desvia o fluxo de execução de dentro do laço para a próxima iteração do loop. 1.13. Vetores Suponha que cada aluno da disciplina CAP-241 tenha tido sua avaliação e que estejamos interessados em computar o número de alunos que obtiveram nota acima da média. Como escrever este algoritmo apenas com as ferramentas discutidas até aqui? Para esse tipo de problema, bem como outros que precisam lidar com uma coleção homogênea de valores, temos a estrutura chamada de vetor: várias posições de memória identificadas por um mesmo nome de variável, sendo acessíveis através de índices (Figura 1.42). Figura 1.42 – Esquema de um vetor de números inteiros. Declaramos um vetor da seguinte forma: declarev[i:f]tipo Obviamente, que: i≤f. Exemplo: Algoritmo para calcular o número de alunos com nota acima da média. algoritmo declarenotas[1:5]numérico{vetordasnotasdosalunos} declaresomanumérico{somadasnotasdosalunos} declaremedianumérico{médiadasnotasdosalunos} declarenum_alunosnumérico{númerodealunosacimadamédia} soma←0 media←0 num_alunos←0 paraide1até5faça leianotas[i] soma←soma+notas[i] fimpara media←soma/5 paraide1até5faça senotas[i]>media então num_alunos←num_alunos+1 fimse fimpara escrevanum_alunos fimalgoritmo Figura 1.43 – Calculando o número de alunos com nota acima da média. 1.14. Matrizes (Arrays) Podemos também criar variáveis compostas multidimensionais denominadas matrizes. Quando estivermos falando de grafos, esta estrutura será fundamental, pois ela serve de suporte para uma das formas de representação conhecida por matriz de adjacência. Declaramos uma matriz da seguinte forma: declarem[i0:f0;i1:f1;...;in:fn]tipo Agora, o acesso a um elemento da matriz é feita utilizando-se índices para cada um dos intervalos das dimensões. Exemplo: Dada a tabela abaixo, contendo as notas de 4 provas de 5 alunos, faça um algoritmo que dada uma matriz bidimensional (5x4), calcule a média de cada aluno e a média da turma. N1 7.5 5 9 5 6 N2 7.2 5 7.8 4 7 N3 8.1 5 6.3 3 8 N4 9 5 9.2 2 9 Figura 1.44 – Tabela de notas de alunos. Exemplo: supondo m[0:4; 0:3] escreva m[0][3]{9} escreva m[2][1]{7.8} 1.15. Estruturas ou Registros Uma estrutura ou registro agrupa elementos de dados, possivelmente de tipos diferentes, em um único nome. Também é uma forma de criarmos nossos próprios tipos de dados. A sintaxe para criação de uma estrutura é a seguinte: declarenomeestrutura(componentes) Vamos criar uma variável chamada ficha_aluno para representar as informações sobre um aluno: declareficha_alunoestrutura(matriculanumérico; nomestring; data_nascimentostring; cpfstring) Podemos acessar cada um dos componentes da estrutura utilizando um separador “.”: ficha_aluno.nome←"Eduardo" escrevaficha_aluno.cpf Observação: • Assim como temos vetores e matrizes dos tipos básicos (numérico, lógico e string), podemos ter também de registros. • Voltaremos a conversar mais sobre estruturas e tipos de dados mais adiante, quando estivermos falando de tipos abstratos de dados. 1.16. Criando Procedimentos e Funções Uma forma que temos de modularizar nossos algoritmos é organizá-los em procedimentos e funções: blocos de código auto-contidos e identificados com um nome, uma lista de parâmetros e que pode ser invocado nos nosso programas. A Figura 1.45 mostra um procedimento que transpõe os valores de uma matriz m1 para uma matriz m2. Veja que um procedimento possui um identificador e uma lista de parâmetros. procedimentotranspor(m1,m2,nlinhas,ncolunas) paraide1aténlinhasfaça parajde1aténcolunasfaça m2[j][i]←m1[i][j]; fimpara fimpara fimprocedimento Figura 1.45 – Procedimento de transposição de uma matriz. Podemos chamar o procedimento acima nos nossos algoritmos da mesma forma que fizemos com as funções matemáticas: colocando o nome da função e entre parênteses, os argumentos a serem passados. Uma função é muito semelhante a um procedimento, só que deve retornar um valor. funçãotranspor(m,nlinhas,ncolunas) declarem2[1:nlinhas;1:ncolunas] paraide1aténlinhasfaça parajde1aténcolunasfaça m2[j][i]←m[i][j]; fimpara fimpara retornam2 fimfunção Figura 1.46 – Função de transposição de uma matriz. Observação: iremos dar mais atenção sobre a passagem de argumentos quando estivermos falando de C++. 1.17. Recursividade Vamos retomar o problema de geração da série de Fibonacci (Algoritmo Figura 1.5). Desta vez, vamos apenas computar o n-th termo. Agora que conhecemos todas as estruturas de controle, comandos, atribuição e funções, podemos construir o seguinte algoritmo: algoritmo funçãofib(n) declaret1,t2,tnumérico t1←0 t2←1 t←1 paraide1aténfaça t←t1+t2 t1←t2 t2←t fimpara retornat fimfunção declarevnumérico declarefnumérico leiav f←fib(v) escrevaf fimalgoritmo Figura 1.47 – Função iterativa para geração do n-th termo da série de Fibonacci. Veja agora esta outra versão: algoritmo funçãofib(n) sen<2 então retornan senão retornafib(n–1)+fib(n–2) fimse fimfunção declarevnumérico declarefnumérico leiav f←fib(v) escrevaf fimalgoritmo Figura 1.48 – Função recursiva para geração do n-th termo da série de Fibonacci. Observação: iremos falar mais sobre recursividade nas próximas aulas. 1.18. Modelo de Programação Alguns autores de livros (Farrer et al., 1989; Manzano e Oliveira, 2004) e professores de Introdução à Lógica de Programação e Algoritmos preferem fazer uma separação completa da teoria e dos conceitos do uso de uma linguagem de programação específica. Em geral, o formalismo adotado por eles baseia-se em uma notação mais próxima à linguagem natural, expressando os algoritmos no que chamamos de pseudocódigo. Esse pseudocódigo mais tarde pode ser transcrito para uma linguagem de programação qualquer. Vamos a um exemplo concreto. Suponha que tenhamos que projetar um algoritmo para reportar os pontos de intersecção entre segmentos de reta de dois conjuntos, um conjunto vermelho e outro azul, conforme a Figura 1.49. Figura 1.49 – Pontos de intersecção entre dois conjunto de segmentos de reta. Uma possível notação algorítmica, que podemos empregar para expressar um algoritmo capaz de determinar os pontos de intersecção entre os segmentos é mostrado na Figura 1.50. algoritmo entrada:R=conjuntodesegmentosdereta B=conjuntodesegmentosdereta saída:conjuntodospontosdeintersecção início paracadardeRfaça paracadabdeBfaça ser∩b entãoreportar_ponto_interseção(r∩b) fimse fimpara fimpara fim fimalgoritmo Figura 1.50 – Algoritmo para computação dos pontos de intersecção entre dois conjuntos de segmentos de reta. Transpondo o algoritmo acima para a Linguagem C++, teríamos a possível implementação mostrada na Figura 1.51. //Definiçãodaestruturaponto #include<ponto.hpp> //Definiçãodaestruturasegmentodereta #include<segmento_reta.hpp> //Funçãoauxiliarparacalculodaintersecçãoentredoissegmentosdereta #include<segment_intersection.hpp> //BibliotecaPadrãoC++ #include<utility>//definiçãodepair #include<vector>//definiçãodevector std::vector<point> computa_intersecao(conststd::vector<segmento_reta>&vermelhos, conststd::vector<segmento_reta>&azuis) { std::vector<point>pts_interseccao; for(constsegment&v:vermelhos) for(constsegment&a:azuis) { std::pair<point,point>par_pts_interseccao; tipo_intert=computa_intersecao(v,a,par_pts_interseccao); if(t==DISJUNTO) continue; if(t==CRUZA) { pts_interseccao.push_back(par_pts_interseccao.first); } else { pts_interseccao.push_back(par_pts_interseccao.first); pts_interseccao.push_back(par_pts_interseccao.second); } } returnpts_interseccao; } Figura 1.51 – Implementação do algoritmo para computação dos pontos de intersecção entre segmentos de dois conjuntos apresentado na Figura 3.2 em C++. Como podemos observar, a implementação precisa considerar muito mais detalhes do que os existentes na descrição algorítmica. Em geral, a justificativa para se evitar o uso de uma linguagem específica, como mostrado na Figura 1.21, e usar uma notação como a apresentada na Figura 1.20, é que usar linguagens de alto nível, como FORTRAN (Backus et al., 1957), PASCAL (Wirth, 1971a), C (Kernighan e Ritchie, 1988), C++ (Stroustrup, 2013) e Java (Gosling, 2013), não só dificulta o aprendizado mas também impõe condições limitantes em relação à forma de expressão do raciocínio lógico. Em outras palavras, esses autores defendem a ideia de que a utilização de uma linguagem de programação para expressar as várias etapas do raciocínio lógico força o iniciante a se preocupar excessivamente com aspectos poucos significativos para o desenvolvimento do raciocínio, que são as regras de sintaxe e os ambientes de compilação das linguagens de programação. Neste curso, iremos aprender os conceitos básicos, mas iremos automaticamente conectá-los ao suporte presente na Linguagem C++. O motivo para isso é simples: este não será um curso apenas de Introdução à Lógica de Programação e eu quero prepará-los para as outras disciplinas que virão na sequência! Além disso, em cursos modernos, as dificuldades apontadas não mais se justificam dada a quantidade de material disponível na Internet, a quantidade de livros sobre o tópico e a facilidade de acesso aos diversos ambientes de programação. Até mesmo autores consagrados, como Nivio Ziviani10 (Ziviani, 2005) e Robert Sedgewick11 (Sedgewick e Wayne, 2011), já utilizam as linguagens Pascal, C, C++ e Java para ensino de algoritmos e estruturas de dados. Portanto, todos os nosso programas serão implementados usando um pequeno subconjunto da Linguagem C++. Isto significa que não vou pedir que façam um código usando construções e idiomas avançados em C++. Mas teremos um mínimo que cada um terá que saber. 10 11 Professor do Departamento de Ciência da Computação da UFMG. Professor de Ciência da Computação da Universidade de Princeton. Minha opção por não utilizar apenas pseudocódigo é justamente para que vocês possam estudar a fundo as propriedades dos algoritmos, colocandoos em prática rapidamente. Apesar de usarmos uma linguagem específica, vocês verão que a forma como apresentaremos os conceitos e depois as implementações serão muito fáceis de serem portadas para outras linguagens de programação. 1.19. Instruções sobre ambiente de programação Os participantes do curso que tiverem interesse em realizar atividades práticas poderão utilizar o seguinte ambiente de máquina virtual: • Oracle VirtualBox • Linux Ubuntu 14.04 LTS (Trusty) • Credenciais: o Usuário: cap241 o Senha: algoritmo • Compilador C++ instalado: GNU g++ 4.8.4 • Qt Creator 3.6.0 O link para download da máquina é o seguinte: http://www.dpi.inpe.br/cap241-2016/vm 1.20. Considerações Finais do Capítulo Este capítulo abordou os principais conceitos da lógica de programação: tipos de dados, variáveis, comandos, operadores, expressões, comando de atribuição, chamada de função, comandos condicionais, comandos de repetição, vetores, matrizes, estruturas, criação de funções e recursividade. Existem diversos livros e apostilas sobre introdução à lógica de programação, sugiro como apoio os livros de Farrer et. al (1989) e de Manzano e Oliveira (2004), que foram utilizados na preparação destas notas de aula. A história das linguagens de programação é muita rica em ideias que foram transformadas em soluções práticas, merecendo uma atenção especial de todo cientista da computação. Existe uma página da Wikipedia12 dedicada a este assunto, contendo links para outras páginas e documentos muito interessantes, que explicam detalhes de como os cientistas da época partiram de problemas práticos e desenvolveram teorias importantes que nos levaram às ferramentas que temos hoje. As obras de Dijkstra (1976) e Wirth (1971b), apesar de antigas, são clássicos sobre a arte de programar, sendo recomendadas como leitura adicional. 12 https://en.wikipedia.org/wiki/History_of_programming_languages 1.21. Referências Bibliográficas do Capítulo BACKUS, J. W. The IBM 701 Speedcoding System. Journal of the ACM, v. 1, n. 1, jan 1954, p. 4-6. BACKUS, J. W.; BEEBER, R. J.; BEST, S.; GOLDBERG, R.; HAIBT, L. M.; HERRICK, H. L.; NELSON, R. A.; SAYRE, D.; SHERIDAN, P. B.; STERN, H.; ZILLER, I.; HUGHES, R. A.; NUTT, R. The FORTRAN automatic coding system. In: Western Joint Computer Conference: Techniques for Reliability, 26 a 28 fev, 1957. Anais... New York, NY, USA: ACM, 1957. p. 188-198. DIJKSTRA, E. W. A Discipline of Programming. Prentice Hall,1976. 217 p. FARRER, H.; BECKER, C. G.; FARIA, E. C.; MATOS, H. F.; SANTOS, M. A.; MAIA, M. L. Algoritmos Estruturados. 2a Edição. Rio de Janeiro, Editora Guanabara, 1989. 252 p. GOSLING, J.; JOY, B.; STEELE JR., G. L.; BRACHA, G.; BUCKLEY, A. The Java Language Specification: Java SE 7 Edition (Java Series). 1a Edição. Addison-Wesley Professional, 2013. 672 p. KERNIGHAN, B. W.; RITCHIE, D. M. The C Programming Language. 2a Edição. Prentice Hall, 1988. 272 p. MANZANO, J. A. N. G.; OLIVEIRA, J. F. Algoritmos: Lógica para Desenvolvimento de Programação de Computadores. 15a Edição. Editora Érica, 2004. 242 p. SEDGEWICK, R.; WAYNE, K. Algorithms. 4a Edição. Addison-Wesley Professional, 2011. 992 p. STROUSTRUP, B. The C++ Programming Language. 4a Edição. AddisonWesley Professional, 2013. 1368 p. WIRTH, N (a). The Programming Language Pascal. Acta Informatica, v. 1, 1971, p. 35-63. WIRTH, N (b). Program Development by Stepwise Refinement. Communications of the ACM, v. 14, n. 4, abr. 1971, p. 221-227. ZIVIANI, N. Projeto de Algoritmos: com implementações em PASCAL e C. 2a Edição. Editora Thomson, 2005. 552 p. 1.22. Exercícios Propostos EP-2.1. Por que não podemos ter um algoritmo com um comando do tipo: “Escreva todos os termos da sequência de Fibonacci”? EP-2.2. Mostre como é a execução do algoritmo abaixo: algoritmo declareA[1:10],B[1:10]numérico paraide1até10faça leiaA[i] fimpara paraide1até10faça r←i–2*trunca(i/2) ser==0 então B[i]←A[i]*5 senão B[i]←A[i]+5 fimse fimpara paraide1até10faça escrevaB[i] fimpara fimalgoritmo EP-2.3. Escreva um algoritmo que leia três números e escreva o valor do maior e menor deles. EP-2.4. Escreva um algoritmo que leia o tamanho dos lados de um triângulo, avalie se esses valores realmente formam um triangulo, e em caso positivo, classifique o triângulo em isósceles, escaleno ou equilátero. EP-2.5. Escreva um algoritmo que calcule o fatorial de um número. Faça uma versão iterativa e outra recursiva. EP-2.6. Escreva um algoritmo que pergunte ao usuário uma temperatura em graus Celsius e escreva a conversão para graus Fahrenheit. O programa deve parar apenas quando o usuário informar que não quer mais continuar. EP-2.7. Seja as seguintes séries: • Lucas: 1,3,4,7,11,18,29,47... • Pell: 1,2,5,12,29,70,169,408... • Triangular: 1,3,6,10,15,21,28,36... • Square: 1,4,9,16,25,36,49,64... • Pentagonal: 1,5,12,22,35,51,70,92... Pede-se: a) Criar um algoritmo iterativo que compute a série num vetor. b) Criar um algoritmo recursivo que compute a série num vetor. EP-2.8. Escreva uma algoritmo que dado um vetor de números encontre o maior e o menor elementos neste vetor. EP-2.9. Escreva uma algoritmo para multiplicar duas matrizes. EP-2.10. Escreva uma algoritmo para ler as coordenadas de dois pontos e então calcular a distância entre eles. E-2.11. Escrever um programa que simule uma calculadora com funções básicas. 1.23. Exercícios Especiais EE-2.1. Escreva uma função para calcular o máximo divisor comum entre três números inteiros positivos quaisquer. EE-2.2. Escreva uma função para converter um número inteiro em uma sequência de caracteres representando esse número. EE-2.3. Defina uma estrutura para representação de um segmento de reta no espaço 2D. EE-2.4. Escreva uma função, que dado dois segmentos de reta, conforme representação do exercício EE-2.3, calcule o ponto de intersecção entre eles. EE-2.5. Escrever um algoritmo para ler as coordenadas de um ponto e de um segmento de reta e calcular a distância perpendicular a este segmento.