Baixe o app para aproveitar ainda mais
Prévia do material em texto
FACULDADE ANHANGUERA – UNIDADE CAMPINAS I TECNOLOGIA EM ANÁLISE E DESENVOLVIMENTO DE SISTEMAS ESTRUTURA DE DADOS – ETAPA 01 ALUNO RA QUEMUEL SANTOS DE AQUINO 8309737322 MARCIO BENAGES 7505571270 ATIVIDADE PRÁTICA SUPERVISIONADA (ATPS) TUTOR: PROF. RICARDO AUGUSTO DA SILVA CAMPINAS, 06 DE NOVEMBRO DE 2014. SUMÁRIO Introdução .............................................................................................................3 Introdução às Estruturas de Dados ........................................................................3 Produzindo Relatório e Implementação de uma Estrutura ...................................14 Implementando uma Estrutura ............................................................................16 Relatório 1 – Estrutura de Dados .........................................................................21 2 INTRODUÇÃO Esta atividade é importante para que possamos conhecer os fundamentos de Estruturas de Dados e à Alocação Estática de Memória. O conteúdo deste trabalho baseia-se em textos introdutórios de programação e textos mais avançados de estruturas de dados. Nosso principal objetivo é o de apresentar os conceitos básicos de estruturas de dados de forma simples e intuitiva. Para isso, estaremos discutindo as funcionalidades das estruturas de dados com base na sua implementação em programas de exemplo. Dessa forma, teremos uma visão prática das estruturas e conseguiremos adaptá-las para aplicações específicas. Para a apresentação das estruturas de dados, optamos por usar a linguagem de programação C, pois é a linguagem básica de programação de maior uso atualmente. Um ponto adicional a favor da escolha de C é que é o ponto inicial de aprendizagem de qualquer outra linguagem de programação, incluindo as linguagens orientadas a objetos tais como C++ e Java. INTRODUÇÃO ÀS ESTRUTURAS DE DADOS Hoje em dia, a grande maioria das pessoas utilizam a agenda do celular para armazenar seus contatos. As tarefas de uma agenda de contatos são basicamente duas: 1 - Definir como as informações dos contatos serão armazenadas. Uma informação armazenada em algum lugar (pedaço de papel, livro, computador, etc) é um dado. 2 - Disponibilizar operações para criar, recuperar, ordenar, atualizar e remover contatos. Além de operações para informar o estado da agenda (a quantidade de contatos existentes na agenda ou a quantidade de espaço disponível para novos contatos). 3 A primeira tarefa é crucial para o desempenho. Se a agenda armazena as informações de forma desorganizada, então será muito mais complicado manipular os dados de forma eficiente. A escolha de como guardar as informações deve levar em consideração as operações que serão disponibilizadas pela agenda. Por exemplo: seria interessante manter os contatos em ordem alfabética para facilitar a busca. Apesar da importância de como os contatos são armazenados, a organização interna da agenda não precisa e não deve ser exposta ao usuário. Afinal de contas, o que o usuário deseja é usar a agenda através das operações e que tudo seja feito o mais rápido possível. A única coisa que precisa ser mostrada para o usuário são as operações que ele pode fazer na agenda (inserir, recuperar, atualizar, remover contato, saber quantos contatos estão na agenda, etc). Este conjunto de operações é a interface que o usuário tem com a agenda. Cada celular pode implementar a sua agenda de contatos de uma forma totalmente diferente um do outro, na tentativa de obter mais performance, ser mais confiável ou gastar menos memória. Porém o conjunto básico de operações oferecidas pelas agendas é sempre o mesmo. Isso facilita a vida do usuário pois se ele tiver que trocar de celular não vai ter que aprender novamente como usar uma agenda de contatos. Essa é a grande vantagem em se pensar em interface. Mantida a interface, podemos trocar uma agenda que não é tão eficiente ou que já não atende mais as nossas necessidades por outra mais eficiente ou adequada, sem problemas em ter de aprender a usar a nova agenda: troca-se a implementação, mas não mudamos a interface. Uma agenda de celular pode ser vista como uma estrutura de dados. Uma estrutura de dados mantém os dados organizados seguindo alguma lógica e disponibiliza operações para o usuário manipular os dados. 4 É importante, quando programar, não misturar dado e estrutura de dados em uma coisa só. Um dado é uma informação armazenada e estrutura de dados é quem administra os dados. O ideal é que a estrutura de dados seja o mais independente possível dos dados que ela vai armazenar. Desta forma pode-se aproveitar a mesma estrutura de dados para diversos tipos de dados. Por exemplo, é melhor ter uma agenda genérica do que uma agenda de telefones. Uma agenda genérica pode ser utilizada para guardar telefones, ou emails, ou até mesmo guardar uma outra estrutura dentro dela: contatos, que seriam compostos por nome, telefone e email. Algumas estruturas de dados são apenas agrupamentos de dados sem um objetivo de aplicar algum algoritmo ou tirar proveito de sua estrutura. Um exemplo seria a estrutura Contato. Algumas outras estruturas são mais espertas e trabalhosas, como a Agenda, assim como Listas Ligadas, Vetores, Tabelas de Espalhamento e outras que veremos no decorrer do texto. Estas estruturas, por sua característica mais complexa e de poder ser reutilizada em outros contextos, devem ser criadas da forma mais independente possível dos dados que estarão dentro dela. Em outras palavras, não devemos misturar a Agenda e o Contato de maneira rígida, para que com isso possamos criar outras Agendas, como por exemplo uma Agenda de Fornecedor. - Vetores e Matrizes A forma mais simples de estruturarmos um conjunto de dados é por meio de vetores. Como a maioria das linguagens de programação, C permite a definição de vetores. Definimos um vetor em C da seguinte forma: int v[10]; A declaração acima diz que v é um vetor de inteiros dimensionado com 10 elementos, isto é, reservamos um espaço de memória contínuo para armazenar 10 valores inteiros. Assim, se cada int ocupa 4 bytes, a declaração acima reserva um espaço de memória de 40 bytes, como ilustra a figura abaixo. 5 O acesso a cada elemento do vetor é feito através de uma indexação da variável v. Observamos que, em C, a indexação de um vetor varia de zero a n-1, onde n representa a dimensão do vetor. Se formos fazer a contagem até 10, ela funcionará assim: v[0] → acessa o primeiro elemento de v v[1] → acessa o segundo elemento de v ... v[9] → acessa o último elemento de v Mas: v[10] → está ERRADO (invasão de memória) Se formos exemplificar isso por um programa, podemos tratar assim: /* Cálculo da media e da variância de 10 números reais */ #include <stdio.h> int main ( void ) { float v[10]; /* declara vetor com 10 elementos */ 6 float med, var; /* variáveis para armazenar a média e a variância */ int i; /* variável usada como índice do vetor */ /* leitura dos valores */ for (i = 0; i < 10; i++) /* faz índice variar de 0 a 9 */ scanf("%f", &v[i]); /* lê cada elemento do vetor */ /* cálculo da média */ med = 0.0; /* inicializa média com zero */ for (i = 0; i < 10; i++) med = med + v[i]; /* acumula soma dos elementos */ med = med / 10; /* calcula a média */ /* cálculo da variância */ var = 0.0; /* inicializa variância com zero */ for ( i = 0; i < 10; i++ ) var = var+(v[i]-med)*(v[i]-med); /* acumula quadrado da diferença */ var = var / 10; /* calcula a variância */ printf ( "Media = %f Variancia = %f \n", med, var ); return 0; } Devemos observar que passamos para a função scanf o endereço decada elemento do vetor (&v[i]), pois desejamos que os valores capturados sejam armazenados nos elementos do vetor. Se v[i] representa o (i+1)-ésimo elemento do vetor, &v[i] representa o endereço de memória onde esse elemento está armazenado. Na verdade, existe uma associação forte entre vetores e ponteiros, pois se existe a declaração: int v[10]; a variável v, que representa o vetor, é uma constante que armazena o endereço inicial do vetor, isto é, v, sem indexação, aponta para o primeiro elemento do vetor. 7 A linguagem C também suporta aritmética de ponteiros. Podemos somar e subtrair ponteiros, desde que o valor do ponteiro resultante aponte para dentro da área reservada para o vetor. Se p representa um ponteiro para um inteiro, p+1 representa um ponteiro para o próximo inteiro armazenado na memória, isto é, o valor de p é incrementado de 4 (mais uma vez assumindo que um inteiro tem 4 bytes). Com isto, num vetor temos as seguintes equivalências: v+0 → aponta para o primeiro elemento do vetor v+1 → aponta para o segundo elemento do vetor v+2 → aponta para o terceiro elemento do vetor ... v+9 → aponta para o último elemento do vetor Portanto, escrever &v[i] é equivalente a escrever (v+i). De maneira análoga, escrever v[i] é equivalente a escrever *(v+i) (é lógico que a forma indexada é mais clara e adequada). Devemos notar que o uso da aritmética de ponteiros aqui é perfeitamente válido, pois os elementos dos vetores são armazenados de forma contínua na memória. Os vetores também podem ser inicializados na declaração: int v[5] = { 5, 10, 15, 20, 25 }; ou simplesmente: int v[] = { 5, 10, 15, 20, 25 }; Neste último caso, a linguagem dimensiona o vetor pelo número de elementos inicializados. - Passagem de vetores para funções Passar um vetor para uma função consiste em passar o endereço da primeira posição do vetor. Se passarmos um valor de endereço, a função chamada deve ter um parâmetro do tipo ponteiro para armazenar este valor. Assim, se passarmos para uma função um vetor de int, devemos ter um parâmetro do tipo int*, capaz de armazenar endereços de inteiros. 8 Salientamos que a expressão “passar um vetor para uma função” deve ser interpretada como “passar o endereço inicial do vetor”. Os elementos do vetor não são copiados para a função, o argumento copiado é apenas o endereço do primeiro elemento. Para exemplificar, vamos modificar o código do exemplo acima, usando funções separadas para o cálculo da média e da variância. (Aqui, usamos ainda os operadores de atribuição += para acumular as somas.) /* Cálculo da media e da variância de 10 reais (segunda versão) */ #include <stdio.h> /* Função para cálculo da média */ float media (int n, float* v) { int i; float s = 0.0; for (i = 0; i < n; i++) s += v[i]; return s/n; } /* Função para cálculo da variância */ float variancia (int n, float* v, float m) { int i; float s = 0.0; for (i = 0; i < n; i++) s += (v[i] - m) * (v[i] - m); return s/n; } int main ( void ) { float v[10]; float med, var; int i; 9 /* leitura dos valores */ for ( i = 0; i < 10; i++ ) scanf("%f", &v[i]); med = media(10,v); var = variancia(10,v,med); printf ( "Media = %f Variancia = %f \n", med, var); return 0; } Observamos ainda que, como é passado para a função o endereço do primeiro elemento do vetor (e não os elementos propriamente ditos), podemos alterar os valores dos elementos do vetor dentro da função. O exemplo abaixo ilustra: /* Incrementa elementos de um vetor */ #include <stdio.h> void incr_vetor ( int n, int *v ) { int i; for (i = 0; i < n; i++) v[i]++; } int main ( void ) { int a[ ] = {1, 3, 5}; incr_vetor(3, a); printf("%d %d %d \n", a[0], a[1], a[2]); return 0; } A saída do programa é 2 4 6, pois os elementos do vetor serão incrementados dentro da função. 10 - Alocação dinâmica Até aqui, na declaração de um vetor, foi preciso dimensioná-lo. Isto nos obrigava a saber, de antemão, quanto espaço seria necessário, isto é, tínhamos que prever o número máximo de elementos no vetor durante a codificação. Este pré-dimensionamento do vetor é um fator limitante. Por exemplo, se desenvolvermos um programa para calcular a média e a variância das notas de uma prova, teremos que prever o número máximo de alunos. Uma solução é dimensionar o vetor com um número absurdamente alto para não termos limitações quando da utilização do programa. No entanto, isto levaria a um desperdício de memória que é inaceitável em diversas aplicações. Se, por outro lado, formos modestos no pré-dimensionamento do vetor, o uso do programa fica muito limitado, pois não conseguiríamos tratar turmas com o número de alunos maior que o previsto. Felizmente, a linguagem C oferece meios de requisitarmos espaços de memória em tempo de execução. Dizemos que podemos alocar memória dinamicamente. Com este recurso, nosso programa para o cálculo da média e variância discutido acima pode, em tempo de execução, consultar o número de alunos da turma e então fazer a alocação do vetor dinamicamente, sem desperdício de memória. - Uso da memória Informalmente, podemos dizer que existem três maneiras de reservarmos espaço de memória para o armazenamento de informações. A primeira delas é através do uso de variáveis globais (e estáticas). O espaço reservado para uma variável global existe enquanto o programa estiver sendo executado. A segunda maneira é através do uso de variáveis locais. Neste caso, como já discutimos, o espaço existe apenas enquanto a função que declarou a variável está sendo executada, sendo 11 liberado para outros usos quando a execução da função termina. Por este motivo, a função que chama não pode fazer referência ao espaço local da função chamada. As variáveis globais ou locais podem ser simples ou vetores. Para os vetores, precisamos informar o número máximo de elementos, caso contrário o compilador não saberá o tamanho do espaço a ser reservado. A terceira maneira de reservarmos memória é requisitando ao sistema, em tempo de execução, um espaço de um determinado tamanho. Este espaço alocado dinamicamente permanece reservado até que explicitamente seja liberado pelo programa. Por isso, podemos alocar dinamicamente um espaço de memória numa função e acessá-lo em outra. A partir do momento que liberamos o espaço, ele estará disponibilizado para outros usos e não podemos mais acessá-lo. Se o programa não liberar um espaço alocado, este será automaticamente liberado quando a execução do programa terminar. Apresentamos abaixo um esquema didático que ilustra de maneira fictícia a distribuição do uso da memória pelo sistema operacional. Quando requisitamos ao sistema operacional para executar um determinado programa, o código em linguagem de máquina do programa deve ser carregado na memória. O sistema operacional reserva também os espaços necessários para armazenarmos as variáveis globais (e estáticas) existentes no programa. O restante da memória livre é utilizado pelas variáveis locais e pelas variáveis alocadas dinamicamente. 12 Cada vez que uma determinada função é chamada, o sistema reserva o espaço necessário para as variáveis locais da função. Este espaço pertence à pilha de execução e, quando a função termina, é desempilhado. A parte da memória não ocupada pela pilha de execução pode ser requisitada dinamicamente. Se a pilha tentar crescer mais do que o espaço disponível existente, dizemos que ela “estourou” e o programa é abortado comerro. Similarmente, se o espaço de memória livre for menor que o espaço requisitado dinamicamente, a alocação não é feita e o programa pode prever um tratamento de erro adequado (por exemplo, podemos imprimir a mensagem “Memória insuficiente” e interromper a execução do programa). - Ponteiros em C A linguagem C permite o armazenamento e a manipulação de valores de endereços de memória. Para cada tipo existente, há um tipo ponteiro que pode armazenar endereços de memória onde existem valores do tipo correspondente armazenados. Por exemplo, quando escrevemos: int a; declaramos uma variável com nome a que pode armazenar valores inteiros. Automaticamente, reserva-se um espaço na memória suficiente para armazenar valores inteiros (geralmente 4 bytes). Da mesma forma que declaramos variáveis para armazenar inteiros, podemos declarar variáveis que, em vez de servirem para armazenar valores inteiros, servem para armazenar valores de endereços de memória onde há variáveis inteiras armazenadas. C não reserva uma palavra especial para a declaração de ponteiros; usamos a mesma palavra do tipo com os nomes das variáveis precedidas pelo caractere * . Assim, podemos escrever: int *p; Neste caso, declaramos uma variável com nome p que pode armazenar endereços de memória onde existe um inteiro armazenado. 13 Para atribuir e acessar endereços de memória, a linguagem oferece dois operadores unários ainda não discutidos. O operador unário & (“endereço de”), aplicado a variáveis, resulta no endereço da posição da memória reservada para a variável. O operador unário * (“conteúdo de”), aplicado a variáveis do tipo ponteiro, acessa o conteúdo do endereço de memória armazenado pela variável ponteiro. PRODUZINDO RELATÓRIO E IMPLEMENTAÇÃO DE UMA ESTRUTURA Agora que já falamos de alocação de memória, ponteiros em C, estruturas de dados, vetores, strings e outros mais, vamos utilizar esses recursos para implementar uma estrutura. Para exemplificar, vamos ilustrar esquematicamente, através de um exemplo simples, o que ocorre na pilha de execução. O Programa abaixo checa a existência de um palíndromo (uma palavra ou frase que pode ser lida invertida tendo o mesmo sentido), exemplo: "O Galo nada no lago". -----Inicio do Programa ----- #include <stdio.h> #include <string.h> ///////// defines //////// #define MAX 100 // mudar tamanho da pilha typedef char TIPO_STACK; // mudar o tipo de pilha ////// variaveis globais ///// TIPO_STACK stack[MAX]; int top = -1; ///////// protótipos de funções /////// void pop( TIPO_STACK * ); void push( TIPO_STACK ); 14 ///////////FUNÇÃO MAIN /////////// int main(void) { char fraseOriginal[MAX], fraseInvertida[MAX]; int i, tamanho; printf("\n=== Checar existencia de palindromo ===\n\n Entre com a frase a ser checada \n(sem caracteres especiais e sem espacos) \n\nFrase: "); scanf("%s", fraseOriginal); //// coloca frase na pilha //// tamanho = strlen( fraseOriginal ); for( i = 0; i < tamanho; i++ ) push( fraseOriginal ); //// tira frase da pilha, agora invertida //// for( i = 0; i < tamanho; i++ ) pop( &fraseInvertida ); fraseInvertida[tamanho] = '{FONTE}'; // finaliza string invertida //// mostra frase invertida //// printf("Frase Invertida: %s", fraseInvertida); //// checa se as duas strings sao iguais //// if( !strcmp( fraseOriginal, fraseInvertida) ) printf("\nResultado: Confere, palindromo existente\n\n"); else printf("\nResultado: Nao confere\n\n"); return ( 0 ); } // fim main //////FUNÇÃO//////////// Nome: pop() ////// Descricao: remove elemento da pilha /// void pop( TIPO_STACK *elemento ) { if( top == -1 ) // pilha vazia printf("\npilha vazia\n"); 15 else { *elemento = stack[top]; top--; } } // fim funcao ////FUNÇÃO//////// Nome: push() ////// Descricao: insere elemento na pilha /// void push( TIPO_STACK elemento ) { if( top == MAX ) // pilha cheia printf("\npilha cheia\n"); else { top++; stack[top] = elemento; } } // fim funcao ----- Fim do Programa ----- IMPLEMENTANDO UMA ESTRUTURA Abaixo estaremos descrevendo e exemplificando o que é alocação estática de memória e criando uma estrutura avião (struct). Esse programa estará permitindo o cadastro e exibição dos campos modelo, fabricante, passageiros, comprimento, altura, velocidade e altitude. - Alocação estática de memória Na alocação estática o espaço de memória, que as variáveis irão utilizar durante a execução do programa, é definido no processo de compilação. Não sendo possível alterar o tamanho desse espaço durante a execução do programa. Exemplos: /*Espaço reservado para um valor do tipo char. O char ocupa 1 byte na memória.*/ char a; 16 /*Espaço reservado para dez valores do tipo int. O int ocupa 4 bytes na memória, portanto 4x10=40 bytes.*/ int vetor[10]; /*Espaço reservado para nove(3x3) valores do tipo double. O double ocupa 8 bytes na memória, portanto3x3x8=72 bytes.*/ double matriz[3][3]; Este tipo de alocação é utilizado quando se sabe de antemão a quantidade de memória que será utilizada pelo programa. - Sistema Avião ----- Início do Sistema ----- // main: #include <stdio.h> #include <stdlib.h> #include "fun.h" void cadastrar(); void reservar(); void consultar(); void consultarpass(); void menu(); main() { menu(); } //cabeçalho: void cadastrar(); void reservar(); void consultar(); void consultarpass(); 17 void menu(); int escolha(int tcl); typedef struct flyer celaviao; typedef struct pass celreserva; struct pass{ int cdaviao; char nome[20]; }; struct flyer{ int cdaviao; int quant; celreserva reserva[15]; }aviao[10]; //arquivo1: #include <locale.h> #include <stdio.h> #include "fun.h" void cadastrar() { setlocale (LC_ALL,""); int i=0, verif; if(i<10){ printf("\nCadastro de avião"); printf("\nInsira o código:\n"); scanf("%i",&verif); if(verif==aviao[i].cdaviao){ printf("\nAvião existente"); cadastrar(); } else aviao[i].cdaviao = verif; printf("\nInforme a quantidade de vagas\n"); scanf("%i",&aviao[i].quant); printf("\nAvião cadastrado!"); 18 i++; system("pause"); }else printf("\nNúmero máximo de aviões alcançado!"); } void reservar() { setlocale (LC_ALL,""); int cod, i=0, j=0; printf("\nInsira o código do avião:"); scanf("%i",&cod); if(j<=10) { if(cod==aviao[j].cdaviao) { aviao[j].reserva[i].cdaviao = aviao[j].cdaviao ; printf("\nInsira o nome do passageiro:"); scanf("%s",aviao[j].reserva[i].nome); printf("\n Reserva feita com sucesso"); i++; aviao[j].quant --; system("pause"); } else j++; } } void consultar(){ setlocale (LC_ALL,""); int cod, j=0, i=0, k=0, num; printf("\nInsira o código do avião:"); scanf("%i",&cod); while(i<=10){ if(cod==aviao[j].cdaviao){ num = aviao[j].quant; 19 printf("\n %d Vagas disponíveis",aviao[j].quant ); printf("\n Reservas do avião nº %d",cod); if(k <= num){ printf("\n %s", aviao[j].reserva[k].nome); k++;break; system("pause"); } } else j++; } } void consultarpass(){ setlocale (LC_ALL,""); char nome[20]; int k, j=0; printf("\nInsira o nome do passageiro:"); scanf("%s",nome); if(nome==reserva[j].nome){ printf("\n Reservas do passageiro %s",nome ); printf("\n %c", reserva[k].cdaviao); }else j++; } void menu(){ int tecla; do{ system("CLS"); printf("\nMENU"); printf("\n1-Cadastrar Avião"); printf("\n2-Reservar passagem"); printf("\n3-Consultar por avião"); printf("\n4-Consultar por passageiro"); printf("\n5-Sair\n"); scanf("%i",&tecla); 20 escolha(tecla); }while(tecla!=5); } int escolha(int tcl){ switch (tcl){ case 1 : cadastrar();break; case 2 : reservar();break; case 3 : consultar();break; case 4 : consutarpass();break; case 5 : return tcl; } } ----- Fim do Sistema ----- RELATÓRIO 1 – ESTRUTURA DE DADOS Na resolução de um problema por meio de um programa, a primeira providência é conceber um algoritmo adequado. A eficiência de um algoritmo qualquer está intimamente relacionada à disposição, na memória, dos dados que são tratados pelo programa. Por exemplo, se freqüentemente enfrentamos o problema de descobrir os números de telefones de nossos conhecidos, é conveniente dispor de uma relação de números, organizada em uma agenda. Se a organização for feita por ordem alfabética, a agenda de fato ajuda. Se, porém, organizássemos nossa agenda pela ordem de altura das pessoas, com raras exceções, a agenda se tornaria difícil de manusear. As estruturas de dados são formas de distribuir e relacionar os dados disponíveis, de modo a tornar mais eficientes os algoritmos que manipulam esses dados. O estudo de estrutura de dados é parte fundamental para o desenvolvimento de programas e algoritmos. Assim como um número pode ser representado em vários sistemas diferentes, 21 também um conjunto de dados relacionados entre si pode ser descrito através de várias estruturas de dados distintas. Quando o programador cria um algoritmo para solucionar um problema, ele também cria uma estrutura de dados que é manipulada pelo algoritmo. A escolha de uma determinada estrutura pode afetar substancialmente a quantidade de área de armazenamento requerida para o processamento bem como o tempo deste processamento. É, portanto, de grande importância o estudo de diferentes estruturas que possam ser utilizadas eventualmente na resolução de um problema, de forma a simplificar a sua implementação prática. Um programador de pouca experiência utilizará em sua programação formas bastante simplificadas para a representação dos dados envolvidos (como matrizes e vetores), que possivelmente seriam adequadamente representados através de estruturas mais sofisticadas (filas encadeadas, árvores, etc.), tornando o processamento mais eficiente em termos de área de armazenamento e tempo. 22 SUMÁRIO INTRODUÇÃO ÀS ESTRUTURAS DE DADOS
Compartilhar