320 likes | 440 Vues
Algoritmos Avançados. Análise de Complexidade. COMPLEXIDADE DE ALGORITMOS.
E N D
Algoritmos Avançados Análise de Complexidade
COMPLEXIDADE DE ALGORITMOS • Definição: A Complexidade de um Algoritmo consiste na quantidade de “trabalho” necessária para a sua execução, expressa em função das operações fundamentais, as quais variam de acordo com o algoritmo, e em função do volume de dados
COMPLEXIDADE DE ALGORITMOS • Um algoritmo serve para resolver um determinado problema, e todos os problemas têm sempre uma entrada de dados • O tamanho dessa entrada (N) tem geralmente efeito direto no tempo de resposta de um algoritmo • Dependendo do problema a ser resolvido, já existem algoritmos prontos ou que podem ser adaptados • O problema é: qual algoritmo escolher?
COMO COMPARAR DUAS SOLUÇÕES PARA UM MESMO PROBLEMA ? • Tomemos duas soluções para localizar um elemento em um vetor: LIN e BIN • Temos dois computadores diferentes, A e B, sendo A um Core 2 Duo e B um Celeron. Qual é a melhor solução? LIN ou BIN ?
COMPLEXIDADE DE ALGORITMOS • Pode-se falar de dois tipos de complexidade de algoritmos : • Complexidade Espacial: Quantidade de recursos utilizados para resolver o problema; • Complexidade Temporal: Quantidade de tempo utilizado. • Pode ser vista também como o número de instruções necessárias para resolver um determinado problema; • Em ambos os casos, a complexidade é medida de acordo com o tamanho dos dados de entrada (n) • Estamos mais interessados em calcular a Complexidade Temporal de um algoritmo!
COMPLEXIDADE DE ALGORITMOS • Existem três perspectivas para análise de complexidade: • Melhor Caso • Caso Médio • Pior Caso • Nas três perspectivas, a função f(n) retorna a complexidade de um algoritmo com entrada de tamanho n
ANÁLISE DO MELHOR CASO • Definido pela letra grega Ω(Ômega) • Exprime o menor tempo de execução de um algoritmo para uma entrada de tamanho n • É pouco usado, por ter aplicação em poucos casos. • Ex.: • O algoritmo de pesquisa sequêncial em um vetor tem complexidade f(n) = Ω(1) • A análise assume que o número procurado seria o primeiro selecionado na lista. • Abordagem otimista!
ANÁLISE DO CASO MÉDIO • Definido pela letra grega θ (Theta) • Deve-se obter a média dos tempos de execução de todas as entradas de tamanho n, ou baseado em probabilidade de determinada condição ocorrer • Ex.: • O algoritmo de pesquisa sequêncial em um vetor tem complexidade f(n) = θ(n/2) • Em média será necessário visitar n/2 elementos do vetor até encontrar o elemento procurado • Melhor aproximação • Muito difícil de determinar na maioria dos casos
ANÁLISE DO PIOR CASO • Representado pela letra grega O • O maiúsculo. Trata-se da letra grega ômicron maiúscula • Baseia-se no maior tempo de execução sobre todas as entradas de tamanho n • É o método mais fácil de se obter. • Ex.: • O algoritmo de pesquisa sequêncial em um vetor tem complexidade f(n) = O(n) • No pior caso será necessário visitar todos os n elementos do vetor até encontrar o elemento procurado • Abordagem pessimista!
A NOTAÇÃO O • Tempo (ou espaço) é contabilizado em número de passos do algoritmo (unidade de armazenamento) • Análise do algoritmo determina uma função que depende do tamanho da entrada n. • 10n3 + 4n -10 • à medida que n aumenta, o termo cúbico começa a dominar • A constante do termo cúbico tem relativamente a mesma importância que a velocidade da CPU
A NOTAÇÃO O • Desprezar constantes aditivas ou multiplicativas • Número de passos 3n será aproximado para n • Interesse assintótico • termos de menor grau podem ser desprezados • n2 + n será aproximado para n2 • 6n3 + 4n - 9 será aproximado para n3
CÁLCULO DA COMPLEXIDADE • Foi visto que, para calcular a complexidade de um algoritmo, deve-se analisar o pior caso • A análise deve ser feita de acordo com a tabela a seguir
CÁLCULO DA COMPLEXIDADE: ALGORITMO ITERATIVO void FloidWarshall(int dist[][]) { int i; int j; int k; for ( k = 0; k < n; k++ ) { for ( i = 0; i < n; i++ ) { for ( j = 0; j < n; j++ ) { int a = dist[i][j]; int b = dist[j][k]; int c = dist[k][j]; dist[i][j] = min(a, b + c ); } } } } int min(int a, int b) { if(a < b) return a; return b; } 1 + 1 + 1 + n * ( n * ( 1 + 1 + 1 + n * ( n * ( n * ( 1 + 1 + 1 + 3 ) ) ) 3 + n * n * n * 6 3 + 6n3 O(n3) n * ( 1 + 1 + 1 + 3 ) ) ) 1 + 1 + 1
CÁLCULO DA COMPLEXIDADE: ALGORITMO RECURSIVO • A questão se complica um pouco quando se trata de algoritmos recursivos • Embora não haja um método único para esta avaliação, a complexidade de um algoritmo recursivo é definida em função de componentes como: • Complexidade da base • Complexidade do núcleo • Profundidade da recursão • Número de vezes que o procedimento recursivo é invocado • Depende do tamanho do problema e da taxa de redução do tamanho do problema • É justamente em sua determinação que reside o problema!
CÁLCULO DA COMPLEXIDADE: ALGORITMO RECURSIVO int fatorial( int n ) { if( n == 0 ) { return 1; //Base } else { return n * fatorial( n – 1 ); // Núcleo } } • A redução se dá de uma em uma unidade, de n até chegar a 0 • Logo, a profundidade da recursão é n • Tanto o núcleo quando a base executam apenas uma operação • A base é executada uma única vez e o núcleo n - 1 vezes • Logo, o número de operações executadas é ((n – 1) * 1) + 1, resultando em uma complexidade O(n)
ORDENS DE ALGORITMOS • Complexidade Constante • Complexidade Linear • Complexidade Logarítmica • Complexidade Log Linear • Complexidade Quadrática • Complexidade Cúbica • Complexidade Exponencial • Complexidade Fatorial
COMPLEXIDADE CONSTANTE - O(1) • São os algoritmos onde a complexidade independe do tamanho n de entradas • É o único em que as instruções dos algoritmos são executadas um número fixo de vezes if (condição == true) then { realiza alguma operação em tempo constante } else { realiza alguma operação em tempo constante }
COMPLEXIDADE LINEAR –O(N) • Uma operação é realizada em cada elemento de entrada, ex.: pesquisa de elementos em uma lista for (i = 0; i < N; i = i + 1 ) { if (condição == true) then { realizaalgumaoperaçãoem tempo constante } else { realizaalgumaoperaçãoem tempo constante } }
COMPLEXIDADE LOGARÍTMICA - O(LOGN) • Ocorre tipicamente em algoritmos que dividem o problema em problemas menores int PesquisaBinaria ( int array[], int chave , int N){ int inf = 0; int sup = N - 1; int meio; while (inf <= sup) { meio = (inf+sup)/2; if (chave == array[meio]) return meio; else if (chave < array[meio]) sup = meio-1; else inf = meio+1; } return -1; // não encontrado }
COMPLEXIDADE LOG LINEAR –O(NLOGN) • Ocorre tipicamente em algoritmos que dividem o problema em problemas menores, porém juntando posteriormente a solução dos problemas menores void merge(int inicio, int fim) { if (inicio < fim) { int meio = (inicio + fim) / 2; merge(inicio, meio); merge(meio + 1, fim); mesclar(inicio, meio, fim); } }
COMPLEXIDADE QUADRÁTICA –O(N²) • Itens são processados aos pares, geralmente com um loop dentro do outro void bubbleSort(int[] a) { for (int i = 0; i < a.length-1; i++) { for (int j = 0; j < a.length-1; j++) { if (a[j] > a[j+1]) { swap(a, j, j+1); } } } }
COMPLEXIDADE CÚBICA –O(N³) • Itens são processados três a três, geralmente com um loop dentro do outros dois int dist[N][N]; int i, j, k; for ( k = 0; k < N; k++ ) for ( i = 0; i < N; i++ ) for ( j = 0; j < N; j++ ) dist[i][j] = min( dist[i][j], dist[i][k] + dist[k][j] );
COMPLEXIDADE EXPONENCIAL – O(2N) • Utilização de “Força Bruta” para encontrar a solução de um problema. • A solução geralmente é baseada diretamente no enunciado do problema e nas definições dos conceitos envolvidos • Ex.: • Utilizando apenas números é possível criar 10n senhas de n dígitos • Um algoritmo de força bruta para quebrar uma dessas senhas tem complexidade O(2n)
COMPLEXIDADE FATORIAL –O(N!) • Também é baseada na utilização de força bruta para encontrar a solução de um problema • Consiste em testar todas as possíveis permutações existentes na solução à procura da solução ótima para o problema • Ex.: Problema do Caixeiro Viajante • Encontrar a rota mínima para visitar várias cidades sem repetir nenhuma • É um problema base para o projeto de microchips, sequênciamento de genôma e muitas outras aplicações • Não possui solução exata eficiente (Problema NP) • Utilização de heurísticas para aproximar a solução ótima
ORDENS DE COMPLEXIDADE • Imagine um computador que leva 1ms para executar uma operação. • A tabela abaixo indica o tempo aproximado de execução de um algoritmo com diferentes ordens de complexidades para 3 tamanhos de entrada
LIMITES SUPERIOR E INFERIOR • Todas as ordens de complexidade vistas definem o Limite Superior (Upper Bound) dos Algoritmos • Qualquer que seja o tamanho da entrada, o tempo de execução crescerá com velocidade igual ou inferior a apontada pela análise de complexidade. • Algumas otimizações podem ser feitas para melhorar o limite superior; • Existem, porém, os Limites Inferiores (Lower Bound) para certos problemas, que são pontos a partir dos quais não é mais possível otimizar uma solução algorítmica
LIMITES SUPERIOR E INFERIOR • Dado um problema de Multiplicação de 2 matrizes N X N. • A solução trivial tem complexidade O(n3); • Sabemos assim que a complexidade deste problema não deve superar O(n3), uma vez que existe um algoritmo com está ordem complexidade que o resolve; • Este limite superior de um algoritmo pode mudar se alguém descobrir um algoritmo melhor.
LIMITES SUPERIOR E INFERIOR • Strassen resolveu o problema com uma complexidade de O(nlog 7) • Outros pesquisadores melhoraram ainda mais este resultado. • Atualmente o melhor resultado é o de Coppersmith e Winograd de O(n2.376). • O limite superior de um algoritmo é parecido com o recorde mundial de uma modalidade de atletismo. • Ele é estabelecida pelo melhor atleta (algoritmo) do momento. Assim como o recorde mundial, o limite superior pode ser melhorado por um algoritmo (atleta) mais veloz.
LIMITES SUPERIOR E INFERIOR • Às vezes é necessário mostrar que, para um dado problema, qualquer que seja o algoritmo a ser usado, requer um certo número mínimo de operações: o Limite Inferior • Para o problema de multiplicação de matrizes de ordem n, apenas para ler os elementos das duas matrizes de entrada leva O(n2). Assim uma cota inferior trivial é Ω(n2).
LIMITES SUPERIOR E INFERIOR • Na analogia anterior, o limite inferior não dependeria mais do atleta. • Seria algum tempo mínimo que a modalidade exige, qualquer que seja o atleta. • Um limite inferior trivial para os 100 metros seria o tempo que a velocidade da luz leva para percorrer 100 metros no vácuo. • Se um algoritmo tem uma complexidade que é igual ao limite inferior do problema então o algoritmo é ótimo. • O algoritmo de CopperSmith e Winograd é de O(n2.376) mas o limite inferior é de Ω(n²). • Portanto não é ótimo. Este limite superior ainda pode ser melhorado