Baixe o app para aproveitar ainda mais
Prévia do material em texto
BIBLIOGRAFIA BÁSICA: SZWARCFITER, Jayme Luiz; MARKENZON, Lilian, Estruturas de dados e seus algoritmos, 2. ed. Rio de Janeiro: LTC, 1997. KOFFMAN, Elliot B., WOLFGANG, Paul A.T., Objetos, Abstração, Estrutura de dados e Projeto usando C++, 1.ed. Rio de Janeiro: LTC,2008. EDELWEISS,N, GALANTE,R.M., Estrutura de Dados, Volume 18 – Série Livros Didáticos Informática UFRGS. 1.ed.RS: Bookman, 2009. BIBLIOGRAFIA COMPLEMENTAR: DEITEL, H. M; DEITEL, P. J. C++ Como programar . 5. ed. Rio de Janeiro: Prentice Hall, 2006. STROUSTRUP, Bjarne. Linguagem de programação C++. 3. ed. Porto Alegre: Bookman, 2002. CORMEN, Thomas H. et al. Algoritmos: teoria e prática. Rio de Janeiro: Campus, 2002. FORBELLONE, André Luiz Villar; EBERSPACHER,Henri Frederico. Lógica de Programação: A construção de Algoritmos e Estrutura de Dados. 3. ed. São Paulo: Makron Books, 2005. CELES, Waldemar; CERQUEIRA,Renato;RANGEL,José Lucas. Introdução a Estrutura de Dados com técnicas de programação em C. Rio de Janeiro : Elsevier, 2004. PINTO, Wilson Silva. Introdução ao desenvolvimento de algoritmos e estrutura de dados. 6.ed. São Paulo: Editora Érica, 1990. >>>> https://www.eecis.udel.edu/~portnoi/classroom/estrutura_dados/2007_1/estrutura_dados2007.1.html AULA 1: ESTRUTURA DE DADOS Conceito de estruturas de dados • Uma estrutura de dados pode ser definida como sendo uma coleção de variáveis, podendo ser tipos iguais ou diferentes, reunidas sob um único nome. Vários autores denominam estrutura de dados como sendo registros. (Mizrahi, 2006) HOMOGÊNEA: VETORES E MATRIZES; SUBPARTES DO MESMO TIPO. HETEROGÊNEA: REGISTROS; SUBPARTES DE TIPOS DIFERENTES. MATRIZ UNIDIMENSIONAL VETOR: • Matrizes e vetores permitem armazenar uma coleção de variáveis do mesmo tipo. • Enquanto a matriz possui varias dimensões, o vetor possui apenas uma dimensão. TIPOS DE DADOS E TIPOS ABSTRATOS DE DADOS: • Tipos de dados: são utilizados pelas linguagens de programação para definir o conjunto de valores que uma variável pode assumir. • Exemplos: – tipo de dado inteiro pode assumir valores como 1, 25, 1896, etc.; – tipo data pode assumir valores como “25/12/2015”, “07/09/1822”, etc. • Tipos abstratos de dados: são formados por um conjunto de tipos de dados e um conjunto de procedimentos (funções) que podem ser aplicados sobre este conjunto de tipos de dados. • Para que um tipo abstrato de dado seja implementado, são utilizadas as estruturas de dados. ÁRVORE, GRAFO, PILHA, FILA E LISTA: • Estruturas de dados classificação – Lineares e não lineares. • Estruturas de dados lineares – Listas, Pilhas, Filas • Estruturas de dados não linear – Árvores e Grafos LISTA: • Listas: são estruturas que permitem representar um conjunto de dados, que de alguma forma se relacionam, de forma que os elementos fiquem dispostos em sequência. (VELOSO, 1986) LISTA OPERAÇÕES MAIS COMUNS: • Criar • Verificar lista vazia • Verificar lista cheia • Inserir • Alterar • Remover • Busca • Exibir a quantidade • Combinar • Dividir lista • Ordenar • Esvaziar PILHA: • Pilha: é um tipo especial de lista onde os elementos a serem inseridos ou removidos ocorrem no topo da pilha. Esta característica é conhecida como LIFO (Last In, First Out Último a Entrar, Primeiro a Sair). (TANENBAUM; LANGSAM; AUGENSTEIN 1995) PILHAS OPERAÇÕES MAIS COMUNS: • Criar: cria uma pilha vazia. • Empilhar: insere um novo elemento no topo da pilha. • Desempilhar: remove um elemento do topo da pilha. • Exibir topo ou a quantidade. • Esvaziar: esvazia todos os elementos da pilha. FILA: • Fila: um tipo especial de lista, onde os elementos são inseridos em uma extremidade, chamada início da fila, e retirados na extremidade oposta, chamada final da fila. Esta característica é conhecida como FIFO (First In, First Out Primeiro a Entrar, Primeiro a Sair). FILAS OPERAÇÕES MAIS COMUNS: • Criar: cria uma fila vazia. • Enfileirar: insere um elemento no fim da fila. • Desenfileirar: remover um elemento no início da fila. • Exibir início: exibe o elemento do início da fila. • Exibir a quantidade: retorna a quantidade de elementos da fila. • Esvaziar: esvazia a fila. ÁRVORES: A árvore é composta de nós e arestas (conexões). GRAFOS: Aula 2: Funções MODULARIZAÇÃO: O grau de complexidade que alcança alguns programas nos leva a dividilo em módulos para que possamos melhorar o entendimento do programa, depurar os erros com mais facilidade, simplificar a manutenção porque podemos trocar somente um determinado módulo, reutilizar o código no mesmo programa ou em outro programa, etc. Esses módulos são “pequenos programas” com tarefas bem definidas. São conhecidos em algumas linguagens por procedimentos e, na linguagem C++, por funções. Chamamos essa técnica que decompõe um programa em partes menores de modularização. FUNÇÃO: Por que criar funções se a linguagem C++ apresenta um conjunto razoável de funções prédefinidas? Porque, por maior que seja esse conjunto, nunca atenderá, totalmente, às necessidades de todos os programas. 2.1 Conceito de Funções Uma função é um conjunto de comandos limitado por um par de chaves e precedido por um cabeçalho. Na construção de uma função, todas as estruturas que você estudou na disciplina de Algoritmos poderão ser usadas no corpo da função, mas uma função é independente da outra e, por essa razão, jamais poderá ser criada dentro de outra função. Definição da função: Entendemos que a criação de uma função é resultado de uma necessidade que você constatou e da ausência de uma função prédefinida. Alguns exemplos de funções que você poderá criar: Função que reajusta salário, Função que exibe um menu, Função que calcule a média aritmética, Função que gera um outro vetor, Função que ordena um vetor, Função que remove elementos de uma fila… 2.2 O Protótipo de uma Função O protótipo de uma função contém o tipo de retorno da função, o nome da função, seguido de um par de parênteses que podem ou não ter parâmetros. É finalizado, obrigatoriamente, com o ;(pontoevírgula). Em outras palavras, o protótipo de uma função é o cabeçalho da função com ;(pontoevírgula) ao final. <TIPO DE FUNÇÃO> NOME_DA_FUNÇÃO (DECLARAÇÕES DOS PARÂMETROS); 2.3 Localização das Funções As funções poderão ser colocadas antes, ou depois, da main. Se colocadas antes da main, elas serão “conhecidas” pelo compilador antes de serem chamadas, evitando maiores problemas, mas, à medida que o número de funções cresce, diminui a legibilidade do programa. Se colocadas depois da main, aumentamos a legibilidade, mas criamos um problema: função é chamada antes da identificação dela pelo compilador. Esse contratempo foi resolvido de uma forma muito simples: os protótipos das funções serão localizados antes da main e as definições, depois da main. ATENÇÃO: 1 Não confunda: Definição é o cabeçalho e o corpo enquanto que protótipo é o cabeçalho com ponto e vírgula ao final, podendo até se apresentar de forma simplificada como veremos mais adiante. 2 A prototipagem é uma técnica que declara funções através dos seus protótipos antes da main, indicando assim que as funções existem, mas se encontram em outro lugar. 2.4 Chamada da função e o seu retorno: Quando uma função é chamada, o fluxo de controle é desviado para essa função e, à partir desse momento, os comandos da funçãosão executados e, ao finalizar o fluxo retorna ao comando seguinte daquele onde ela foi ativada (se for void) ou ao ponto de onde ela foi chamada em funções com retorno. 2.5 Tipos de Funções 2.5.1 Função sem parâmetros Uma função que não retorna nada para a função chamadora é do tipo void e esse tipo de função pode ser considerado, hierarquicamente, abaixo da main, tendo em vista que ela faz tudo mesmo sem receber qualquer argumento. Vejamos alguns protótipos: void asteriscos(); void menu() ; void calculaReajuste(); ATENÇÃO: Você deve ter percebido que nada foi colocado entre os parênteses, mas poderia, embora seja dispensável, colocar a palavra void. Sendo assim, o primeiro protótipo poderia ser escrito da seguinte forma: void asteriscos(void); Uma função do tipo void, tendo ou não parâmetros, é chamada pelo nome, isto é, não precisará que um comando lhe anteceda. Se tiver parâmetros, eles estarão presentes entre os parênteses. nomeDafunção(...) Construa uma função que exiba 20 asteriscos: void asteriscos() { for(int x=1; x<=20; x++) cout<<"*"; } Construa uma função que exiba um menu com os seguintes itens: pilha, fila, lista, arvore, grafo: void menu() { system("cls"); cout<<"\nMenu\n"; cout<<"\n1Pilha"; cout<<"\n2Fila"; cout<<"\n3Lista"; cout<<"\n4Arvore"; cout<<"\n5Grafo"; cout<<"\n6Sair"; cout<<"\nOpcao: "; } Construa uma função que calcule o reajuste salarial de uma pessoa, tendo em vista um valor de resjuste: void reajuste() { float valor, percentual, reajustado; cout<<"\nDigite o valor que devera ser reajustado R$ "; cin>>valor; cout<<"\nDigite o valor do percentual de reajuste de 0 a 100: "; cin>>percentual; reajustado= valor + valor * percentual/100; cout<<"\nValor reajustado R$ "<<reajustado; } FUNÇÃO COM PARÂMETROS: 1 Passagem por valor Na passagem por valor, uma cópia do valor, ou valores, é passada para os parâmetros formais da função chamada através dos parâmetros reais da função chamadora. Sendo que os parâmetros reais poderão estar representados por variáveis ou constantes. A função chamada poderá operar esses valores, mas não alterará os valores da função chamadora. Suponha um autor de livro e um leitor. Ao tentar entender a solução de um problema, o leitor percebe que tem um erro. Ele corrige o erro, mas não corrige o original que está na editora, visto que ele recebeu uma cópia do livro. As funções prédefinidas que você viu em Algoritmos eram todas com passagem por valor. Funções com passagem por valor podem, ou não ter retorno. >>> Se a função tiver retorno, lembrese de que uma função só poderá retornar um valor para a função chamadora. 2 Passagem por referência Na passagem por referência, o endereço da variável da função chamadora é passado para a função chamada e dessa forma, o valor poderá ser alterado. Na linguagem C, essa passagem tinha que ser feita através do uso da variável ponteiro e acarretava, quando não operado corretamente, em muitos problemas. Tendo em vista os problemas causados pela manipulação dos ponteiros, a linguagem C++ criou um tipo novo de dado, chamado referência que nada mais é do que uma forma de atribuir um nome alternativo(apelido ou alias) para um objeto. Esse conceito aplicado aos parâmetros formais, parâmetros da função chamada, possibilitará que a função chamada receba o endereço do valor da função chamadora, alterandoo. Esse tipo de passagem ficou mais simples do que o uso de ponteiros porque o compilador se responsabiliza por tudo. Como indicar uma referência? O operador & símbolo de endereço na linguagem C é usado com outra conotação na linguagem C++ como afirma Saade, Joel:” ... assume o papel de declarador de referências”(pág. 112). void troca(float&a, float&b); Então, concluímos que a passagem por referência, na linguagem C++, poderá ser feita de duas maneiras: Passagem por referência por ponteiros: uma variável do tipo ponteiro não guarda dado, mas o endereço de uma variável. Suponha uma variável inteira de nome a e uma variável inteira do tipo ponteiro de nome pa. Assuma que a variável pa aponta para a variável a, significando que o conteúdo de pa é o endereço de a. Trabalhar com variável do tipo ponteiro implica no uso de dois operadores: &(endereço) e *(conteúdo do endereço apontado por pa). Passagem por referência: >>> referência não é ponteiro mas ambas manipulam endereço. FUNÇÃO SIGNIFICADO void linha(char c, int n); Função que recebe dois argumentos por passagem de valor. Um do tipo char e outro do tipo int, mas não retorna nada para função chamadora. int descobreIdade(int anoAtual, int anoNas); Função que recebe dois argumentos por passagem de valor. Os dois do tipo inteiro e retorna, para a função chamadora, um valor inteiro. float areaRetangulo(float, float); Função que recebe dois argumentos reais e retorna, para a função chamada, um valor real. void troca(float& , float&); Função que recebe dois argumentos que são endereços que armazenam números reais, por passagem por referência. A função não retorna nada para a função chamadora. Uma função que retorna valor deverá ser chamada através de um comando, não importando se ela tem, ou não, parâmetros. O comando return Uma função que retorna valor terá, obrigatoriamente esse comando no corpo da função. Isso não quer dizer que não poderá ter mais de um comando return no corpo da função. Quando existir múltiplas respostas, poderemos ter vários tipos de retorno. Pode estar presente uma variável, uma constante, uma expressão. variável = nomeDafunção(...); cout<<nomeDafunção(...); if(nomeDafunção(...)...); >>> Fique atento. Uma função só poderá retornar um valor! Construa uma função que receba valores que correspondem ao caracter e à quantidade de vezes que se deseja exibilo: void linha(char c, int n) { for(int x=1; x<=n; x++) cout<<c; } Construa uma função que receba valores que correspondem ao ano atual e ao ano de nascimento de uma pessoa, retornando a idade que a pessoa terá até 31 de dezembro: int descobreIdade(int anoAtual, int anoNas) { return anoAtual anoNas; } Construa uma função que receba valores que correspondem à base e à altura de um retângulo e retorne a área: float areaRetangulo(float b, float h) { return b*h; } Construa uma função que possa trocar valores de duas variáveis: void troca(float& a, float &b) { float aux; aux=a; a=b; b=aux; } Qualquer função poderá chamar outra função desde que a função chamada já esteja declarada, ou definida. Embora, a princípio, possa parecer mais simples, é sempre bom declarar as funções porque você não fica limitado à sequência das definições. Para dar maior legibilidade, seria melhor definir depois da main e, para isso, teria que declarar todas as funções. Já vimos que para declarar, é só colocar o protótipo antes da main. Variáveis Locais: variáveis declaradas dentro das funções. A esse tipo de declaração, onde as variáveis só são visualizadas nas funções onde foram declaradas, chamamos de variáveis locais. Variáveis Globais: Variáveis declaradas fora do escopo de todas as funções são chamadas de variáveis globais. Esse tipo de variável poderá ser manipulado por qualquer função. E como fica a análise da função main()? Ao longo dos anos, vários ajustes foram feitos para que existisse um padrão. O tipo int foi escolhido embora pudesse ser omitido. Mais tarde, aconteceu a remoção do int implícito.Sendo assim, o cabeçalho da main passou a ser: int main() Já vimos que é opcional o uso da palavra void entre os parênteses quando a função não tem parâmetros. Se a main é do tipo int, então tem retorno. Porque funciona sem return? A main é uma função chamada pelo sistema operacional e, ao finalizar, retorna, automaticamente, para o S.O. Sendo pontual, o return da main poderia ser dispensável, visto que ele serve para informar que o programa foi executado com sucesso e isso, podemos constatar. Entretanto seu retorno poderá ser usado pelo sistema operacional, mas isso é uma outra história. >>> Um bom hábito é escrever return 0; ou return EXIT_SUCCESS; em projetos maiores. Passando uma estrutura de dados homogênea para uma função: O armazenamento na MP de um vetor apenas é armazenado o primeiro endereço do primeiro elemento do vetor. Considerações : 1 O endereço do 1o elemento do vetor é igual ao endereço do vetor. 2 Obtémse o mesmo valor quando se pede para exibir o endereço do vetor e exibir o valor do vetor. PROTÓTIPO SIGNIFICADO double media(double [ ], int tam); Função que recebe dois argumentos. O primeiro é o endereço de um vetor do tipo real de dupla precisão (double). Enquanto que o segundo, por passagem de valor, recebe um número inteiro que corresponde ao tamanho do vetor. A função retorna um valor de dupla precisão. int contaMaioresQueH(double [ ], int tam, double altP); Função que recebe três argumentos. O primeiro é o endereço de um vetor do tipo real de dupla precisão (double). O segundo, por passagem de valor, recebe um número inteiro que corresponde ao tamanho do vetor e o terceiro, recebe, por passagem de valor, um número de dupla precisão (double) que corresponde a uma altura específica. A função retorna um valor inteiro. void arredonda(double a[ ], int tam); Função que recebe dois argumentos. O primeiro é o endereço de um vetor do tipo real de dupla precisão(double). Enquanto que o segundo, por passagem de valor, recebe um número inteiro que corresponde ao tamanho do vetor. A função não retorna valor. void dobro(double [ ], double [ ], int tam); Função que recebe três argumentos. Os dois primeiros são endereços de dois vetores do tipo real de dupla precisão (double). Enquanto que o terceiro, por passagem de valor, recebe um número inteiro que corresponde ao tamanho do vetor. A função não retorna valor. Construa uma função que receba o endereço um vetor que armazena notas dos alunos de uma turma e seu tamanho, retornando a média da turma: double media(double n[ ], int tam) { double soma=0; for(int x=0; x<tam;x++) soma+=n[x]; return soma/tam; } Contrua uma função que receba o endereço de um vetor que armazena alturas de um grupo de atletas, seu tamanho e a altura mínima de corte, retornando a quantidade de atletas com altura superior ou igual ao corte. int contaMaioresQueH(double alts[ ], int tam, double altP) { int conta=0; for(int x=0; x<tam;x++) if(alts[x]>=altP) conta++; return conta; } Construa uma função que receba o endereço um vetor que armazena as notas da AV1 de uma turma e seu tamanho. A função deverá arredondar as notas para cima. void arredonda(double a[ ], int tam) { for(int x=0; x<tam;x++) a[x]=ceil(a[x]); } Construa uma função que receba endereços de dois vetores e o tamanho deles. A função deverá gerar o vetor dobro. void dobro(double v1[ ], double v2[ ], int tam) { for(int x=0; x<tam;x++) v2[x]= v1[x] * 2; } ESCOPO DE VARIÁVEIS (LOCAL E GLOBAL): PALAVRAS RESERVADAS: FUNÇÕES E VETORES COMO PARÃMETROS: Aula 3: Estruturas Heterogêneas Podemos definir uma estrutura como sendo um conjunto de elementos, geralmente, agrupados sob uma lógica e associados por um nome. Esses elementos podem ser variáveis simples, matrizes, outras estruturas e até funções. Por essa definição, podemos concluir que uma estrutura pode ser formada por elementos de tipos diferentes. Cada elemento da estrutura é chamado de membro ou campo. A definição termina com um ; porque é um comando. É bom ressaltar que nenhuma variável foi declarada. Estruturas poderão ser definidas globalmente ou localmente. A declaração da estrutura pode ser feita antes ou depois da definição. Existe ainda a estrutura anônima: Um membro da estrutura pode ser acessado usando o operador membro da estruturas(.), chamado operador ponto, colocado entre o nome da variável estrutura e o nome do membro. nomeDaVarávelEstrutura.nomeDoMembro EX: Nome da variável estrutura: a Nome do primeiro membro: x Nome do segundo membro: y Acessando o membro x da variável estrutura a: a. x Acessando o membro y da variável estrutura a: a. y Nome da variável estrutura: b Nome do primeiro membro: x Nome do segundo membro: y Acessando o membro x da variável estrutura b: b. x Acessando o membro y da variável estrutura b: b. y 1º método: Atribuindo valores aos membros Usando comando de atribuição na hora da declaração das variáveis. Atenção para as chaves e para a sequência correta dos dados. struct coordenadas { int x, y; }; . . . coordenadas a={5, 2}; coordenadas b={ 6, 5}; 2º método: Atribuindo valores aos membros Atribuição na hora da definição/declaração das variáveis struct prod { char nomeProd[21]; float valor; }produto1={“martelo”, 35.90}, produto2={“furadeira”, 256.75}; Neste exemplo, apresentamos uma das grandes vantagens do uso da variável estrutura na programação: a facilidade de se atribuir todo o conteúdo de uma variável estrutura a outra variável estrutura. Você deve estar lembrado que precisávamos usar strcpy para copiar vetores de char e comandos de atribuição para as demais variáveis. Usando variável estrutura, com um único comando de atribuição, copiamos todos os conteúdos dos membros mesmo que eles sejam vetores de char. Observe como teria ficado o programa sem o uso da variável estrutura. Imagine se a variável estrutura tivesse 10 membros? 3º método: Atribuição através da leitura via teclado struct prod { char nomeProd[21]; float valor; }produto1, produto2; EXEMPLOS: CONSTRUINDO UM ARRAY DE ESTRUTURA: 1º método Da mesma forma que declaramos as estruturas simples, podemos declarar os arrays de estruturas. A declaração é feita depois da definição. Se a definição for global, a declaração acontece em cada função. Struct <identificador> { … }; <identificador> nomeDaVariável; Struct alCad { … }; alCad alunos[15]; 2º método A declaração é feita junta com a definição. Struct <identificador> { … } nomeDaVariável; Struct alCad { … } alunos[15]; Acessando um elemento do array: Precisamos ter muito cuidado quando formos acessar um array de estruturas porque primeiro temos que escolher a variável do array como fazíamos com os arrays unidimensionais. Depois, acrescentamos separado por um ponto, os membros da estrutura. Struct prodCad { int codigo; float precoCompra, precoVenda; }; prodCad produtos[1000]; EXEMPLOS: Exemplo 1 Tomando como base o array de estruturas, construa um programa que armazene código e preço de compra de 1000 produtos, calcule um reajuste de 60% para venda e exiba código, valor de compra e venda. Observação:Para que você pudesse visualizar a entrada e a saída, foi usada a diretiva define que criou a constante TAM com valor 3 (deveria ser 1000) Exemplo 2 Construa um array de estruturas com duas variáveis. O único membro da estrutura é um array de caracter (vetor de char) que deverá ser capaz de armazenar até 20 caracteres sem incluir o terminador. Os valores deverão ser lidos via teclado. Na saída, cada letra de cada vetor de char deverá ser exibida em uma linha, sendo que uma linha em branco separará as letras dos dois vetores. Além disso, a primeira letra da primeira palavra tem que estar em maiúscula mesmo que o usuário não tenha digitado assim. Na maioria dos exemplos anteriores, já tivemos como membro de uma estrutura, um vetor de char. Além disso, estudamos na disciplina de Algoritmos que o vetor de char tem um tratamento diferenciado dos demais vetores unidimensionais, pois ele é declarado como um vetor e tratado como uma variável simples exceto quando precisamos manipular uma letra de cada vez. Observe a visualização de um vetor de char e as saídas que obteremos com os comandos abaixo. Atente para os parênteses usados quando desejamos exibir um caracter do vetor. *** Você terá que usar a função toupper() da biblioteca cctype para converter para maiúscula a primeira letra da primeira palavra. Observe a linha 36 do código fonte. Você poderá refazer o trecho que exibe uma letra em cada linha, usando a função strlen() da biblioteca cstring. Preferimos controlar usando o terminador \0. Observe que a estrutura do for não está sendo controlada pela variável y logo, neste exemplo, está sendo usada como se fosse a estrutura while. Experimente trocar pelo trecho abaixo e veja se faz alguma diferença. Nosso array só terá dois elementos para que você possa visualizar a saída. Observe a definição/ declaração e a visualização do array de estruturas. Reforçando os conceitos: A estrutura criada tem nome : cad. A estrutura só tem um membro cujo nome é: pal. O nome do array de estruturas do tipo cad é: palavra. Esse array tem duas variáveis estruturas: palavra[0] e palavra[1]. Para acessar o único membro da estrutura, a variável palavra[0] precisa ser unida por um “.” ao membro pal, ficando assim: palavra[0].pal. Da mesma forma, palavra[1].pal. Quando usarmos um dos nomes acima com cout, exibirá na tela todo o conteúdo de um dos elementos do vetor. Quando desejarmos acessar uma letra de uma das variáveis estruturas, precisaremos usar a seguinte sintaxe: nomeDoArray[deslocamento array].membro[deslocamento na variável] Exemplo: Se você desejar a terceira letra da segunda palavra: palavra[1].pal[2]. Estruturas aninhadas Muitas vezes, definimos estruturas que podem ser membros de várias estruturas. Em qualquer cadastro, por exemplo, tem data: data de nascimento, data de pagamento, data de recebimento, data de entrega, etc. Sendo assim, poderíamos definir uma estrutura data e visualizála da seguinte forma: E usarmos esta estrutura com membro de outra estrutura que iremos definir/declarar e visualizar assim: Acessando um membro de uma estrutura dentro de um array de estruturas: nomeDoArrayl[deslocamento no array].nomeDaEstruturaAninhada.membro Exemplo Acessando o dia da variável estrutura de deslocamento 1 = promissorias[1].venc.dia Vamos construir um programa para armazenar valores nesse array de estruturas aninhadas que declaramos acima. Na aula passada, estudamos que o uso de funções melhora a legibilidade e a manutenibilidade do programa logo, chegou o momento de criarmos funções com/para estruturas. Você verá algumas coisas interessantes que poderemos fazer ao construirmos funções com estruturas como por exemplo: estruturas poderão ser retornos de função, estruturas poderão conter função dentro delas além de tudo que já foi feito com os arrays homogêneos. Passagem por valor Da mesma forma que passamos o conteúdo de uma variável simples ou de um elemento do vetor, podemos passar um membro (campo) de uma estrutura. Vale a pena relembrar que, na passagem por valor, passamos uma cópia do conteúdo logo, o valor original não se altera. Neste exemplo, chamaremos a função três vezes, passando os membros da estrutura de tal maneira que retorne o maior dos quatro números. Passagem por referência Quando estudamos funções, para quem já tinha visto a linguagem C, notou que a passagem por referência simplificou o uso da passagem por endereço(ponteiros). Por isso, citamos SAADE, Joel(2003, p.112): “Podese dizer que a passagem por referência é uma forma disfarçada de ponteiro”. Neste exemplo, passaremos o endereço de um membro do struct por referência. A função irá calcular a idade da pessoa em 2020 e irá alterar o valor do membro. Passagem de um membro que é um vetor passagem por endereço Os arrays são em verdade, endereços tanto é que o que fica entre parênteses é o deslocamento em relação ao endereço base. Sendo assim, a passagem de um membro que é um vetor se dá de mesma forma que os arrays unidimensionais. Perceba que o endereço da estrutura é o mesmo da primeira variável do array de estruturas, pois o primeiro elemento do array tem valor 0(zero). E o endereço da segunda variável está distante 31 posições. Em hexadecimal, 10 equivale em decimal a 16(1*16 + 0) enquanto 2f seria equivalente a 47 (2*16 + 15). e 47 16 = 31. Estrutura como parâmetro de uma função Estrutura como valor de retorno de função Neste exemplo, apresentaremos uma função do tipo estrutura logo, terá como retorno, uma estrutura. Funções como membros de estrutura apresentaremos uma estrutura cujos dois membros são funções. Uma com retorno e outra, sem retorno. Já que provamos que o vetor é um endereço, vamos ao exemplo onde passaremos um membro da estrutura que é um vetor. Aula 4: Ordenação e Pesquisa if(vet[0]>vet[1]) { aux=vet[0]; vet[0]=vet[1]; vet[1]=aux; } if(strcmp(vet[0],vet[1]) > 0) { strcpy(aux,vet[0]); strcpy(vet[0],vet[1]); strcpy(vet[1],aux); } Cada caracter ocupa um endereço na MP e se pedirmos para exibir o nome de um vetor de char, sairá o primeiro endereço de todos os endereços que são ocupados pelo conjunto. Por essa razão, foi criado um conjunto de funções para manipularem vetores de char e nesse nosso estudo, usaremos duas delas: strcmp( abreviatura de string compare) cuja função é comparar dois vetores de char e strcpy(abreviatura de strng copy) cuja função é copiar o conteúdo de um vetor de char nas posições ocupadas por outro vetor de char. Essas funções foram estudadas na AULA10 da disciplina de Algoritmos. Comparando Struct: if(candidatos[0].ninsc>candidatos[1].ninsc) { aux=candidatos[0]; candidatos[0]=candidatos[1]; candidatos[1]=aux; } OS MÉTODOS DE ORGANIZAÇÃO Ordenar significa dispor elementos de um conjunto seguindo um determinado critério. Este critério estará baseado em um atributo do elemento do conjunto (nome, matrícula, salário, coeficiente de rendimento, etc.) ORDENAÇÃO INTERNA E EXTERNA Quando o método de organização utiliza apenas a memória principal para realizar o processo de ordenação, é classificado como ordenação interna, mas se usa uma memória auxiliar porque o arquivo é muito grande, é uma ordenação externa. MÉTODOS DE ORDENAÇÃO INTERNA SELEÇÃO:INSERÇÃO: void insercao(int vet[ ], int tam) { int j, i, aux; for ( i=1; i<tam; i++ ) { aux = vet[i]; for ( j=i; j>0 && aux <vet[j1]; j ) vet[j] = vet[j1]; vet[j] = aux; } } Observe que o for mais externo começa com a segunda posição do vetor porque o for de dentro sempre compara com os elementos anteriores. O teste de j>0 se justifica porque dentro dos colchetes, ele subtrai de 1 o valor de j e se deixasse j chegar a zero, o índice ficaria negativo o que não é permitido na linguagem C++. O elemento fundamental do for interno é o teste: j>0 && aux <vet[j1] porque incorpora um teste interrompendo a repetição quando a condição aux <vet[j1] não for mais atendida. O for usado desta forma é uma herança da linguagem C e talvez mais fácil de ser construído do que a estrutura do while. Nada contra a estrutura do while, mas como já havia dito, tentamos manter uma uniformidade na apresentação desses métodos. BOLHA: VALORES ADJACENTES SÃO COMPARADOS DE FORMA CONTÍNUA DANDO A IMPRESSÃO DE UM BORBULHAMENTO. void bolha(int vet[ ], int tam) { int j,i, aux; for (i=0; i<tam 1; i++) for(j=tam1; j>i; j) if(vet[j] < vet[j1] ) { aux=vet[j]; vet[j]= vet[j1]; vet[j1]=aux; } } SELEÇÃO: Ideal para arquivos pequenos. Muito simples. Não melhora sua performance se o arquivo já estiver ordenado. Tem menos movimentos do que o Insertion Sort. INSERÇÃO: Só ordena quando necessário. Quando um elemento é inserido, todos os elementos maiores do que ele, são deslocados para a direita. É considerado o melhor dos três métodos estudados. BOLHA: É o mais conhecido método. É muito simples. Muito lento. É considerado o pior dos três estudados. SELEÇÃO: INSERÇÃO: BOLHA: PESQUISA SEQUENCIAL: PRIMEIRO TRECHO: SEGUNDO TRECHO: cout<<"\n...? "; cin>>varProcura; achou=0; for(L=0;L<tamLinha;L++) { if(varProcura==vetor[L]) { achou=1; posicao=L; } } if(achou==1) cout<<"\n...: "<<outroVetor[posicao]<<endl; else cout<<"\nDado nao achado\n"; cout<<"\n...? "; cin>>varProcura; achou=0; for(L=0;L<tamLinha && achou==0;L++) { if(varProcura==vetor[L]) { achou=1; posicao=L; } } if(achou==1) cout<<"\n...: "<<outroVetor[posicao]<<endl; else cout<<"\nDado nao achado\n"; Esse trecho fará uma varredura em toda a matriz e mesmo depois de achar, continuará até o fim. Para melhorar o trecho anterior, vamos introduzir um teste da estrututra for, assim que o elemento for achado o for será interrompido. TRECHO DA PESQUISA BINÁRIA: inicio=0; fim= tamanho 1; meio=(inicio+fim)/2; while(procura != nomeVetor[meio] && inicio != fim) { if(procura > nomeVetor[meio]) inicio=meio+1; else fim=meio; meio=(inicio+fim)/2; } if(nomeVetor[meio]==procura) cout<<"\n....: "<<outroVetor[meio]<<endl; else cout<<"\nDado nao encontrado\n"; AULA 5 A ESTRUTURA DE DADOS LISTAS “A estrutura que permite representar um conjunto de dados de forma a preservar a relação de ordem linear (ou total) entre eles é a lista linear. Uma lista linear é composta de nós, os quais podem conter, cada um deles, um dado primitivo ou um dado composto.” (VELOSO,P.,SANTOS,C., AZEREDO,P., FURTADO, A., 1983,79) Nó ou nodo – é um item da lista. Comprimento ou tamanho de uma lista – números de nós de uma LISTA. Exemplo de uma LISTA de comprimento 10 porque tem 10 nós. Se “uma Lista é composta de nós” então, uma Lista Vazia é uma lista que não tem nós. Os elementos de uma lista linear podem ser agrupados de duas formas: Sequencial: Esse tipo de estrutura apresenta os nós em posições contíguas de memória, isto é, um após o outro como já vimos quando estudamos matrizes na disciplina de Algoritmos. Sendo assim, fica fácil identificarmos o endereço de qualquer nó de uma Lista Sequencial. • Vantagens – Qualquer elemento da lista pode ser acessado direto indexado. – Tempo constante para acesso a qualquer elemento da lista. • Desvantagem – Movimentação de todos os elementos da lista quando há uma inserção ou remoção de elemento. – O tamanho máximo da lista deve ser préestimado. Encadeado: Um conjunto de nós (nodos) encadeados onde cada nó contém o dado e um apontamento para o próximo nó, visto que eles não estão alocados de forma contígua. • Vantagens – Facilidade de inserir ou remover um elemento em qualquer ponto da lista. – Não há a necessidade de movimentar os elementos da lista quando há uma inserção ou remoção de elemento. • Desvantagem – Por utilizar ponteiros, a implementação deve ser feita com muito cuidado para que não ocorra um mal encadeamento (ligação) dos nós e consequentemente a lista seja perdida. – Encontrar um determinado elemento na posição n da lista é necessário percorrer os n1 anteriores. – Necessidade de memória extra para armazenamento dos ponteiros. Esta classificação diz respeito à forma como os dados são armazenados na Memória Principal. Entretanto, não pode ser confundido com Alocação Estática ou Alocação Dinâmica. Alocação Estática é determinada durante a compilação. Já na Alocação Dinâmica, você poderá determinar o tamanho, ou acrescentar, durante a execução. Reunindo estas classificações, podemos concluir que existem quatro possibilidades de Alocação de Memória. A Lista Linear Sequencial é ideal para um conjunto pequeno de dados. Apresenta a vantagem do acesso ao nó pelo índice e a desvantagem da movimentação quando se remove dado e insere se desejarmos deixar os vazios ao final. A forma de acesso à LISTA no que diz respeito à inserção e à remoção de um dado determina uma classificação Pilha A inserção e a remoção é sempre realizada em um extremo da lista. Fila – A inserção é feita em um extremo e a remoção em outro. Fila Dupla – DEQUE( DoubleEnded QUEue), significando fila de extremidade dupla. FDER – Fila Dupla de Entrada Restrita – Nesse tipo de Fila, a remoção do elemento pode acontecer em qualquer extremidade, mas a inserção, em apenas uma. FDSR – Fila Dupla de Saida Restrita – Nesse tipo de Fila, a inserção do elemento pode acontecer em qualquer extremidade, mas a recuperação, em apenas uma. Inicializar uma lista é atribuir zero à variável que será responsável por armazenar a quantidade de dados que já se encontram na lista. OPERAÇÕES BÁSICAS COM LISTAS SEQUENCIAIS • Criar: • Verificar lista vazia: • Verificar lista cheia: • Inserir: • Alterar: • Remover: • Buscar: • Exibir a quantidade: • Combinar: • Dividir lista: • Ordenar: • Esvaziar: EXEMPLOS APLICAÇÃO: APLICAÇÃO: #include <iostream> using namespace std; void insere(int IDS[ ], int idade, int &t, int tamanho); void exibe(int IDS[ ], int t); void selecao(int IDS[ ], int t); void frequencias(int IDS[ ], int t); int main() { int tam, op, f3=0, idades[10], id; //Inicialização tam = 0; do { system("cls"); cout<<"\nMenu 2 LISTA \n"; cout<<"\n0 Reinicar a LISTA"; cout<<"\n1 Inserir idade na lista"; cout<<"\n2 Exibir lista"; cout<<"\n3 Ordenar lista";cout<<"\n4 Exibe frequencia"; cout<<"\n5 Sair"; cout<<"\nOpcao: "; cin>>op; system("cls"); switch(op) { case 0: //reinicialiação tam = 0; break; case 1: cout << "\nDigite idade(1019) a ser inserida na Lista: "; cin >> id; while(id<10 || id >19) { cout<<"\nAtencao para o intervalo."; cout<<"Digite idade(1019) a ser inserida na Lista: "; cin>>id; } insere(idades,id,tam, 10); break; case 2: exibe(idades,tam); break; case 3: //ordena Metodo selecao poderia ser insercao ou bolha selecao(idades, tam); f3=1; break; case 4:if(f3==1) frequencias(idades,tam); else cout<<"\nOrdene primeiro o vetor\n"; break; case 5: cout<<"\nFinalizando o programa da LISTA\n"; break; default: cout<<"\nOpcao invalida\n"; } cout<<"\n\n"; system("pause"); }while(op !=5); } void insere(int IDS[ ], int idade, int &t, int tamanho) { if (tamanho == t) cout << "\nAtencao! Lista cheia\n"; else { IDS[t] = idade; t++; } } void exibe(int IDS[ ], int t) { int x; if (t == 0) cout << "\nAtencao! Lista vazia\n"; else for(x = 0; x < t; x++) cout << "\n" << IDS[x]; } void selecao(int IDS[ ], int t) {int i, j, menor, temp; if (t == 0) cout << "\nAtencao! Lista vazia\n"; else { for(i=0; i<t 1; i++) { menor=i; for(j=i+1; j<t ; j++) if(IDS[j] < IDS[menor]) menor=j; temp=IDS[i]; IDS[i]=IDS[menor]; IDS[menor]=temp; } } cout<<"\nLista Ordenada\n"; } void frequencias(int IDS[ ], int t) { int i, c, frequencia[ ]={0,0,0,0,0,0,0,0,0,0}; if (t == 0) cout << "\nAtencao! Lista vazia\n"; else { for(i=0;i<t;i++) { frequencia[IDS[i]10]++; cout<<"\n"<<IDS[i]<<"\t"<< frequencia[IDS[i]10]; //SÓ PARA FINS DIDÁTICOS } // exibe frequência dos números sem repetição cout<<"\n\nIdade\tFrequencia\n"; cout<<"\n"<<IDS[0]<<"\t"<< frequencia[IDS[0]10]; for(i=1; i<t; i++) if (IDS[i] != IDS[i1] ) cout<<"\n"<<IDS[i]<<"\t"<< frequencia[IDS[i]10]; } } APLICAÇÃO: ESTRUTURAS HOMOGÊNEAS CRIAR ESTRUTURAS HOMOGÊNEAS INSERIR ESTRUTURAS HOMOGÊNEAS INSERIR EM UMA POSIÇÃO DETERMINADA ESTRUTURAS HOMOGÊNEAS EXIBIR TODA A LISTA PESQUISAR UM DETERMINADO ELEMENTO E RETORNAR SUA POSIÇÃO REMOVER UM ELEMENTO EM UMA POSIÇÃO DETERMINADA ESTRUTURA HETEROGÊNEA CRIAR ESTRUTURA HETEROGÊNEA INSERIR INSERIR UM ELEMENTO EM UMA POSIÇÃO DETERMINADA ESTRUTURAS HETEROGÊNEAS EXIBIR TODA A LISTA PESQUISAR UM DETERMINADO ELEMENTO E RETORNAR SUA POSIÇÃO REMOVER UM ELEMENTO EM UMA POSIÇÃO DETERMINADA Aula 6: A Estrutura de Dados – Pilha Nesta aula, estudaremos a Pilha uma das mais simples Estruturas de Dados, mas com muitas aplicações. Lendo a definição acima, podemos concluir que a remoção do elemento acontece num processo inverso ao da inserção, isto é, o último a entrar é o primeiro a sair. Este critério que determina a sequência de entrada e saída desta forma, chamase LIFO( Last In First Out). Observe a figura abaixo: Você percebe que o número 30 foi o último a entrar na Pilha e passa a se posicionar no topo. Na remoção, o número 30 é o primeiro a sair. Uma das características da Pilha é que a inserção, remoção e acesso acontecem somente em uma extremidade como você pode observar na figura. Normalmente, apresento o vetor como uma matriz linha, mas para deixar mais parecido com uma Pilha, apresenteio como matriz coluna e ainda dei um giro de 180º. Tudo pelo “bem da Pilha”. O número de operações que pode ser realizado com uma Pilha é muito pequeno, tendo em vista o critério LIFO. Três operações são consideradas básicas e, vou colocálas no início, mas as demais também são necessárias para que as primeiras possam funcionar perfeitamente ou para fornecer alguma informação. Inicializa() ou Init() Inicializa a Pilha Empilhar() ou Push() Insere no topo da Pilha um elemento Desempilhar() ou Pop() Remove do topo da Pilha um elemento acessoTopo() ou Top() Tem acesso ao elemento que se encontra no topo da Pilha verificaPilhCheia() ou isFull() Verifica se Pilha está cheia verificaPilhVazia() ou isEmpty() Verifica se Pilha está vazia Muitos autores não destacam as três últimas operações porque, normalmente, elas estão embutidas nas demais ou, se não estiverem, um comando if, ou de atribuição, pode resolver o problema. Nós vamos construir trechos para uma Pilha que poderá armazenar até 5 números inteiros. Estarei apresentando, quando existir, o trecho que chama a função e a função. INICIALIZAR A inicialização da Pilha irá se resumir a um comando de atribuição, preparandoa para receber os elementos que serão inseridos. Como na Pilha, o topo, isto é, a posição mais alta da Pilha é de maior importância, uma variável sempre deverá conter este valor. Sendo assim, para inicializar uma Pilha, precisaremos colocar um valor que não seja possível ser índice de vetor na linguagem C++. Gostaria de enfatizar que a inicialização não significa zerar todos os elementos da Pilha, mas é como se deixássemos engatilhado um ponteiro que seria acionado assim que o primeiro valor entrasse na pilha. Em programação, usamos uma variável e a inicialização foi feita na declaração. Observe o trecho: EMPILHAR (PUSH) Esta é uma operação básica da Pilha. Para que possa ser empilhado um elemento, é preciso testar se existe lugar na Pilha. Verificar se a Pilha está cheia poderia ser feito em separado, mas preferi fazer junto, pois se resume a um if, tornandose mais rápido do que chamar uma função. Além disso, uma chamada de função implica em colocar o endereço da instrução seguinte em uma Pilha para que o retorno seja possível e, se a função for algo muito simples, talvez não valha a pena ser criada. Foi assim que pensei e por essa razão reuni duas operações nesta função. Observe. >>> A função recebe o endereço do vetor, o endereço da variável que contém a posição do elemento que se encontra no topo e o valor a ser inserido. Os dois primeiros parâmetros precisam ter seus endereços passados porque serão alterados dentro da função. Observe que antes de inserir na Pilha, a variável t é incrementada porque o topo passa a ser o valor que será inserido na instrução abaixo. É uma função de fácil implementação. Acompanhe agora, na figura abaixo, supondo que você digitou cinco números para serem empilhados. DESEMPILHAR (POP) Esta é outra operação básica da Pilha. Para que possa ser desempilhado um elemento, é preciso testar se a Pilha não está vazia. Verificar se a Pilha está vazia poderia ser feito em separado, mas preferi fazer junto, pois se resume a um if, tornandose mais rápido do que chamar uma função. Esta função é do tipo int, mas poderia ser do tipo void. >>> A função recebe o endereço do vetor, o endereço da variável que contém a posição do elemento que se encontra no topo e o endereço da variável que vai receber o valor que será “desempilhado”. Todos os parâmetros precisam ter seus endereços passados porque serão alterados dentro da função. Observe que depois de copiar o conteúdo do elemento que estava notopo da Pilha, a variável t é decrementada. Em nenhum momento foi removido o valor que estava armazenado na posição. Só o topo que se deslocou. Sanou sua dúvida agora? Assim como a função empilhar, a função desempilhar é de fácil implementação e bem parecida. Acompanhe a agora, na figura abaixo, supondo que você escolheu desempilhar cinco números. ACESSO AO TOPO (TOP) A única posição que poderá ser acessada na Pilha é a que está no topo da Pilha. Esta é uma função que possibilita que você visualize o elemento do topo sem “removêlo”. Na verdade, você não precisaria criar esta função tal sua simplicidade. Poderia simplesmente colocar no case. Só criei para modularizar todos os casos. acesso topo(pilha, topo); void acessoTopo(int p[ ], int &t) { if(t == 1) cout<<’\nATENCAO. Pilha Vazia\n”; else cout<<”\nElemento do topo da PILHA: “<< p[t]; Esta função testa se a Pilha está cheia ou se a Pilha está vazia e, se não for nenhuma das duas opções, exibirá o total de elementos na Pilha e o espaço disponível. Você poderá escolher qual dos dois desejará exibir. Como todas as seis operações já foram apresentadas, vou apresentar mais uma. Nada importante, mas que poderá ser útil em algum momento. Assim como a função anterior, você não precisaria criála. situacaoPilha(pilha, topo); void situacaoPilha(int p[ ], int &t) { if(t == 1) cout<<’\nATENCAO. Pilha Vazia\n”; else if(t == TAM) cout<<”\nATENCAO. Pilha Cheia\n”; else { cout<<”\nTotal de elementos na pilha: “<<t + 1<<\n”; cout<<”\n\nEspaco disponivel na pilha: “<<TAM (t+1)<<”\n”; } } Depois de termos entendido o conceito de empilhar e desempilhar, acredito que seja mais fácil perceber a presença do uso desta estrutura de dados em várias aplicações. Histórico de páginas visitadas num navegador. Sequencia de desfazer em vários softwares, o famoso atalho Ctrl Z. Implementação de recursividade (a torre de Hanói que vimos na disciplina de Algoritmos). A cadeia de chamadas de funções num programa. Avaliação de expressões aritméticas. Conversão de Decimal para Binário, etc. Na aritmética, estamos acostumados com a notação infixa onde o operador é colocado entre dois operandos. Esta notação é problemática, pois requer conhecimento da hierarquia das operações, obrigando o uso dos parênteses para sobrepor esta hierarquia quando necessário. Existem duas outras notações, a prefixa e a posfixa. Na notação prefixa(notação polonesa), o operador antecede os operandos enquanto que na notação posfixa (notação polonesa reversa), os operandos antecedem o operador. A grande vantagem dessas notações é que elas não precisam de parênteses. Procure, a partir da esquerda para direita, colocar os operandos na ordem em que aparecem na expressão. Quanto aos operadores, eles devem estar depois dos seus operandos. Observe a expressão deste exemplo: tem duas operações onde a primeira tem hierarquia maior do que a segunda logo, a operação 4 * 3 é executada primeiro: Depois, o resultado será o primeiro operando da segunda operação. Observe a tabela abaixo. >>> Muita atenção. Algumas vezes, você poderá ter dois operadores juntos, pois tudo dependerá do número de operandos e da hierarquia das operações. Fique ligado. Neste exemplo, temos três operações. Duas delas têm a mesma hierarquia. Acompanhe no quadro como você deverá colocar esta expressão na notação posfixa. APLICAÇÕES VIDE ARQUIVO PDF DA 6ª AULA! ESTRUTURA DE DADOS PILHA POR CONTIGUIDADE OPERAÇÕES MAIS COMUNS • Criar: cria uma pilha vazia. • Empilhar: insere um novo elemento no topo da pilha. • Desempilhar: remove um elemento do topo da pilha. • Exibir topo ou a quantidade: • Esvaziar: esvazia todos os elementos da pilha. PILHA CRIAR PILHA EMPILHAR (PUSH) EXIBIR O TOPO DA PILHA (STACKTOP) PILHA EXIBIR TODA A PILHA (POP) PILHA DESEMPILHAR EXEMPLO CRIAR UMA ESTRUTURA DE DADOS PARA A FICHA A SEGUIR: Aula 7: A Estrutura de Dados – Fila “Uma fila é um tipo especial de Lista Linear em que as inserções são realizadas num extremo, ficando as remoções restritas ao outro. O extremo onde os elementos são inseridos é denominado final da fila, e aquele de onde são removidos é denominado começo da fila.” (PEREIRA, Silvio. L., 2004, p. 57) A Fila, assim como a Pilha, também é um caso particular de Lista Linear e se diferencia dela pela forma de acesso porque a inserção sempre acontece em uma extremidade e a remoção, em outra. O conceito da Fila na programação tem a mesma filosofia que a fila do mundo real onde o primeiro que chega é o primeiro a ser atendido. Este método é conhecido como First In, First Out(FIFO). A operação enqueue só poderá ser realizada se a fila não estiver cheia assim como a operação dequeue, se não estiver vazia. Percebese então que, pelo menos, mais duas operações serão necessárias. >>> Overflow na fila – Quando se deseja inserir um elemento, mas a fila está cheia. Trecho de proteção evita este problema. Underflow na fila Quando se deseja remover um elemento, mas a fila está vazia. Trecho de proteção evita este problema. As filas possuem uma operação semelhante a de uma pilha. A seguir, apresentaremos os nomes pelos quais elas são reconhecidas, porém,você pode usar outra nomenclatura para as funções que criar em seus programas: IMPLEMENTAÇÃO A implementação de filas pode ser feita por arrays (matrizes) ou ponteiros, mas, nesta aula, porque ainda não tivemos a oportunidade de estudarmos ponteiros, implementaremos com matrizes e structs. A vantagem de começarmos usando matriz é sua fácil implementação, mas temos que ter consciência das desvantagens, uma vez que, na alocação estática, precisaremos definir um espaço suficientemente grande para armazenar um grande volume de dados. Além disso, indicadores de inicio e fim da fila. Os indicadores serão variáveis que sinalizarão o estado da Fila em relação à possibilidade, ou não, da inserção de novos elementos. O indicador de fim de Fila será incrementado toda vez que um elemento é inserido na Fila, enquanto que o Indicador de início de Fila será incrementado quando um elemento for removido, sendo este um grande problema porque, após sucessivas remoções, teremos uma Fila com vários nodos disponíveis no início, mas que não poderão ser ocupados. Este problema será resolvido na aula de hoje quando conseguirmos visualizar o array de forma circular, mas vamos dar um passo de cada vez porque precisamos primeiro entender as operações básicas da Fila. Nós vamos construir trechos para uma Fila que poderá armazenar até 5 números inteiros. Estarei apresentando, quando existir, o trecho que chama a função e a função. Antes de apresentar as operações básicas, gostaria de mostrar a variável estrutura que foi criada para trabalhar com a Fila. Como no estudo da estrutura Pilha usei uma matriz e uma variável simples, achei que seria mais produtivo implementar com variável estrutura que apresento abaixo. INICIALIZAR A inicialização da Fila irá se resumir a comandos de atribuição, preparandoa para receber os elementos que serão inseridos. Na Fila, os dois extremos são importantes e, por esta razão, estaremos trabalhando com duas variáveis. Sendo assim, para inicializar uma Fila, precisaremos atribuir valores a essas duas variáveis. Não se preocupe porque não inicializarei da mesma forma nos exemplos para que você não ache que só existe uma maneira. Assim como na Pilha, inicializar não significa zerar todos os elementos da Fila. Neste exemplo,fila.fim foi inicializada com –1 porque é comum você encontrar assim com a justificativa que em um primeiro momento fila.inicio seria igual a fila.fim, mas se você fizer os teste de fila vazia e fila cheia de forma correta, não tem problema começar por 0 as duas varáveis. //inicializa a fila fila.inicio = 0; fila.fim = 1; EFILEIRAR(ENQUEUE) Esta é uma operação básica da Fila. Para que possa ser enfileirado um elemento, é preciso testar se existe lugar na Fila. Verificar se a Fila está cheia poderia ser feito em separado, mas, assim como fiz na Pilha, resolvi fazer neste trecho. enfileira(fila); >>> A função recebe, por referência, o endereço da variável estrutura e como todas as variáveis necessárias, inclusive o vetor, são membros da estruturas, tudo e passado de uma só vez, aumentando a legibilidade. Começa testando se a Fila está cheia e, se não estiver, o valor é solicitado, fil.fim é incrementado e o valor armazenado na Fila. É uma função de fácil implementação. Acompanhe agora, na figura abaixo, supondo que você digitou cinco números para serem enfileirados. DESENFILEIRAR(DEQUEUE) Esta é outra operação básica da Fila. Para que possa ser desenfileirado um elemento, é preciso testar se a Fila não está vazia. desenfileirar(fila); A função recebe, por referência, o endereço da variável estrutura cujo um dos membros contem a posição do elemento a ser “desenfileirado”. >>> Observe que depois de exibir o conteúdo do elemento que era o primeiro da Fila, a variável fil.inicio é incrementada. Em nenhum momento foi removido o valor que estava armazenado na posição. Só o início que se deslocou para a direita. Assim como a função enfileirar, a função de desenfileirar é de fácil implementação e bem parecida. Acompanhe a agora, na figura abaixo, supondo que você escolheu desempilhar cinco números. primeiro elemento (firstElem) Esta é uma função que possibilita que seja visualizado o primeiro elemento da Fila sem “removêlo”. Na verdade, você não precisaria criar esta função tal sua simplicidade. Poderia simplesmente colocar no case. Só criei para modularizar todos os casos. elemPrimeiro(fila); situacaoFila(fila); >>> Esta função testa se a fila está vazia, se não estiver, exibirá vários dados sobre ela a fim de que você possa acompanhar melhor a execução do programa. Preste bem atenção às funções apresentadas e você verá que elas são muito diferentes das que operam a pilha, afinal, ambas as estruturas são casos particulares da Lista Linear. Aplicações com Filas As Filas têm muitas aplicações e acho que todos concordam que o uso mais comum seja em Sistemas Operacionais. Alguns exemplos: Fila de arquivos para impressão Atendimento de processos requisitados a um sistema operacional. Buffer para gravação de dados em mídia. O tratamento do armazenamento das teclas que estão sendo digitadas antes da tecla enter ser pressionada. UMA APLICAÇÃO O PROGRAMA DA AGENDA O programa que será apresentado a seguir é uma adaptação do código fonte do livro C Completo e Total de Herbert Schildt, cujo site está indicado no Aprenda Mais desta aula para que você faça o download dos códigos. É um livro clássico e, nesta aula, selecionei dois programas dele. Evidentemente, algumas modificações foram necessárias porque os códigos estavam escritos na linguagem C e usavam matriz de ponteiros assunto que será estudado na próxima aula. Para este exemplo, mais uma vez, considerei uma matriz de 5 elementos para que você possa entender o funcionamento da aplicação. Este programa da Agenda de Compromissos é uma boa aplicação para Fila, principalmente, para quem está começando. Ele será adaptado, nesta aula para Fila circular onde será resolvido o problema dos espaços ociosos à medida que os elementos vão sendo “removidos”. FILA CIRCULAR Para resolvermos o problema do espaço ocioso, o ideal seria que a última posição antecedesse à primeira e, se pegássemos um pedaço de barbante e uníssemos suas pontas sobre uma mesa, perceberíamos que ela se fecharia em uma curva. Enfileirar (enqueue) entra(fila); >>> A função começa testando a variável fil.total para verificar se seu valor é igual ao máximo. Caso não seja, o valor é solicitado e inserido na Fila. Depois, a variável fil.final é incrementada e testada. Se atingir o valor final, será reinicializada. Lembrese de que ela começou com zero. A variável fil.total é incrementada e, na próxima vez que se tentar inserir um elemento, a mensagem Atencao. Fila Cheia, irá aparecer até que um elemento seja removido. Desenfileirar (dequeue) deleta(fila) >>> Esta função também começa testando a variável fil.total para verificar se a Fila está vazia. A variável fil.inicio é incrementada porque um elemento foi removido e fil.total, decrementada. Já falei nesta aula, que o trecho de remoção, não estava removendo valor da Fila, mas neste exemplo, resolvi, toda vez que fosse removido um elemento, atribuir o valor –999 para que pudesse, no próximo trecho, listar os elementos da Lista Circular para aqueles que dizem:”Só acredito vendo! Não é para fazer disso um hábito, mas nunca se sabe se um dia você pode precisar usar algo parecido, não é? Meu objetivo foi didático. Função Lista Somente a primeira linha de código do else seria necessária para esta função. Todo o conjunto de comando foi acrescido para fins didáticos, visto que fica muito difícil visualizar, num primeiro momento, o comportamento da Fila Circular. Foi por esta razão que no trecho de remoção que atribui –999 à posição quando era “removido” um elemento. lista(fila) Aula 8: Alocação Dinâmica / Listas Encadeadas Introdução Alocação Dinâmica Durante todo esse tempo que estamos estudando Algoritmos e Estruturas de Dados, temos determinado quanto de memória nossos programas vão usar através das declarações que fazemos das variáveis locais, globais, matrizes, sejam elas de tipos primitivos ou de structs. Muitas vezes, sentimos necessidade de determinar durante a execução do programa a quantidade de elementos de uma matriz unidimensional, por exemplo, mas, ou nós superdimensionávamos alocando mais do que iria ser usado para que o usuário pudesse entrar com “qualquer valor(?)”, ou não conseguíamos fazer o que queríamos. A alocação dinâmica de memória vem suprir essa deficiência e possibilitar a criação de tipos de dados e estruturas de qualquer tamanho durante a execução do programa. Sabemos que durante a execução de um programa, o Sistema Operacional, aloca espaço na Memória Principal para as variáveis locais e globais, outro espaço é alocado para armazenar o código do programa, outro espaço para armazenamento das funções e o que sobra, é chamado de Área de memória Livre(free store) ou Heap que é usada para alocação dinâmica de memória.É bom frisar que a área da Pilha cresce em direção ao Heap e o Heap, em direção à área da Pilha. “ Embora a disposição física exata de cada uma das quatro regiões possa diferir entre tipos de CPU e implementações...” Operadores new e delete Na linguagem C++, alocar dinamicamente é relativamente simples porque temos somente dois operadores para alocar e desalocar memória. Entretanto, é imprescindível o conhecimento sobre ponteiros. Operador new Sua função é fazer alocação dinâmica de memória, retornando o endereço inicial do bloco alocado. Você poderá alocar espaço para armazenar um dado ou um conjunto de dados(array). Se você foi alocar para armazenar um dado, deverá fazêlo com new, nas se for para armazenar um array, use new[ ] Operador delete Sua função é retornarpara a área de memória livre o espaço que foi alocado dinamicamente. Se você for retornar uma área que estava armazenando um valor, deverá delete, mas se for um array, use delete[ ] Só usarmos esse operador não no dará garantia de que tudo ocorrerá sem problema com afirma DROZDEK, A(2002, p.10) Se depois de emitirmos a declaração delete não apagarmos o endereço da variável ponteiro que participa da supressão do bloco de memória, o resultado é potencialmente perigoso e podemos fazer o programa entrar em colapso quando tentarmos acessar locais inexistentes, particularmente para objetos mais complexos do que valores numéricos. Isso que se chama de problema de referência pendente(grifo nosso). “ Uma das formas de se fazer isso, é atribuir 0(zero) à variável ponteiro para que a variável ponteiro se torne nula. Alocar e desalocar memória poderão trazer alguns problemas para a programação. Vamos entender melhor isso. Quando desalocamos, uma porção da memória fica livre e, à medida que continuamos fazendo mais deslocações, poderemos ficar com uma área de memória livre razoável mas que não poderá ser alocada porque está fragmentada e, não poderá ser feita uma alocação de um bloco de forma não contígua. A MP(Memória Principal) consiste de trilhões de posições para armazenamento do Sistema Operacional, dados, programas, etc. Seu tamanho depende de quanto sua placa mãe suporta porque se foi o tempo em que o dinheiro era o principal fator para se aumentar a memória, visto que hoje em dia ela está relativamente barata. Nós já vimos que, ao declararmos uma variável nas linguagens de alto nível e de nível intermediário, o "nome" dado é associado a uma posição de memória (endereço). Este tipo de variável que nós conhecemos, armazena dado. Hoje vamos conhecer a variável ponteiro. Esta é uma variável que armazena o endereço de outra variável, possibilitando receber o endereço que é retornado quando se aloca a memória dinamicamente. Declaração de uma variável ponteiro: tipo *nomeDaVariávelPonteiro; tipo: O “ * ” é o caractere ou operador que sinaliza que a variável está sendo declarada como ponteiro. O operador “ & “ será usado antes de uma variável quando quisermoso endereço da variável e não, seu conteúdo. inicialização Ponteiros para Estruturas Nada nos impede de passarmos uma estrutura para uma função, entretanto isso sobrecarrega a pilha, aumentando o tempo de transferência dos dados entre as funções. Para minimizar esse problema e tendo em vista que já estudamos funções, vamos aprender agora como passar somente um ponteiro como argumento para função, e, através dele, o endereço da estrutura. Saibam também que será de muita utilidade nas próximas aulas. Existem duas formas de se acessar o membro através do ponteiro. Usando o operador “>” ou o “ * “. … nomeDaVariávelPonteiro > membro … … (*nomeDaVariávelPonteiro).membro ... Para se atribuir o endereço de uma estrutura a um ponteiro, usamos um comando de atribuição: nomeDaVariávelPonteiro = &nomeDaEstrutura; I. ptr é um ponteiro que aponta para uma matriz de struct. II. ptr + x (considere x como sendo uma variável do for). Quando um ponteiro se desloca de um, na verdade, ele não se desloca para o endereço vizinho, ele avança tantos endereços quantos forem os bytes da variável/estrutura apontada por ele que neste caso, são 8. Se você incluísse, depois da linha 16, duas linhas que exibissem esses endereços, obteria a saída abaixo. 16 dados *ptr = new dados[nfunc]; count<<";\nEndereco da primeira struct da matriz:";<<&ptr[0]; count<<";\nEndereco da segunda struct da matriz:";<<&ptr[1]; Observe que de 0x 3e3838 ate ox3e3840, endereços em hexadecimal, temos os seguintes endereços: 3e3838, 3e3839, 3e383A, 3e383B, 3e383C, 3e383D, 3e383E e 3e383F, totalizando 8 endereços(4 endereços para int e 4 endereços para float). III. (ptr+x) > matric é equivalente à ptr[x].matric porque você pode tratar o ponteiro como se fosse uma matriz e amatriz, com se fosse um ponteiro. ponteiro > alocação dinâmica > Listas Encadeadas. Listas de encadeamento simples, normalmente chamadas simplesmente de Listas Encadeadas, são compostas de elementos individuais, cada um ligado por um único ponteiro. Cada elemento consiste de duas partes: um membro de dados e um ponteiro. (LOUDON, K, 2000, p.56) As Listas Encadeadas são estruturas dinâmicas e, por essa razão, não existe restrição, a não ser de memória, de aumentar, ou diminuir, de tamanho durante a execução do programa. As lista encadeadas podem ser: Cada nó tem duas partes. Uma, armazena a informação e a outra, o endereço do próximo Nó. Se olharmos para este nó, podemos entendêlo como uma estrutura, visto que ele é formado por dois tipos diferentes, sendo um, necessariamente um ponteiro. Se formos audaciosos, diríamos que é uma variável struct com dois membros e, para chegarmos mais próximos do nó da Lista, poderíamos nomear esta estrutura de nó. O tipo do membro que armazena a informação não será difícil de identificarmos porque dependerá da aplicação, mas qual será o tipo da variável ponteiro? No nosso breve estudo sobre ponteiros, aprendemos que o tipo do ponteiro tem que ser do mesmo tipo que o elemento que ele vai apontar. Acredito que a única dificuldade ficará por conta de permitir que esta definição possa acontecer dentro dela mesma. Como DROZDEK, A(2002,p.68) diz: “Essa circularidade, no entanto, é permitida em C++”. Operações que podem ser realizadas com as Listas Simplesmente Encadeadas Inicializar O processo de inicialização de uma Lista consiste em criar uma Lista vazia, isto é uma lista sem elementos. A Lista sempre será representada pelo ponteiro para o primeiro elemento e, se é uma lista vazia, seu valor será NULL(constante escrita com letras maiúsuculas). Você pode inicializar a Lista na declaração. Exemplo: no *Lista = NULL; Inserir um nó no início, no fim ou em qualquer posição Após a criação da Lista, os nós podem ser inseridos, um a um, ou aos grupos, dependendo como você implementou seu código. Como não existe restrição para posição de inserção na Lista, você vai escolher, mas você concluirá que a forma mais simples é a de inserir no início da Lista. Remover nó A remoção de um nó poderá ser mais simples, ou mais complexa. Tudo vai depender da sua posição na Lista. Insere nó na frente, em qualquer lugar ou insere nó atrás? Remove nó que está na frente, em qualquer lugar ou nó que está atrás? Quem lê pensa que estamos falando ora de Fila, ora de Pilha ou ora, de Lista já que não tem restrição todas. Claro que para implementar algumas formas são mais simples do que outras. Entretanto vamos tentar ensinar apresentar as três formas. Observe as figuras abaixo onde nós são inseridos e removidos e, depois de analisar, pense qual estrutura poderia ser essa, tendo onde os dados são inseridos/removidos e quais seriam as etapas da inserção e da remoção de um nó? Resposta Como Insere na frente e Remove na frente, é a mesma filosofia da PILHA. Etapas para inserir um nó no início da Lista 1 Foi criado um novo nó; 2 O novo nó foi inicializado com valor 15; 3 Como o novo nó foi inserido na frente, o membro que contém o apontador deste novo nó se torna um ponteiro para o primeiro nó da lista atual; 4 O novo nó se torna o primeiro da lista e, sendo assim, o ponteiro que sinaliza o primeiro nó, é desviado para o nó incluído. Etapas para remover um nó no início da Lista 1)O ponteiro que sinaliza o primeiro nó da Lista é reajustado para que o segundo nó passe a ser o primeiro. 2) Algo que você não poderiasaber: Para que seja possível a etapa 1, a informação do nó removido é armazenada para que depois que finalizar a etapa 1, possa ser deletada(delete). Percorrer a lista Percorrer a lista significa “passar”por todos os nós. Seja para exibir todos os nós, contar os nós, buscar um elemento para remoção ou para alterar seu valor. Além destas, ainda temos as seguintes operações: EXIBIR A LISTA; DETERMINAR O NÚMERO DE NÓS; LOCALIZAR UM NÓ; ALTERAR O CONTEÚDO DE UM NÓ, ENTRE OUTRAS. Para que possamos fazer o passo a passo, vamos supor que nosso nó tenha como membro um número inteiro fora o ponteiro. Assim, vamos construir uma lista com um nó. Definindo a Struct Criando nó nodo* no1 = new nodo; atribuindo valores aos membros no1>num = 23; no1>prox = NULL; Exibindo cout<< ”\nValor do no1:” << no1>num; Liberando delete no1; no1=0; //pelo que falamos Agora que conseguimos, vamos construir uma lista com dois nós. Usaremos a mesma struct para facilitar. Observe que os dois nós serão dependentes da mesma variável ponteiro. Insere nó ao final Aula 9: Listas Encadeadas – Finalizando / Pilhas e Filas Dinâmicas LISTANDO A LISTA REMOVENDO ELEMENTO DA LISTA INSERINDO ELEMENTO DA LISTA Aula 10: Listas Duplamente Encadeadas Vamos analisar como seria a remoção de um nó que se encontra no meio através da figura abaixo e de dois comandos que considero de maior dificuldade na função remover. Todas as operações realizadas com as Listas Simplesmente Encadeadas poderão ser realizadas com as Duplamente Encadeadas e algumas, com mais facilidade. Como na aula 8, já falamos sobre alocação dinâmica de Memória, sobre Lista Encadeada, explicamos da necessidade do ponteiro, constato que não temos muito que acrescentar de teoria nesta aula. Sendo assim, vou apresentar nosso último programa, evidentemente com menu como gosto e com algumas funções que considero básicas e que já fizemos com Listas simplesmente Encadeadas. Gostaria de deixar para você um presente. Então pensei: Professor deixa que tipo de presente? Um exercido que dê muito trabalho, é claro, pois o resultado é gratificante. Deixo então, uma função que estava originalmente neste programa. Gostaria que você analisasse antes de inserir no programa e descobrisse o que ela faz. Depois, pesquisasse na Internet e pensasse em algumas alterações que poderia fazer nesta função para que ela pudesse ter uma função mais ampla.
Compartilhar