280 likes | 366 Views
Engenharia de Computação. ECE05968 - Estruturas de Dados I - 3º período - 60 h - 3 créditos Ementa:
E N D
Engenharia de Computação. • ECE05968 - Estruturas de Dados I - 3º período - 60 h - 3 créditos • Ementa: • Fundamentos de Análise de Algoritmos; Recursividade; Alocação dinâmica de memória; Conceito de Tipos Abstratos de Dados; Listas, Pilhas, Filas e Árvores como Tipos Abstratos de Dados; Implementação de Tipos Abstratos de Dados. • Bibliografia Básica: • Silva, Osmar Quirino. Estrutura de Dados e Algoritmos usando C: fundamentos e aplicações. Rio de Janeiro: Ciência Moderna, 2007. • Tanenbaum, A.M. Estruturas de Dados em C. Makron Books. • Schildt, H. C Completo e Total. ed. 3. Makron Books.
Start • Programar é muito mais que escrever um programa. • Além de resolver o problema o programa deve ser: claro, eficiente e fácil de modificar. • Para isso, é necessario: disciplina e metodologia que imponham um estilo de desenvolvimento que garanta a qualidade do produto
Introdução a Análise de Algoritmos • Estruturas de dados são formas sistemáticas de organizar e acessar dados. • Algoritmo é um procedimento, passo a passo, para realizar alguma tarefa em um tempo finito. • A análise de algoritmos permite avaliar a qualidade e eficiência de algoritmos: • Complexidade tempo • Complexidade espaço
Motivação • Considere os programas P1 e P2 que resolvem o mesmo problema. • Como decidir qual dos dois é o melhor? • Solução 1: implementar e testar Problemas: • Diversidade de soluções • Modificar dados de entrada para poder medir o desempenho medio pode ser inutil • Solução 2: Analisar para medir sua qualidade
Objetivos da análise • Estabelecer uma medida da qualidade de um algoritmo sem a necessidade de implementa-lo. • Estratégia: associar a cada algoritmo uma função matemática f(n) que meça sua eficiencia utilizando para isso unicamente as características estruturais do algoritmo. • Aplicado também a avaliação de estruturas de dados.
Análise de Algoritmo • A análise da eficiência de um algoritmo ou estrutura de dado pode basear-se em: • tempo de processamento em função dos dados de entrada; • espaço de memória total requerido para os dados; • comprimento total do código; • correcta obtenção do resultado pretendido; • robustez (como comporta-se com as entradas inválidas ou não previstas).
Tempo de Execução • Depende de: • Velocidade do processador • Compilador • Estrutura do algoritmo • Com exceção da última, os fatores mencionados não são inerentes à solução • Portanto, para a análise considera-se apenas a estrutura do algoritmo, além do tamanho dos dados de entrada.
Exemplo de análise • Considere o problema de inverter uma lista simplesmente encadeada:
Solução 1 (inverte direção dos ponteiros): p = cab; cab = NULL; while (p != NULL) { q = p->next; p->next = cab; cab = p; p = q; }
Solução 2 (inverte direção dos nós): p = cab; int temp; struct no *p,*q,*r; for(q = cab ; q->next != NULL ; q = q->next); for(p = cab ; p != q && p != q->next ; p = p->next) { temp = p->info; p->info = q->info; q->info = temp; for(r = p ; r->next != q ; r = r->next); q = r; }
Eficiência de um algoritmo // laco executado 1000 vezes repeticao1() { int i = 1; while(n <= 1000) n = n + 1; } } // laco executado 500 vezes repeticao2() { int i = 1; while(n <= 1000) n = n + 2; } } Nestes casos a eficiência é proporcional ao número de interações, ou seja, f(n) = n
Complexidade de um algoritmo • Análise de Algoritmos é medição de complexidade de algoritmo • Quantidade de "trabalho" necessária para a sua execução, expressa em função das operaçõesfundamentais (ou primitivas), as quais variam de acordo com o algoritmo, e em função do volume de dados.
Operações primitivas (1) Uma operação primitiva corresponde a uma instrução de baixo nível com um tempo de execução constante, mas que depende do ambiente de hardware e software. • Atribuição de valores a variáveis • Chamadas de métodos • Operações aritméticas • Comparações de dois números • Acesso a um array • Retorno de um método
Operações primitivas (2) Ao invés de determinar o tempo de execução específico de cada operação primitiva, deve-se apenas contar quantas operações serão executadas. Assim será assumido que os tempos das diferentes operações primitivas são similares.
Exemplo de contagem do número de operações primitivas // a[] é um array com n>0 elementos inteiros // retorna o maior elemento do array void maximo(a,n) { int c; c = a[0]; for(i = 1; i < n; i++) { if(a[i] > c) c = a[i]; } return c; }
void maximo(a,n) { int c; c = a[0]; 2 operações for(i = 1; i < n; i++) { 4 operações if(a[i] > c) 2 operações c = a[i]; 2 operações } return c; 1 operação } O mínimo de operações é: 2+1+n+4(n-1)+1 T(n) = 5n O máximo de operações é: 2+1+n+6(n-1)+1 T(n) = 7n - 2
Notação Assintótica • É mais importante concentrar na taxa de crescimento de tempo de execução como uma função do tamanho da entrada n, ao invés de concentrar nos detalhes (quantidade de operações primitivas) • A ideia principal é: • encontrar f(n) fácil de calcular e conhecida que se comporte como T(n) – a qtde exata de operações primitivas. • T(n) cresce aproximadamente como f(n) • Em nenhum caso T(n) se comporta pior que f(n) ao se aumentar o tamanho do problema • No exemplo anterior f(n) = n
Dominação Assintótica Definicão : f(n) domina assintoticamente g(n) se existem duas constantes positivas c e n0 tais que, para n ≥ n0, temos |g(n)| ≤ c |f(n)| Exemplo: Seja g(n) = n e f(n) = -n2 Temos que |n| ≤ |-n2| para todo n є N. Fazendo c = 1 e n0 = 0 a expressão |f(n)| ≤ c |g(n)| é satisfeita. Logo, f(n) domina assintoticamente g(n).
Notação O • Notação O = big O = grande O = = ordem de magnitude = O • Caracteriza o comportamento assintótico de uma função, estabelecendo um limite superior quanto à taxa de crescimento da função em relação ao crescimento de n.
Definição: Considere uma função f(n) não negativa para todos os inteiros n≥0. Dizemos que “f(n) é O(g(n))” e escrevemos f(n) = O(g(n)), se existem um inteiro N e uma constante c>0, tais que para todo o inteiro n ≥ N, |f(n)| ≤ c |g(n)| • O(f(n)) representa o conjunto de todas as funções que são assintoticamente dominadas por f(n).
Permite ignorar factores constantes e termos de menor ordem, centrando-se nos componentes que mais afectam o crescimento de uma função. • A ordem de magnitude de f(n) = n + n2é n2, ou seja, O(n2) • A ordem de uma função é igual a ordem do seu termo que cresce mais rapidamente • f(n) = n + 2(n-1) + nk, logo O(f(n)) = O(nk) • f(n) = n + 2(n-1) + 3n + 10, logo O(f(n)) = O(n) • f(n) = 210+ 100, logo O(f(n)) = O(1)
Ordens mais comuns (2) • Terminologia de classes mais comuns de funções: • Constante – O(c) • Logarítmica - O(log n) • Linear - O(n) • Logarítmica linear - O(n log n) • Quadrática - O(n2) • Polinomial – O(nk), com k ≥ 1 • Exponencial – O(an), com a > 1 • Fatorial – O(n!)
Avaliação de complexidade (1) • Algoritmos de Complexidade Exponencial (ou Fatorial) só podem ser executados "em tempo útil" para instâncias de dimensões muito pequenas. • Portanto, no sentido restrito de Solução de um Problema, o termo Algoritmo só deve ser aplicado quando a Complexidade é Polinomial (ou inferior). • Métodos de Complexidade Exponencial correspondem a abordagens do tipo pesquisa exaustiva (ou força-bruta) quando não existe Algoritmo para a resolução de um problema.
Avaliação de complexidade (2) • Os problemas de Pesquisa num Vetor são solucionado por Algoritmos de complexidade entre O(log2n) e O(n). • O problema de Ordenção de um Vetor é solucionado por Algoritmos entre O(n log2n) e O(n2). • Os problemas relacionados com operações Matriciais exigem, em princípio, Algoritmos de complexidade O(n3). Não existe ainda um valor mínimo teoricamente estabelecido.
Teoremas da notação O • Teorema 01: • Se T (n) é O (k f(n)) => T (n) também é O( f(n)) • O que importa não é o valor exato da função de complexidade e sim sua forma • Teorema 02: • Se A1 e A2 são algoritmos tais que TA1(n) é O(f1(n)) e TA2 é O(f2(n)), o tempo empregado para executar A1 seguido de A2 é O (max(f1(n),f2(n))) • Teorema 03: • O(f1(n)) . O(f2(n) = O(f1(n) . f2(n))