Baixe o app para aproveitar ainda mais
Prévia do material em texto
TRABALHO PRATICO DE AEDS2 TP2: OTIMIZACAO SERVIDOR DE EMAILS Prof. Gisele Pappa e Rodrygo Santos Aluna: Julia Ferri Leite Storino Matricula: 2014088661 1. Introdução Ao longo da disciplina, foi trabalhado dois conceitos fundamentais: Alocação dinâmica de memória e Tipo Abstrato de dados. Alem desses, que foram base para o feitio do Trabalho, foi utilizada duas estruturas, Tabela Hash e Arvore Binaria, que deram um “upgrade” no funcionamento do antigo servidor de emails, implementado com listas encadeadas. O objetivo desse trabalho é melhorar um programa ja existente de um simulador de um servidor de emails com listas encadeadas. Este servidor da o suporte ao gerenciamento de contas, assim como entrega mensagens de usuários, consulta de contas, cadastramento de usuário, e remoção de emails. Para essa melhoria, foi criado uma Tabela Hash, no qual os registros armazenados na tabela são diretamente endereçados a partir de uma transformação aritmética sobre a chave de pesquisa. Entretanto, essa pode gerar colisões, enderecando ao mesmo local; para resolver esse problema cada entrada da tabela hash apontara para uma arvore binaria que, em cada no desta arvore estará um email armazenado, totalizando a arvore então, uma caixa de entrada. Assim, espera-se com isso reforçar os conceitos de Alocação dinâmica de memória, e Tipo Abstrato de dados, trabalhados no primeiro e segundo trabalho pratico. Além disso, espera-se aprender a trabalhar e manipular Tabelas Hashing, que são tipicamente usadas para indexação de grandes volumes de informação (como bases de dados), no caso, provenientes do servidor de emails. Assim como o manuseio e implementação das Arvores, estrutura muito utilizada na computação para realizar buscas, de extrema importância e uma boa eficiência. 2. Implementação Estrutura de dados Para facilitar a implementação e evitar a criação de varias variáveis, foi utilizado no código o conceito aprendido de Estrutura de Dados. De acordo com a proposta, foi criado, juntamente com o typedef, varias estruturas (structs) para armazenar os dados que estarão presentes dentro do servidor de emails. E importante ressaltar que o typedef foi utilizado para definir uma nova estrutura, que possa ser usada dentro de outra estrutura, sendo assim, uma definição de um novo tipo (type definition). A primeira estrutura criada foi a Struct Mensagem. Nessa Estrutura encontra- se uma variável do tipo inteiro “ID”, que será utilizado para identificar os usuários. Alem disso foi criado variável tipo caractere “mensagem” para armazenar o campo da mensagem, o conteúdo escrito do email. A segunda estrutura de dados criada foi a Struct MailBox, que possui uma variável Email do tipo Estrutura Mensagem que armazenara o email. Alem disso, Cada Estrutura do tipo Mailbox possui um apontador do tipo Mailbox para a sua esquerda (left) e para a sua direita (right). Alem disso, e criado a tabela Hashing, “GavetaUsuarios”, um vetor de arvores que vai de 0 a M-1. Sendo que o “M” e informado previamente. Alem disso, as funções que serão criadas também utilizam os parametros “struct Mailbox”, visto que essa estrutura será manipulada nas funções. Funções Alem das estruturas de dados, foi implementado funções que dividiram o problema em 3: a entrega de uma mensagem (1), a remoção de um email (2), e a consulta de uma caixa de email presente na lista de usuários (3). Entretanto, foi necessário duas funções auxiliares: a criação de uma caixa de emails (0), e a procura do menor entre os maiores filhos de um no da arvore (00). 0) struct MailBox* cria_mail(Mensagem *email): essa função recebe um email do tipo Mensagem, e cria uma Mailbox vazia. Primeiramente é alocado dinamicamente um tipo “arvoredosmails”, a partir do “malloc”, para inicializar a estrutra. Então, os ponteiros contidos em “arvoredosmails”, são transformados, tal que o esquerda (left) e o direita (right) apontam para null, e o que aponta para o email recebe o email. Assim, a arvore contem apenas o email do tipo mensagem recebido na função. 00) struct MailBox* acha_menor(MailBox *arvoresdosmails): essa função recebe “arvoredosmails”, a arvore como parâmetro o primeiro filho da direita da arvore e a partir desse filho ela procura o menor dele. Ou seja, a função retorna o menor entre os maiores filhos da raiz. Essa funcao será útil para as principais, visto que a ordem da arvore (a esquerda menores do que a raiz e a direita maiores do que a raiz) deve ser mantida em todas as funcoes que serão descritas abaixo: 1) int entregaMsg(Mensagem *email, struct MailBox *arvoresdosmails): essa função recebe um email do tipo Mensagem, e uma arvoredosmails, a caixa de entrada, do tipo MailBox na qual o email devera ser inserido. Essa função faz um caminhamento binário na arvore com chamadas recursivas, realizando recursos percorrendo a arvore encontrada a partir do valor do hashing. Visto que o valor de entrada é conhecido, este vai diretamente para o endereço da arvore do usuário na tabela hash. Em seguida, para realizar essa entrega no lugar correto, foi criado um if que compara o valor do no atual e o valor do elemento a ser inserido. Caso o no seja maior, este vai para esquerda; caso seja menor, este vai para a direita; sempre mantendo a ordem da arvore binaria. Quando o resultado dessa operação é “null”, isso significa que o local de inserção foi encontrado. A partir desse momento, é alocado então o elemento a ser inserido. Desse modo, o email então é entregue. Entretanto, se o local de inserção não é encontrado, é constatado um erro. Se a operação é efetuada com sucesso, a função retorna “1”; se não esta retorna 0; ou seja, o elemento desse email ja existe na arvore. 2) struct MailBox* limpa(int id, struct MailBox *arvoresdosmails): essa função recebe um ID da mensagem a ser removida. Primeiramente, é verificado se a arvore de mensagens do usuário esta nula. Se sim, é constatado um erro, pois não ha nada a ser apagado. Se não, a arvore é percorrida por meio de apontadores em comparação com item a ser apagado. De modo que, se o id é menor que o apontador da arvore para o item a ser deletado, é chamado recursivamente a função limpa para o apontador da esquerda (arvoredosmails->left); caso contrario, é chamada a função recursivamente a função limpa para o apontador da direita (arvoredosmails->right). Entretanto, é necessário verificar se o no a ser deletado possui filhos. Se os apontadores da esquerda e da direita são nulos, isso significa que não possui filhos, não sendo necessário realocar os apontadores e simplesmente apagando com o comando “free”. Entretanto, se apenas o apontador da esquerda (arvoredosmails- >left) é nulo, significa que existe apenas um filho para a direita. Assim, é criado um aux do tipo Estrutura Mailbox que recebe o filho existente, no caso, apontador da direita; e então, o apontador do email é deletado a partir do comando (free(arvoredosmails)) com sucesso. O mesmo processo é feito se apenas o apontador da direita é nulo (ou seja, existe apenas um filho para a esquerda), so que agora o auxiliar aux recebe o filho da esquerda. Em seguida, o o apontador do email é deletado a partir do comando (free(arvoredosmails)) com sucesso. Todavia, se o email a ser deletado possui 2 filhos, é criada uma nova variável “suc” do tipo MailBox que recebe o valor do menor filho a direita da arvore a partir da chamada da função acha_menor com os apontadores da direita, onde se localizam os maiores valores de uma arvore ordenada (arvoredosmails->right). Em seguida, essa variável suc passa a ocupar o lugar do email que foi apagado com sucesso. 3) struct MailBox* consulta(int id, struct MailBox *arvoresdosmails): a função recebe o id, e a arvore dos emails. Assim, a função procura o email a ser consultado com as operações básicas de arvores:se o id passado é igual ao ponteiro da arvore da id do email, a mensagem foi encontrada e retornada; se o id é menor que o ponteiro da arvore da id do email, a mensagem encontra-se a esquerda (é menor que onde o ponteiro aponta), e é chamado recursivamente o consulta para a esquerda (arvoredosmails->left). Caso contrario, se o id passado é maior que o ponteiro da arvore da id do email, a mensagem encontra-se a direita (é maior que onde o ponteiro aponta), e é chamado recursivamente o consulta para a direita (arvoredosmails->right). Entretanto, se mesmo com essas operações a mensagem não foi encontrada, isso significa que a mensagem não existe, retornando null. Programa Principal O programa main.c e responsável por receber os argumentos de entrada e saída (input/output),e inseri-los nas devidas funções acima. O arquivo de entrada (input) é aberto com o comando “fOpen”, e fechado com o comando “fClose”. Para isso, primeiramente o programa lê o arquivo de entrada a partir do comando “fOpen”, que a partir da opção “r” (read) lê o arquivo, que contem as operações a serem feitas no servidor de email. Foi criado no programa principal, uma variável comando do tipo “char” que definira a operação a ser realizada. Primeiramente o numero M é lido pelo comando fscanf, e depois, é alocado dinamicamente o “GavetaUsuarios” pelo comando malloc, que cria um vetor de arvores de acordo com o tamanho de M; ou seja, em cada posição de i ate m-1 desse existe a raiz de uma arvore. Então é feito um loop que cria para cada posição i desse vetor uma “caixinha”, uma parte reservada para o email. Isso também é feito dinamicamente com o comando malloc de acordo com o tamanho do tipo Mensagem. Em seguida, foi necessário ler os comandos contidos no arquivo de entrada. Para isso, foi criado um loop com o comando while no qual enquanto a função fgets, que pega uma linha inteira do arquivo de entrada e armazena na forma de uma string é diferente de zero. Depois de lido a string, é necessário codificar a string contida no arquivo de entrada. Ou seja, assimilar as palavras contidas no arquivo as operações a serem feitas, e assimilar o numero que esta escrito no arquivo de entrada como tipo “char” para um numero inteiro que será o ID. Para isso foi utilizado o comando “strcmp” utilizada para comparar strings, retornando “0” se as strings forem realmente iguais. No programa este comando compara a palavra escrita no arquivo de entrada com as operações disponíveis (apaga,entrega,consulta) e, dependendo do resultado (que será 0 se a comparação for realmente igual), chama a função relacionada a tal comando. Ja para a conversão do numero do tipo caractere para um inteiro ID, foi utilizado o comando “atoi”, que converte strings em números inteiros. Ademais, o endereço de cada “arvoredosmail” na tabela hash é feita pela operação de resto entre U, que é um numero também lido do arquivo de entrada pelo comando atoi, e o numero M, também lido da tabela anteriormente. Assim é cada arvore é endereçada na tabela hash. Alem disso, dentro de cada função, como foi explicado, foi estabelecido um valor de retorno. Desse modo, se a função retorna “null”, ocorreu um erro durante a execução da função, que e explicitado na função main, que a partir do comando “printf”, alerta ao usuário sobre o erro ocorrido, de acordo com a especificação do servidor. Entretanto, se a função retorna o valor “1”, essa função foi executada com sucesso, e esse processo também e comunicado ao usuário, de acordo com a especificação do servidor. Esse processo de comparação dos valores de retorno da função foram feitos a partir do comando “if”, designando cada valor para seu respectiva saída adequada, de acordo com a especificação do servidor. Essa saída adequada é impressa na tela. Desse modo, o programa retorna, e imprime na tela a conclusão ou não de cada operação solicitada pelo arquivo de entrada. Desse modo, para cada operação requerida pelo arquivo de entrada é gerada uma saída. Vale ressaltar que o programa trabalha apenas com o retorno de funções, não acessando a estrutura interna dos TADS, apenas as funções oferecidas em sua interface. Organização do Código, Decisões de Implementação e Detalhes técnicos Em geral, o arquivo Emails foi dividido em 3 partes: o main.c, programa principal que define a entrada e a saída e “chama as funções”, Email.c onde foi definido o que cada função faz, e o Email.h onde foi definido as estruturas (structs) a serem utilizadas. Isso foi feito visando evitar a criação de variáveis e funções de forma desorganizada. O programa foi compilado em Linux, e também pelo sistema pratico. Para executa-lo, bastou digitar “gcc Wall std=c99 *.c o tp1” no ambiente Linux, e “./ tp2 entrada ” no Pratico. 3. Analise de Complexidade A complexidade de cada função varia, visto o uso do Hashing e Arvores; entretanto, como o programa busca otimizar o anterior, todas buscam ser menores que o servidor que utiliza listas encadeadas. Foi considerado que a complexidade do hashing é O(1) pois para calcular uma posição qualquer em um vetor basta aplicar a operação aritmética explicitada na especificação. Desse modo, a analise de complexidade será feita em relação ao numero de comparações na arvore, de tal forma que: ⁃ Função CriaMail: essa função possui complexidade O(1), visto que sua função e de apenas criar o no da arvore inicial. ⁃ Função acha_menor: a função acha menor, que acha o menor dos filhos entre os maiores filhos da arvore, tem seus casos particulares que alteram sua complexidade. Seu pior caso é tido quando a arvore inteira é procurada, e o menor se encontra na ultima posição da arvore, sendo desse modo, O(n). Isso pode ocorrer nesta arvore pois não foram implementados mecanismos de balancear a arvore, sendo comparados todos seus elementos assim como em uma lista encadeada. Entretanto, se a arvore estivesse balanceada, seu pior caso seria O(log n). Ja seu o melhor caso é O(1), quando o elemento procurado é o primeiro a ser comparado. ⁃ Função limpa: a função limpa é necessário analisar todos os casos. A função então apaga um email, mas leva em consideração os filhos ou não que esse no possui ou nao. Desse modo, se o no a ser apagado não possui filhos, a complexidade é o pior caso da função, em que ela percorre todos os elementos de uma arvore não balanceada, sendo então O(n). Se o no possui 1 filho, no pior caso, da arvore não balanceada e na ultima posição, a arvore inteira é percorrida menos a posição que sucede, o seu filho; sendo então O(n-1). Entretanto, se o no possui 2 filhos, no pior caso, a arvore não balanceada é inteiramente percorrida menos suas 2 posições sucessoras, no caso, seus dois filhos, sendo assim O(n-2). Comparando com o primeiro trabalho, a complexidade se equiparam em uma arvore não balanceada, por ser O(n). Entretanto, se tivéssemos implementado mecanismos para balancear a arvore, o programa novo seria mais eficiente, visto que a complexidade de uma arvore balanceada é O(log n) se o no não possuir nenhum filho. ⁃ Função EntregaMsg: essa função também deve ser analisada em seus casos extremos. Em seu melhor caso, a entrega possui O(1), pois é encontrado diretamente o local de entrega. Entretanto no seu pior caso, deve se considerar que a arvore não esta balanceada e o local de entrega é o ultimo, sendo desse modo, necessário percorrer todas as posições da arvore, pois todos os itens encontram-se em um lado da arvore, resultando em uma complexidade O(n). Desse modo, visto que a arvore não teve mecanismos de balanceamento, a complexidade se equipara ao programa anterior implementado com listas encadeadas. ⁃ Função Consulta: a função consulta deve achar o email o qual a consulta deve ser feita. Em seu melhor caso, o elemento a ser consultado é o primeiro, sendo feito assim apenas 1comparação; sendo assim O(1). Ao analisarmos seu pior caso, todos os itens encontram-se em um lado da arvore (arvore não foi implementada com mecanismos de balanceamento), e o item a ser consultado encontra-se na ultima posição, resultando desse modo em O(n). Ao compararmos com o programa anterior, os programas se equiparam, resultando em complexidades iguais a O(n). ⁃ Programa Principal: o programa principal apenas chama cada uma das funções descritas acima e faz alguns comandos constantes, tais quais abrir o arquivo, comparar strings.. Desse modo, a sua complexidade é regida pela função de maior custo computacional; se a arvore esta balanceada a complexidade é O(1) + O (log n) + O(log n-1) + O(log n) + O(log n) = O(log n) sendo mais vantajoso que o programa anterior. Entretanto se a arvore não esta balanceada, a sua complexidade O(1) + O (n) + O(n-1) + O(n) + O(n) = O(n); equiparando ao programa anterior, 4. Conclusão O desenvolvimento do algoritmo ocorreu de forma tranquila, visto que foi trabalhado desde o dia do lançamento do trabalho, ate a data de entrega, evitando desesperos e surpresas. A principal dificuldade encontrada foi o desenvolvimento inicial do trabalho, visto que de primeira vista, o Trabalho parece requerer muitas funcionalidades, assustando um pouco. Entretanto, ao olhar mais cuidadosamente, foi possível encontrar soluções para o mesmo, buscando a forma mais simples possível. Apesar disso, o resultado foi satisfatorio, visto que o servidor de email foi criado, atendendo todos requerimentos, como por exemplo a criação de funções, uso de TADS, e manipulação da tabela hash e de arvores. Alem disso, foi constatado que o programa não foi totalmente otimizado, visto que as complexidades de certas funções permaneceram as mesmas em relacao ao programa implementado com listas encadeadas. Isso ocorreu pois não tivemos base o suficiente para implementar mecanismos que fossem capazes de balancear arvores, o que diminuiria a complexidade de O(n) para O(log n) em todos os piores casos. Referencias [1] Ziviani, N., Projeto de Algoritmos com Implementações em Pascal e C, 2ª Edição, Editora Thomson, 2004. [2] Material disponibilizado pelo Moodle da disciplina [3] Pesquisas diversas no Google
Compartilhar