Apostila de COM146 Algoritmos e Estruturas de Dados I Disciplina do 1o módulo do Curso de Ciência da Computação da Universidade Federal de Lavras Olinda Nogueira Paes Cardoso1 Agosto de 2002 1 Professora Auxiliar do Departamento de Ciência da Computação da UFLA 1 ÍNDICE 1. Introdução ......................................................................................................................................... 2 Necessidade do uso da lógica ...................................................................................................... 2 Conceitos Básicos......................................................................................................................... 2 Conceito de Algoritmo................................................................................................................... 2 2. PORTUGOL...................................................................................................................................... 3 2.1. Definição de Variáveis – Tipos Básicos........................................................................................ 3 2.2. Comandos Básicos ....................................................................................................................... 4 2.3. Blocos e Comandos Básicos de Controle..................................................................................... 4 2.4. Outras estruturas de repetição...................................................................................................... 7 2.5. Definição de novos tipos de dados ............................................................................................... 8 2.6. Estruturas Condicionais Encadeadas ........................................................................................... 8 3. Ordenação ........................................................................................................................................ 9 3.1. Ordenação de Vetores .................................................................................................................. 9 3.1.1. Ordenação por inserção ........................................................................................................ 9 3.1.2. Ordenação por seleção........................................................................................................ 10 4. Matrizes .......................................................................................................................................... 11 4.1. Matrizes Bidimensionais ............................................................................................................. 11 5. Registros......................................................................................................................................... 12 5.1. Registro com Vetor ..................................................................................................................... 12 5.2. Conjuntos de Registros............................................................................................................... 13 6. Arquivos .......................................................................................................................................... 13 6.1. Organização de Arquivos............................................................................................................ 14 6.1.1. Organização Seqüencial...................................................................................................... 14 6.2. Organização Direta ..................................................................................................................... 15 7. Modularização ................................................................................................................................ 17 7.1. Benefícios da modularização ...................................................................................................... 18 7.2. Ferramentas para modularização ............................................................................................... 18 7.3. Modos de transferência de parâmetros ...................................................................................... 19 8. Recursividade ................................................................................................................................. 19 8.1. Exemplo de problema recursivo ................................................................................................. 20 8.2. Recursão × Iteração.................................................................................................................... 20 8.3. Exemplos mais famosos de problemas recursivos..................................................................... 20 9. Apontadores ................................................................................................................................... 21 10. Listas Lineares................................................................................................................................ 22 10.1. Operações em listas lineares .................................................................................................. 22 10.2. Implementação de listas lineares ............................................................................................ 23 1.1. 1.2. 1.3. 2 1. INTRODUÇÃO 1.1. NECESSIDADE DO USO DA LÓGICA A lógica é a ciência que estuda as leis e os critérios de validade que regem o pensamento e a demonstração, ou seja, ciência dos princípios formais do raciocínio. A lógica é usada no dia a dia das pessoas que trabalham com computação para solucionar problemas de forma eficiente. A técnica mais importante no projeto da lógica de programas é chamada programação estruturada, a qual consiste em uma metodologia de projeto que objetiva: • agilizar a codificação da escrita da programação; • facilitar a depuração da leitura da mesma; • permitir a verificação de possíveis falhas apresentadas pelos programas; • facilitar as alterações e atualizações dos programas. 1.2. CONCEITOS BÁSICOS O conceito central da programação em ciência da computação é o de algoritmo. Segundo Wirth a programação estruturada é “a arte ou técnica de construir e formular algoritmos de uma forma sistemática”. Programas são “formulações concretas de algoritmos abstratos, baseados em representações e estruturas específicas de dados”. Estruturas de dados são usadas no algoritmo para representar as informações do problema a ser resolvido. Na programação deve-se distinguir claramente dois aspectos: Aspecto estático – a formulação de um algoritmo consiste em um texto contendo comandos (instruções) que devem ser executados numa ordem prescrita. Aspecto dinâmico – os efeitos que são causados pela execução do programa no tempo, dado um conjunto de valores iniciais. 1.3. CONCEITO DE ALGORITMO Um algoritmo é uma norma executável para estabelecer um certo efeito desejado, que na prática será geralmente a obtenção de uma solução a um certo tipo de problema. Exemplo de algoritmo para trocar uma lâmpada queimada: “pegar uma lâmpada nova no armário”; “pegar a escada na área de serviço”; “subir na escada com a lâmpada nova na mão”; “retirar a lâmpada queimada”; “colocar a lâmpada nova”; “descer a escada”; “testar se a lâmpada nova está funcionando” O símbolo de seqüenciamento (;) tem duas funções: no texto a de separar um comando do outro; e no evento a de indicar que os comandos separados devem ser executados na mesma seqüência em que aparecem no texto. O símbolo ; representa a mais simples estrutura de controle, a seqüência simples. Exercício: escreva um algoritmo para se trocar um pneu furado. Assuma que estão disponíveis um macaco e um estepe em boas condições. Seguindo com o exemplo da troca de lâmpadas, vamos supor que há a possibilidade de que a escada disponível não seja alta suficiente para alcançar a lâmpada e que, neste caso, gostaríamos prever este possível erro. Poderíamos reescrever o algoritmo desta forma: “pegar uma lâmpada nova no armário”; “pegar a escada na área de serviço”; “subir na escada com a lâmpada nova na mão”; 3 se “for possível alcançar a lâmpada a ser trocada” então “retirar a lâmpada queimada”; “colocar a lâmpada nova”; “descer da escada”; “guardar a escada”; Outro caso: supondo que havia várias lâmpadas para serem trocadas na casa. Poderíamos reescrever o algoritmo desta forma: “pegar todas as lâmpadas novas no armário”; “pegar a escada na área de serviço”; enquanto “existirem lâmpadas novas disponíveis” faça “subir na escada com uma lâmpada nova na mão”; se “for possível alcançar a lâmpada a ser trocada” então “retirar a lâmpada queimada”; “colocar a lâmpada nova”; “descer da escada”; “guardar a escada”; 2. PORTUGOL A partir de agora será introduzida uma linguagem de expressão de algoritmos, o PORTUGOL. Serão apresentadas a sintaxe e a semântica dos comandos da linguagem. PORTUGOL é uma pseudolinguagem de programação utilizada para obter uma notação para algoritmos, a ser usada na definição, na criação, no desenvolvimento e na documentação de um programa. O objetivo não é criar mais uma linguagem de programação, por isso as regras não precisam ser seguidas de forma muito rígida. Considerando o PORTUGOL, a sintaxe é definida e a forma apresentada e aceita como padrão. Para cada declaração e/ou comando a semântica deve ser explicada. O identificador é o elemento básico da linguagem, a sua sintaxe é definida pelo diagrama apresentado na Figura 1: letra letra dígito Figura 1: Diagrama que representa um identificador 2.1. DEFINIÇÃO DE VARIÁVEIS – TIPOS BÁSICOS No PORTUGOL existem quatro tipos básicos, isto é, tipos básicos de dados que podem ser utilizados: INTEIRO, REAL, CARACTER e LÓGICO. Uma variável pode ser entendida como um local onde se pode colocar qualquer valor do conjunto de valores possíveis do tipo básico associado. O nome da variável é o identificador tal como definido anteriormente. Por exemplo: SOMA Variável SOMA Toda variável deve ser declarada conforme a sintaxe apresentada na Figura 2. A semântica de uma declaração de variáveis corresponde à criação de locais na memória, rotulada com o nome do identificador (variável) e marcada com o tipo de valores que ela pode conter. 4 , : inteiro ; identificador real caracter lógico Figura 2: Diagrama que representa a definição de uma variável. 2.2. COMANDOS BÁSICOS O comando de ATRIBUIÇÃO é utilizado para atribuir um valor a uma variável. Para isso usase o símbolo ←, conforme a seguinte sintaxe: identificador ← expressão ; A notação usada para expressões é basicamente uma forma linear comumente usada na matemática, que pode conter operadores: ARITMÉTICOS: +, -, /, *, raiz( ), **, sen( ), cos( ), mod, div,... LÓGICOS: e, ou, não ( Λ, V, ) RELACIONAIS: =, ≠, >, ≥ (ou >=), <, ≤ (ou <=) É importante observar que o resultado da expressão (do lado direito do comando de atribuição) deve ser coerente com o tipo declarado para a variável (do lado esquerdo). 2.3. BLOCOS E COMANDOS BÁSICOS DE CONTROLE Um bloco pode ser definido como um conjunto de comandos com uma função bem definida. Serve também para definir os limites onde as variáveis declaradas em seu interior são conhecidas. Exemplo: início < declaração de variáveis > < comandos > fim Uma seqüência simples é um conjunto de comandos separados por ponto e vírgula (;), que serão executadas numa seqüência linear de cima para baixo. Exemplo: comando1; comando2; comando3; ... comandoN; Quando a ação a ser executada depender de uma inspeção (teste), teremos uma alternativa, ou estrutura condicional, simples ou composta. 5 Exemplo de uma estrutura condicional simples: se < condição > então comando1; comando2; ... comandoN; fim se; Exemplo de uma estrutura condicional composta: se < condição > então comando1; comando2; ... comandoN; senão comando1’; comando2’; ... comandoN’; fim se; Nos comandos apresentados, < condição > é qualquer expressão cujo resultado seja falso ou verdadeiro. Exercício: Qual será o valor final das variáveis A e B depois da execução do seguinte algoritmo? início inteiro: A, B; A ← 1; B ← 2; se A > B então A ← 5; senão A ← 10; fim se; fim; Uma estrutura de repetição é quando um conjunto de ações é executado repetidamente enquanto uma determinada condição permanece válida (ou seja, quando o resultado de da expressão é um valor lógico verdadeiro). Exemplo de uma estrutura de repetição: enquanto < condição > faça comando1; comando2; ... comandoN; fim enquanto; Enquanto o valor da < condição > for verdadeiro, as ações dos comandos são executadas e quando se tornar falso, o comando é abandonado. Se já da primeira vez o resultado é falso, os comandos não são executados. 6 Exercício: Qual será o valor final das variáveis declaradas no seguinte algoritmo, depois de sua execução? início inteiro: A, B, C, I; A ← 1; B ← 1; I ← 1; enquanto I < 10 faça C ← A + B; A ← B; B ← C; I ← I + 1; fim enquanto; fim; Até agora todos os valores calculados pelos algoritmos foram gerados e permaneceram na memória. Para obter ou para fornecer dados ao ambiente exterior ao algoritmo, por exemplo do teclado e para o vídeo, é preciso utilizar comandos de entrada e saída. O comando de entrada é leia e o comando de saída é imprima, e suas sintaxes são apresentadas a seguir. , leia ( identificador ) ; , imprima ( identificador ) ; expressão caracter Exemplo de um algoritmo que usa comandos de entrada e saída: início inteiro: A, B, SOMA; leia (A, B); SOMA ← A + B; imprima (“A soma entre ”, A, “ e ”, B, “ é ”, SOMA); fim; Exercício: Escreva o algoritmo que realize a multiplicação de dois números inteiros, utilizando apenas o operador da soma (+). Regras práticas para a construção de algoritmos legíveis: • • • • Procure incorporar comentários no algoritmo para descrever o significado das variáveis e ações utilizadas. Para isso use chaves {}. Escolha nomes de variáveis que sejam significativos, isto é, que traduzam o tipo de informação a ser armazenada na variável. Por exemplo: NOTA, SOMA, MÉDIA, etc. Grife as palavras-chave (escritas com letras minúsculas) do algoritmo, destacando as estruturas de controle. Procure alinhar os comandos de acordo com o nível a que pertencem, isto é, destaque a estrutura na qual estão contidos. 7 Exercícios de Fixação: Construa algoritmos para solucionar os seguintes problemas. 1. Como saber se um número é divisível por outro. 2. Calcular a média final de um aluno que realizou 4 avaliações, sabendo que todas têm o mesmo peso. 3. Dados 3 números, verificar qual deles é o menor. 2.4. OUTRAS ESTRUTURAS DE REPETIÇÃO Serão apresentadas a seguir outras duas estruturas de repetição que também podem ser utilizadas na construção de algoritmos usando o PORTUGOL, são elas: repita e para. A estrutura de repetição repita difere da enquanto no que diz respeito ao momento em que o teste da condição é submetido. repita comando1; comando2; ... comandoN; até < condição >; Na estrutura enquanto o teste é realizado antes da execução do primeiro loop, ou seja, pode ser que os comandos não sejam realizados sequer uma vez, caso a condição seja falsa já na primeira vez em que foi testada. Já na repita os comandos sempre serão executados pelo menos uma vez, até que a condição seja testada, no final da estrutura. A estrutura de repetição para difere das estruturas enquanto e repita, pois utiliza uma variável de controle, que atua como um contador de repetições. para I de 1 até 10 passo 1 faça comando1; comando2; ... comandoN; fim para; Observando o exemplo, percebe-se que foi utilizada uma variável do tipo inteiro (I), que deve ter sido declarada anteriormente. Esta estrutura irá executar os comandos 10 vezes, pois possui I variando automaticamente de 1 em 1 (passo 1) até 10, ou seja, não é necessário fazer o incremento deste dentro da estrutura de repetição. Exercício: Avalie os algoritmos construídos até o presente momento e, quando achar viável, substitua a estrutura de repetição enquanto pela estrutura repita ou para, a fim de melhorar o seu desempenho. O comando abandone só tem sentido dentro de um comando de repetição (enquanto, repita e para). Além disso, estará sempre associado ao teste de uma condição com comando se. Sintaxe: abandone; A semântica do comando é a seguinte: quando o abandone é encontrado, o próximo comando a ser executado é o primeiro logo após o fim do comando de repetição mais interno onde este aparece. Exemplo: ... enquanto I > 0 faça I ← I + 1; imprima (I); 2 se I + 1 <= 150 então I ← I + 25; senão abandone; 8 fim se; fim enquanto; 2.5. DEFINIÇÃO DE NOVOS TIPOS DE DADOS Nem sempre os tipos básicos (inteiro, real, caracter e lógico) são suficientes para exprimir estruturas de dados em algoritmos. Daí a necessidade de novos tipos de dados serem criados. Um destes tipos é o vetor. No PORTUGOL a criação de um vetor segue as especificações: tipo nome_do_tipo = vetor [li:ls] <tipo_básico>; É dado um nome_do_tipo ao novo tipo de vetor criado, onde li é o limite inferior e ls é o limite superior de um intervalo que define o tamanho do vetor (valores inteiros), e tipo_básico é um dos tipos básicos já conhecidos. Esta especificação apenas indica um modelo para a criação de variáveis deste novo tipo. Para efetivar esta estrutura dentro do algoritmo, é necessário declará-la dando um nome a variável que será criada segundo o modelo especificado. Exemplo: um vetor que armazena as notas de todos os alunos de uma turma com 25 alunos. tipo v = vetor [1:25] real; v: NOTAS; O número de elementos de um vetor é dado por ls-li+1. Isto significa que as posições do vetor são identificadas a partir de li, com incrementos unitários até ls. li li+1 li+2 ls … Cada elemento de um vetor é tratado como se fosse uma variável simples. Para referência a um elemento do vetor utiliza-se o nome do vetor e a identificação do elemento (índice) entre colchetes ([ ]). o Por exemplo, se quisermos atribuir o valor de uma 45 a nota do 6 aluno (que é identificado pelo índice 6 do vetor de notas): NOTAS [6] ← 45; Exemplo: O que será impresso no algoritmo abaixo? início inteiro: I; tipo vc = vetor [1:7] caracter; vc: DIAS; DIAS [1] ← “domingo”; DIAS [2] ← “segunda-feira”; DIAS [3] ← “terça-feira”; DIAS [4] ← “quarta-feira”; DIAS [5] ← “quinta-feira”; DIAS [6] ← “sexta-feira”; DIAS [7] ← “sábado”; para I de 1 até 7 passo 2 faça imprima (DIAS [I]); fim para; fim. Exercício: Um professor de uma turma com 30 alunos quer armazenar as notas de seus alunos em um vetor e depois calcular a média geral da turma. Escreva um algoritmo que solucione este problema usando uma estrutura de vetor. 2.6. ESTRUTURAS CONDICIONAIS ENCADEADAS Existem casos em que é necessário se estabelecerem verificações de condições sucessivas, onde uma determinada ação poderá ser executada se um conjunto anterior de condições for satisfeito. Isto significa usar uma condição dentro de outra. Este tipo de estrutura pode possuir diversos níveis de condição, sendo chamada de aninhamento ou encadeamento de estruturas condicionais. Exemplo: se <condição 1> então <comandos para condição 1 verdadeira> senão se <condição 2> então <comandos para a condição 1 falsa e 2 verdadeira> senão <comandos para a condição 1 e 2 falsas > 9 fim se; {final do se mais interno} fim se; {final do primeiro se (mais externo)} Exercício: Escreva um algoritmo que efetue o cálculo de reajuste de salário de um funcionário. Considere que o aumento será de 15% se o salário for até R$500,00, de 10% se for entre R$501,00 e R$1.000,00 e de 5% se for maior que R$1.000,00. 3. ORDENAÇÃO A atividade de ordenação é o processo de rearranjo de um certo conjunto de objetos de acordo com um critério (ordem) específico. O objetivo da ordenação é facilitar a localização dos membros de um conjunto de dados. 3.1. ORDENAÇÃO DE VETORES A preocupação mais importante a ser estabelecida em relação aos métodos de ordenação de vetores corresponde ao uso econômico da memória disponível. Isto implica que a permutação de elementos, responsável por levar o elemento à ordem desejada, deve ser efetuada in situ, e que, portanto, são de menor interesse os métodos que efetuam o transporte físico dos elementos de um vetor A para um vetor resultante B. Restringindo-se a escolha dos métodos, dentre as inúmeras soluções possíveis, de acordo com o critério de economia de memória, pode-se promover uma primeira classificação de acordo com a eficiência do mesmo em relação à economia de tempo. Uma boa medida de eficiência é obtida contando-se o número C de comparações necessárias e o número M de movimentos (transposições) dos elementos. Estes números são funções do número n de elementos a serem ordenados. Os métodos de ordenação que ordenam os elementos in situ podem ser classificados em três principais categorias: • Ordenação por inserção • Ordenação por seleção • Ordenação por troca Estes três princípios serão examinados e comparados. Os exemplos operam sobre a variável A, cujos componentes serão ordenados e se referem a um vetor de inteiros de tamanho variável (N), definido como se segue: inteiro: N; tipo vet = vetor [1:N] inteiro; vet: A; 3.1.1. ORDENAÇÃO POR INSERÇÃO Em cada passo, iniciando-se com i=2 e incrementando-se i de uma em uma unidade, o i-ésimo elemento da seqüência vai sendo comparado com os elementos anteriores e, se for o caso, retirado e inserido na posição apropriada. O processo de ordenação por inserção será mostrado em um exemplo, em que são ordenados oito números (N=8) escolhidos aleatoriamente. O algoritmo deve fazer o seguinte: para I de 2 até N faça X ← A[I]; inserir X no local adequado em A[1]...A[I] fim para Valores iniciais 44 55 12 42 94 18 06 67 i=2 i=3 i=4 i=5 i=6 44 12 12 12 12 55 44 42 42 18 12 55 44 44 42 42 42 55 55 44 94 94 94 94 55 18 18 18 18 94 06 06 06 06 06 67 67 67 67 67 10 i=7 i=8 06 12 18 42 44 55 94 67 06 12 18 42 44 55 67 94 Para encontrar o local apropriado do elemento observado é conveniente utilizar, de modo alternado, operações de comparação e de movimentação, examinando X, e comparando-o com o elemento A[J], e então efetuando ou a inserção de X ou a movimentação do elemento A[J], e prosseguindo-se para a esquerda no tratamento dos outros elementos. Para isso será necessário testar duas condições distintas que causam o término deste processo de análise: • Um elemento A[J] é encontrado com um elemento de valor menor do que o seu • A extremidade esquerda é atingida É um caso típico de uma repetição com duas condições de término, que conduz a utilização de um elemento sentinela (para armazenar temporariamente o valor de algum elemento que está sendo analisado). Para isso, será utilizada uma posição do vetor A como sentinela, o A[0] que receberá o valor de X. início inteiro: I, J, N, X; tipo vet = vetor [0:N] inteiro; vet: A; {supondo que o vetor A tenha sido preenchido}... para I de 2 até N faça X ← A[I]; A[0] ← X; J ← I; enquanto X < A[J-1] faça A[J] ← A[J-1]; J ← J-1; fim enquanto; A[J] ← X; fim para; fim; 3.1.2. ORDENAÇÃO POR SELEÇÃO Este método é baseado no seguinte princípio: • Selecionar o elemento que apresenta o menor valor • Trocá-lo com o primeiro elemento da seqüência A[1] • Repetir estas operações, envolvendo agora os N–1 elementos restantes, depois os N–2 elementos, ..., até restar um só elemento, o maior deles. Valores iniciais 44 06 06 06 06 06 06 06 55 55 12 12 12 12 12 12 12 12 55 18 18 18 18 18 42 42 42 42 42 42 42 42 94 94 94 94 94 44 44 44 início inteiro: I, J, N, K; tipo vet = vetor [1:N] inteiro; vet: A; {supondo que o vetor A tenha sido preenchido}... para I de 1 até N–1 faça K ← I; X ← A[I]; para J de I+1 até N faça se A[J] < X então K ← J; 18 18 18 55 55 55 55 55 06 44 44 44 44 94 94 67 67 67 67 67 67 67 67 94 11 X ← A[K]; fim se; fim para; A[K] ← A[I]; A[I] ← X; fim para; fim; 4. MATRIZES Uma matriz é uma estrutura de dados homogênea, ou seja, todos os elementos de uma matriz são do mesmo tipo. Um vetor é uma matriz unidimensional, a partir de agora serão apresentadas matrizes com mais de uma dimensão. 4.1. MATRIZES BIDIMENSIONAIS A forma mais comum de trabalhar com matrizes é utilizando duas dimensões, apesar de que em alguns casos possa ser necessário trabalhar com mais de duas. Uma matriz bidimensional é composta por linhas e colunas. As linhas podem ser consideradas como a primeira dimensão e as colunas a segunda dimensão. É preciso definir o tamanho de cada uma dessas dimensões, ou seja, o número de linhas e o número de colunas que esta matriz deverá possuir. Exemplo: Definição de uma matriz com 8 linhas e 5 colunas. tipo mat = matriz [1..8, 1..5] inteiro; mat: TABELA; TABELA 1 2 3 4 5 6 7 8 1 2 3 4 5 Exemplo de algoritmo utilizando matriz com duas dimensões: Seja uma matriz a representação das notas obtidas pelos alunos em uma determinada disciplina. A quantidade de linhas deverá ser equivalente ao número de alunos, neste caso 25. Cada coluna deverá conter o valor de uma das avaliações de cada aluno, neste caso são 3 avaliações. O algoritmo deve preencher a matriz com as notas. início inteiro: I, J; tipo m = matriz [1..25, 1..3] real; m: NOTAS; para J de 1 até 3 faça imprima (“Digite as notas referentes a prova”, J); para I de 1 até 25 faça leia (NOTAS [I, J]); fim para; fim para; fim. 12 Exercício: Escreva um algoritmo que receba as notas referentes a três avaliações realizadas por 25 alunos, e as armazene numa matriz, juntamente com a média total obtida pelo aluno. Sabendo que: as duas primeiras avaliações têm peso de 35 cada uma e a terceira tem peso de 30 pontos. Além disso, para cada média total deve ser enviada uma mensagem informando se o aluno foi aprovado (>=50) ou reprovado (<50) e qual foi a porcentagem da turma aprovada. 5. REGISTROS O registro é um conjunto de dados logicamente relacionados e é uma das principais estruturas de dados. Um registro consiste em trabalhar vários dados de tipos diferentes em uma mesma estrutura e por isso é considerado heterogêneo. Para se declarar um registro segue-se a sintaxe: tipo <identificador> = registro <lista de campos e seus tipos> fim registro; Por exemplo, seja um registro constituído dos campos referentes aos dados de um aluno da universidade, tais como, número de matrícula, nome completo, idade, turma, período em que se encontra e média geral. O algoritmo para criar o registro e ler os dados referentes a um aluno poderia ser o seguinte: início tipo reg_aluno = registro caracter: MAT caracter: NOME inteiro: IDADE caracter: TURMA inteiro: PERIODO real: MEDIA fim registro; reg_aluno: ALUNO; leia (ALUNO.MAT); leia (ALUNO.NOME); leia (ALUNO.IDADE); leia (ALUNO.TURMA); leia (ALUNO.PERIODO); leia (ALUNO.MEDIA); fim 5.1. REGISTRO COM VETOR Um ou mais campos de um registro pode ser do tipo vetor. A construção do registro é feita da mesma forma, porém o vetor a ser utilizado em sua estrutura deve ser declarado anteriormente. Exemplo: início tipo vet = vetor [1..6] real; tipo reg_aluno = registro caracter: MAT caracter: NOME inteiro: IDADE caracter: TURMA inteiro: PERIODO vet: NOTAS fim registro; reg_aluno: ALUNO; inteiro: I; imprima (“Digite a matrícula do aluno”); leia (ALUNO.MAT); imprima (“Digite o nome do aluno”); leia (ALUNO.NOME); 13 imprima (“Digite a idade do aluno”); leia (ALUNO.IDADE); imprima (“Digite a turma do aluno”); leia (ALUNO.TURMA); imprima (“Digite o período do aluno”); leia (ALUNO.PERIODO); imprima (“Digite as notas do aluno”); para I de 1 até 6 faça leia (ALUNO.NOTAS[I]); fim para; fim 5.2. CONJUNTOS DE REGISTROS No conjunto de registros são armazenados dados de várias ocorrências de um determinado tipo de registro. Por exemplo, para armazenar os dados de diversos alunos: início inteiro: I, J, N; tipo vet = vetor [1..6] real; tipo reg = registro caracter: MAT caracter: NOME inteiro: IDADE caracter: TURMA inteiro: PERIODO vet: NOTAS fim registro; tipo aluno = conjunto [1..N] reg; aluno: ALUNOS; imprima (“Digite o número de alunos”); leia (N); para I de 1 até N faça imprima (“Digite a matrícula do aluno”); leia (ALUNOS[I].MAT); imprima (“Digite o nome do aluno”); leia (ALUNOS[I].NOME); imprima (“Digite a idade do aluno”); leia (ALUNOS[I].IDADE); imprima (“Digite a turma do aluno”); leia (ALUNOS[I].TURMA); imprima (“Digite o período do aluno”); leia (ALUNOS[I].PERIODO); imprima (“Digite as notas do aluno”); para J de 1 até 6 faça leia (ALUNOS[I].NOTAS[J]); fim para; fim para; fim 6. ARQUIVOS Até o momento, todas as estruturas de dados estudadas ficaram armazenadas no ambiente do algoritmo, ou seja, tinham duração apenas enquanto o algoritmo estava sendo executado. O arquivo é uma alternativa para estrutura de dados que pode ser fisicamente alocado em outro meio de armazenamento, em disco, por exemplo. 14 Um registro é a parte lógica de uma estrutura de dados, enquanto que um arquivo, constituído por um conjunto de um ou mais registros, é a parte física, e pode ser chamado de registro físico. 6.1. ORGANIZAÇÃO DE ARQUIVOS As operações básicas que podem ser feitas em um arquivo através de um algoritmo são: obtenção de um registro do arquivo, inserção de um novo registro, modificação ou exclusão de um registro. A disposição (organização) de registros no arquivo pode favorecer determinadas operações em detrimento de outras. Conhecendo a organização, o projetista de algoritmos pode escolher aquela que seja mais adequada à solução do seu problema em termos de eficácia e eficiência. Basicamente, existem duas possibilidades de organização de arquivos: • Seqüencial – na qual os registros são obtidos ou inseridos no arquivo em ordem seqüencial; • Direta – em que o acesso do registro é feito de forma direta através do uso de um identificador para o registro. O fato de o arquivo ser armazenado em uma memória secundária o torna independente de qualquer algoritmo, ou seja, ele pode ser criado, consultado, processado e até mesmo removido por algoritmos distintos. Sendo o arquivo uma estrutura fora do ambiente do algoritmo, para que este tenha acesso aos dados do arquivo são necessárias as operações de leitura e escrita de registros no arquivo. No algoritmo, o arquivo deve ser declarado e aberto antes que o acesso possa ser feito. No final do algoritmo, ou quando houver necessidade, o arquivo deve ser fechado. A declaração de um arquivo é feita através da especificação: arquivo <organização> de NOME-DO-REGISTRO: NOME; Exemplo: tipo reg_aluno = registro ... fim registro; reg_aluno: ALUNO; arquivo seqüencial de ALUNO: ALUNOS; A declaração do arquivo é a definição, para o algoritmo, do modelo e dos nomes que estarão associados à estrutura de dados. A associação deste modelo ao arquivo físico é feita no algoritmo com um comando de abertura: abra NOME-DO-ARQUIVO <tipo de utilização>; onde o tipo de utilização pode ser para leitura, escrita ou ambos. Exemplos: abra ALUNOS leitura; abra ALUNOS escrita; abra ALUNOS; Para se desfazer a associação entre o modelo e o arquivo físico, usa-se o comando de fechamento: feche NOME-DO-ARQUIVO; Os formatos para leitura e escrita de um arquivo são dependentes do seu tipo de organização. 6.1.1. ORGANIZAÇÃO SEQÜENCIAL A principal característica da organização seqüencial é a de que os registros são armazenados contiguamente, isto é, um após o outro na ordem em que foram inseridos. A acesso aos registros do arquivo, tanto na leitura quanto na escrita, são feitos seqüencialmente, ou seja, a leitura de um registro só é possível após a leitura de todos os registros anteriores e a escrita de um registro só é feita após o último registro. 15 O comando de leitura de um registro em um arquivo seqüencial é: leia NOME-DO-ARQUIVO . NOME-DO-REGISTRO; E o comando de escrita similar é: escreva NOME-DO-ARQUIVO . NOME-DO-REGISTRO; Observação: Para escrever dados numa próxima posição do arquivo, deve-se incluir “próximo” ao comando de escrita. Por exemplo, escreva próximo ARQUIVO.REGISTRO; Observação: Existe uma variável lógica pré-definida para cada arquivo chamada FDA (Fim De Arquivo), que indica se um arquivo chegou ou não ao seu último arquivo. Exercício 1: Supondo-se a existência de um arquivo A composto por nomes, salários e número de horas trabalhadas por mês dos funcionários de uma empresa, faça um algoritmo que crie um novo arquivo B, com a mesma estrutura, porém contendo apenas os dados referentes aos funcionários que trabalharam mais de 400 horas / mês. Exercício 2: Crie um algoritmo para entrar com dados, a partir do teclado, de um novo funcionário no arquivo A. 6.2. ORGANIZAÇÃO DIRETA A principal característica da organização direta é a facilidade de acesso a um registro desejado, pois, ao contrário da organização seqüencial, para acessar um determinado registro não é preciso percorrer todos os anteriores a ele, o acesso é feito diretamente. Este acesso direto é possível porque a posição do registro no espaço físico do arquivo é univocamente determinada a partir de um dos campos do registro, escolhido no momento de criação do arquivo direto como sua chave. Exemplo: Suponha-se um arquivo seqüencial contendo dados dos alunos, como na tabela abaixo: MATRICULA NOME TURMA PERIODO 9800012 Maria Araújo A 3 9900001 Joaquim Silva A 2 9800002 Carlos Menezes B 3 9900010 Fátima Andrade C 1 9900025 Ana Lúcia Dias B 1 9800005 Marcelo Costa C 3 9900003 Flávio Martins C 2 9800040 Luiz Carvalho A 2 Um algoritmo para encontrar os dados do aluno e imprimir seu nome, cuja matrícula seja 9900003, seria o seguinte: início tipo regaluno = registro caracter: MATRICULA caracter: NOME caracter: TURMA inteiro: PERIODO fim registro; regaluno: DADOS; arquivo seqüencial de DADOS: ALUNOS; abra ALUNOS leitura; repita leia ALUNOS.DADOS; se DADOS.MATRICULA = “9900003” então imprima (DADOS.NOME); abandone; fim se; até ALUNOS.FDA; feche ALUNOS; fim. 16 Se a organização for direta, a disposição dos registros no arquivo não será necessariamente a apresentada no arquivo seqüencial. Através de funções internas ao computador, cada registro será alocado em uma posição univocamente determinada pela chave escolhida, como mostra a tabela abaixo: {função que associa chave ao registro} MATRICULA 9800002 9800005 9800012 9800040 9900001 9900003 9900010 9900025 NOME Maria Araújo Joaquim Silva Carlos Menezes Fátima Andrade Ana Lúcia Dias Marcelo Costa Flávio Martins Luiz Carvalho TURMA A A B C B C C A PERIODO 3 2 3 1 1 3 2 2 Para se ter acesso a um registro, basta efetuar-se a leitura no arquivo usando a chave, no caso o número da matrícula, desejada. Não há necessidade do algoritmo fazer nenhum tipo de pesquisa. O mecanismo de gerência do arquivo direto no computador é capaz de associar a chave ao registro procurado. Caso a chave não exista, uma condição de chave inválida (INV) poderá ser testada. A escolha da chave é feita pelo usuário no momento da criação do arquivo de organização direta e, em geral, é um dos campos do registro. Observações importantes: • Cada registro deverá ser gravado usando sua chave. • Não pode haver registros usando a mesma chave (é única). As operações de leitura e escrita num arquivo de organização direta são indicadas nos algoritmos pelos seguintes comandos: leia item [chave] NOME-ARQUIVO.NOME-REGISTRO; e escreva item [chave] NOME-ARQUIVO.NOME-REGISTRO; O algoritmo desenvolvido anteriormente para encontrar os dados do aluno e imprimir seu nome, cuja matrícula seja 9900003, seria: início tipo regaluno = registro caracter: MATRICULA caracter: NOME caracter: TURMA inteiro: PERIODO fim registro; regaluno: DADOS; arquivo direto de DADOS chave MATRICULA: ALUNOS; abra ALUNOS leitura; leia item [9900003]ALUNOS.DADOS; se ALUNOS.INV {erro se a chave não for encontrada} então imprima (“Aluno não existe”); senão imprima (DADOS.NOME); fim se; feche ALUNOS; fim. 17 Exercícios: 1. Escreva um algoritmo que abra um arquivo VENDAS contendo os seguintes campos: código da peça, quantidade vendida, valor unitário, cliente e data. E crie dois outros arquivos da seguinte forma: • • Contendo os nomes dos clientes que compraram mais de R$500,00 (valor total da compra). Contendo os códigos das peças que venderam mais de 100 unidades para um cliente, no mês de junho de 2002. 2. No mesmo arquivo da questão anterior, faça a busca de uma determinada peça por seu código. Implemente com os dois tipos de organização de arquivos e discuta os problemas relacionados a estas diferentes soluções. 7. MODULARIZAÇÃO No fim da década de 60, alguns problemas no desenvolvimento de sistemas de programação levaram os países desenvolvidos a um evento chamado “crise de software”. Os custos das atividades de programação mostravam a cada ano uma clara tendência a se elevarem muito em relação aos custos dos equipamentos, e isto era devido ao avanço tecnológico na fabricação dos equipamentos de computação e a lenta evolução de técnicas aplicadas ao desenvolvimento de software. A ausência de uma metodologia para a construção de programas conduzia a programas geralmente cheios de erros e com altos custos de desenvolvimento que, conseqüentemente, exigiam custos elevados para a sua correção e manutenção futuras. A programação estruturada foi o resultado de uma série de estudos e propostas de metodologias para desenvolvimento de software. Uma das técnicas aplicadas na programação estruturada, a modularização de programas é uma ferramenta para a elaboração de programas visando, os aspectos de confiabilidade, legibilidade, manutenibilidade e flexibilidade, dentre outros. A modularização é um processo que aborda os aspectos da decomposição de algoritmos em módulos. Módulo é um grupo de comandos, constituindo um trecho do algoritmo, com uma função bem definida e o mais independente possível em relação ao resto do algoritmo. Exemplo – Seja um algoritmo para calcular o salário líquido de um empregado, com as seguintes etapas: início Leia os dados do empregado Determine o salário Escreva o salário fim. Onde “Determine o salário” pode ser refinado como: Calcule as vantagens Calcule as deduções SALARIOLIQ ← VANTAGENS – DEDUÇÕES No refinamento anterior não houve preocupação de como o processo de cálculo das vantagens e deduções seria efetuado. Essas ações constituem funções bem definidas e que serão executadas por módulos específicos, neste caso, o algoritmo anterior ficaria: início Leia os dados do empregado Ative o módulo “Cálculo das vantagens” Ative o módulo “Cálculo das deduções” SALARIOLIQ ← VANTAGENS – DEDUÇÕES Escreva o salário fim. Exemplo da descrição estrutural da modularização: Módulo Principal Módulo Vantagens Módulo Deduções 18 A maneira mais intuitiva de proceder a modularização de problemas é feita definindo um módulo principal de controle e módulos específicos para as funções do algoritmo. Recomenda-se que os módulos de um programa tenham um tamanho limitado, pois módulos muito grandes são difíceis de ser compreendidos e, em geral, são multifuncionais. As linguagens de programação dispõem de recursos que facilitam a construção e manipulação de módulos, permitindo não só a modularização dos comandos do programa, como também dos dados utilizados. Cada módulo pode definir as próprias estruturas de dados, suficientes e necessárias apenas para atingir o objetivo final do módulo. Todo módulo é constituído por uma seqüência de comandos que operam sobre um conjunto de objetos, que podem ser globais ou locais. Objetos globais são entidades que podem ser usadas em módulos internos a outro módulo do algoritmo onde foram declaradas. Objetos locais são entidades que só podem ser usadas no módulo do algoritmo onde foram declaradas. Estes objetos não possuem nenhum significado fora deste módulo. São exemplos de objetos globais ou locais: variáveis, arquivos, outros módulos, etc. A comunicação entre módulos deverá ser feita através de vínculos, utilizando-se objetos globais ou transferência de parâmetros. 7.1. BENEFÍCIOS DA MODULARIZAÇÃO A independência do módulo permite uma manutenção mais simples e evita efeitos colaterais no restante do algoritmo; • A elaboração do módulo pode ser feita independentemente e em época diferente do restante do algoritmo; • Testes e correções dos módulos podem ser feitos separados; • Um módulo pode ser utilizado em outros algoritmos que requeiram o mesmo processamento por ele executado. 7.2. FERRAMENTAS PARA MODULARIZAÇÃO Sub-rotinas e funções são módulos que servem aos objetivos: • Evitar que em certa seqüência de comandos necessária em vários locais de um algoritmo tenha que ser escrita repetidamente nesses locais; • Dividir e estruturar um algoritmo em partes fechadas e logicamente coerentes; • Aumentar a legibilidade de um algoritmo. Sub-rotinas e funções são módulos hierarquicamente subordinados a um algoritmo, comumente chamado de módulo principal. Da mesma forma uma sub-rotina ou uma função pode conter outras sub-rotinas e funções aninhadas. A sub-rotina e a função são criadas através das suas declarações em um algoritmo e para serem executadas, necessitam de ativação por um comando de chamada. A declaração de uma subrotina ou função é constituída de um cabeçalho, que a identifica e contém seu nome e uma lista de parâmetros formais, e de um corpo que contém declarações locais e os comandos. Criação de sub-rotina subrotina NOME (lista-de-parâmetros-formais) declarações dos objetos locais a sub-rotina comandos da sub-rotina fim subrotina; Chamada da sub-rotina NOME (lista-de-parâmetros-atuais); As funções têm a característica de retornar ao algoritmo que as chamou um valor associado ao nome da função. 19 Criação de função função tipo NOME (lista-de-parâmetros-formais) declarações dos objetos locais a função comandos da função fim função; Chamada da função NOME (lista-de-parâmetros-atuais); Como esta função irá retornar um valor, este pode ser atribuído a alguma variável, contanto que esta seja de tipo compatível. A ← NOME (lista-de-parâmetros-atuais); Ao terminar a execução dos comandos de uma sub-rotina ou função, o fluxo de controle retorna ao comando seguinte àquele que provocou a chamada. 7.3. MODOS DE TRANSFERÊNCIA DE PARÂMETROS Os parâmetros de uma sub-rotina ou função classificam-se em: de entrada – são aqueles que têm seus valores estabelecidos fora da sub-rotina ou função e não podem ser modificados dentro dela. de saída – são aqueles que têm seus valores estabelecidos dentro da sub-rotina ou função. de entrada-saída – são aqueles que têm seus valores estabelecidos fora da sub-rotina ou função, mas podem ter seus valores alterados dentro dela. A vinculação entre módulos pode ser feita através da transferência ou passagem de parâmetros, que associam parâmetros atuais com parâmetros formais. Dentre os modos de transferência de parâmetros, pode-se destacar: a passagem por valor, a passagem por resultado e a passagem por referência. Na passagem de parâmetros por valor, as alterações feitas nos parâmetros formais, dentro da sub-rotina ou função, não se refletem nos parâmetros atuais. O valor do parâmetro atual é copiado no parâmetro formal, na chamada da sub-rotina ou função. Assim, quando a passagem é por valor significa que o parâmetro é de entrada. Na passagem de parâmetros por resultado, as alterações feitas nos parâmetros formais, na sub-rotina ou função, refletem-se nos parâmetros atuais. O valor do parâmetro formal é copiado no parâmetro atual, ao retornar da sub-rotina ou função. Assim, quando a passagem é por resultado significa que o parâmetro é de saída. Na passagem de parâmetros por referência, a toda alteração feita num parâmetro formal corresponde a mesma alteração feita no seu parâmetro atual associado. Neste caso, quando a passagem é por valor significa que o parâmetro é de entrada-saída. 8. RECURSIVIDADE Um objeto é dito recursivo se ele consistir parcialmente ou for definido em termos de si próprio. 20 Uma função é recursiva quando no corpo dessa função existe uma chamada a si própria, podendo utilizar os mesmos parâmetros de entrada (correndo riscos de provocar um ciclo infinito) ou outros. 8.1. EXEMPLO DE PROBLEMA RECURSIVO Imagine que temos um monte de pregos e queremos saber quantos são. Se pegarmos num prego, sabemos que temos um prego, mas não sabemos quantos ainda existem no monte restante... efetuamos a mesma operação (recursividade) e somamos o prego ao que já temos. Fazemos o mesmo até não existir mais pregos para contar, isto é, pegamos num e somamos aos que temos, repetimos a mesma operação perguntando sempre entre as operações, "ainda há mais pregos para contar?", caso haja, repetimos, caso contrário paramos. A recursividade é uma ferramenta muita poderosa quando bem implementada, senão pode ser muita perigosa. É preciso ter cuidado com as condições de parada, se faltar alguma condição de parada ou alguma condição de parada está errada pode acontecer um ciclo infinito. 8.2. RECURSÃO × ITERAÇÃO Paradigma iterativo: uma seqüência de instruções é executada de uma forma repetitiva, controlada por uma dada condição (ciclo iterativo). Paradigma recursivo: • existência de casos simples, em que a resposta é determinada diretamente; • ser possível uma decomposição recursiva de uma instância do problema, em instâncias mais simples da mesma forma. Numa função recursiva, são criadas várias ativações dela própria que desaparecem à medida que a execução avança. Em cada momento apenas uma das ativações está ativa, estando as restantes à espera que essa termine para continuarem. Os dois paradigmas são equivalentes: dada uma função recursiva existe sempre uma iterativa e vice-versa. 8.3. EXEMPLOS MAIS FAMOSOS DE PROBLEMAS RECURSIVOS Fatorial: Cálculo de n! = n x (n - 1) x...x 1 Seqüência de Fibonacci: 0 1 1 2 3 5 8 13 21 34 55 89 144 Exemplo de problema: Considerar uma população de coelhos que se reproduz segundo as seguintes regras: Cada par de coelhos produz um novo par por mês Os coelhos são férteis a partir do segundo mês Os coelhos não morrem Supondo que nasce um par de coelhos em Janeiro, quantos pares de coelhos existem no fim do ano? Algoritmo Determinar o número de pares em cada mês: 0 1 2 3 4 5 6 7 8 9 10 11 12 0 1 1 2 3 5 8 13 21 34 55 89 144 Generalizando, ao fim de n > 1 etapas temos: fn = fn - 1+fn - 2 e f0=0 e f1=1. 21 O Puzzle das Torres de Hanói: (inventado por Eduard Lucas (1880)) São dados três suportes (a, b e c) e n discos de tamanhos diferentes. Os discos estão empilhados num dos suportes por ordem crescente de tamanhos. Pretende-se mover os discos para outro suporte de modo que: em cada passo exatamente um disco seja movido de um suporte para o outro um disco não pode nunca estar por cima de um menor o terceiro suporte pode ser usado como auxiliar 9. APONTADORES É na memória RAM que são carregados os nossos programas e também onde são armazenadas as variáveis que fazem parte dos programas. A memória RAM pode ser vista como um enorme vetor de Bytes consecutivos, cada um ocupando uma posição bem determinada, que é identificada por um número único que varia entre 0 e a totalidade de Bytes. Para os programadores, é muito mais simples referenciar uma variável pelo seu nome do que referenciá-la pela posição que essa variável ocupa em memória. O compilador associa a cada nome de variável uma posição única em memória, capaz de suportar os dados do tipo dessa variável. Sempre que num programa se faz referência a uma variável, na realidade é o endereço ou conjunto de endereços que essa variável ocupa, que está sendo referenciado. O apontador é um mecanismo particularmente flexível de manipulação de dados, pois permite manipular diretamente dados contidos em endereços específicos de memória. Supondo que exista um apontador denominado ptr, que como qualquer variável ocupa uma posição em memória. Como ptr é um apontador, deverá conter o endereço de memória de outra variável (notar que o endereço de uma variável não é mais do que o número da casa que ocupa em memória). A Figura 3 mostra este exemplo. Figura 3: Exemplo de apontador. 22 10. LISTAS LINEARES Uma lista linear é uma estrutura dinâmica caracterizada por uma seqüência ordenada de elementos, no sentido da sua posição relativa: E1, E2, ..., En, onde: • Existem n elementos na seqüência; • E1 é o primeiro elemento da seqüência; • En é o último elemento da seqüência; • Para todo i, j entre 1 e n, se i < j, então o elemento Ei antecede o elemento Ej; • Caso i = j – 1, Ei é o antecessor de Ej e Ej é o sucessor de Ei. Exemplos de listas lineares: 1) Fila de clientes de um banco, onde existem o primeiro e o último da fila, e uma “ordem” de atendimento. 2) Pilha de processos de uma repartição a serem atendidos. 10.1. OPERAÇÕES EM LISTAS LINEARES Criar – uma estrutura dinâmica será criada durante a execução do programa; Destruir – depois de ser utilizada, a estrutura deve ser destruída; Percorrer – todos os elementos da lista podem ser utilizados, sendo que para isso a lista tem que ser percorrida; Buscar – um determinado elemento da lista pode ser identificado ou por sua posição, ou por seu conteúdo; Inserir – um novo elemento é colocado na lista numa determinada posição e n aumenta em 1; Remover – um elemento é retirado da lista numa determinada posição e n diminui em 1. Os diferentes tipos de listas lineares, possuem características especiais com relação a forma como são manipuladas. Filas - Uma fila (queue) é uma lista linear onde as operações de inserção são efetuadas apenas no final e as operações de retirada apenas no início, ou seja: • A inserção de um novo elemento X o torna o último da fila; • A retirada é sempre efetuada sobre o elemento E1. X P T O R Devido às características das operações da fila, o primeiro elemento a ser inserido será o primeiro a ser retirado. Estruturas deste tipo são chamadas de FIFO (First In, First Out). Pilhas - Uma pilha (stack) é uma lista linear onde tanto a operação de inserção, quanto a de retirada são efetuadas no final, ou seja: • A inserção de um novo elemento X o torna o último da pilha; • A retirada é sempre efetuada sobre o elemento En. P T O R 23 Devido às características das operações da pilha, o primeiro elemento a ser inserido será o último a ser retirado e o último a ser inserido será o primeiro a ser retirado. Estruturas deste tipo são chamadas de LIFO (Last In, First Out). 10.2. IMPLEMENTAÇÃO DE LISTAS LINEARES Alternativas: • Contigüidade física (com o uso de vetores) • Encadeada (com o uso de apontadores) Contigüidade Fila: 1 2 3 4 R 5 O 6 T 7 P 8 X início 9 10 fim Pilha: topo 6 5 4 3 2 1 X P T O R Encadeamento Simples R O T P X Exercícios: 1) Considere um conjunto de informações relativas a alunos, constituído de nome, número de matrícula e data de nascimento. Organize estas informações em uma lista encadeada, ordenada pelo nome do aluno. Escreva funções que efetuem as seguintes ações: • imprimir os nomes e números de matrícula dos alunos que nasceram após uma determinada data (passada como parâmetro); • procurar as informações relativas a um determinado aluno, cujo número de matrícula é passado como parâmetro; • incluir um novo aluno na lista, respeitando a ordenação. 2) Construa um procedimento que recebe uma lista encadeada (endereço inicial no apontador Lista) e monta uma nova lista a partir dos dados desta, com os elementos em ordem inversa. Somente a lista final deve estar alocada ao final da execução do procedimento. 3) Escreva um procedimento que recebe duas filas, que contém valores numéricos ordenados. O procedimento deverá formar uma terceira fila, também ordenada, na qual estarão os valores armazenados nas filas originais. Considere duas possibilidades: as filas implementadas sobre arranjos, e as filas implementadas através de apontadores.