Prévia do material em texto
CEFET-MG – Engenharia de Computação Recursão e Análise de Complexidade de Algoritmos Recursivos Algoritmos e Estruturas de Dados I 2015/1 Sumário Conceito matemático de recursão Conceito computacional de recursão Vantagens e desvantagens da recursão Aplicações da recursão Análise de complexidade de algoritmos recursivos Resolução de relações de recorrência Recursão Um objeto recursivo é descrito em termos de si mesmo Objetos recursivos Conceito matemático de recursão Uma função recursiva é uma função parcialmente definida em termos de si mesma Função fatorial Série de Fibonacci: 0,1,1,2,3,5,8,13,21, 34,55,89,144,233,377,610,987,1597, ... Soma dos primeiros números naturais O próprio elemento definido aparece na definição... Conceito matemático de recursão Função fatorial Série de Fibonacci Soma dos primeiros números naturais Condição de terminação parte fundamental da definição Conceito computacional de recursão Um procedimento recursivo é aquele que chama a si mesmo direta ou indiretamente int abc() { ...... ...... abc (); ...... } int abc() { ...... ...... xyz (); ...... } int xyz() { ...... ...... abc (); ...... } Recursão direta Recursão indireta Conceito computacional de recursão Recursão indireta Função fatorial recursiva – recursão direta /* entrada: inteiro n não negativo resultado: retorna o fatorial de n */ long fatorial(int n) { if ((n == 0) || (n == 1)) // condição de terminação return 1; else // caso recursivo return (n * fatorial(n 1)); } Função fatorial recursiva – execução /* entrada: inteiro n não negativo resultado: retorna o fatorial de n */ long fatorial(int n) { if ((n == 0) || (n == 1)) return 1; else return (n * fatorial(n 1)); } Exemplo: fatorial de 5 5! = 5 * 4! 4! = 4 * 3! 3! = 3 * 2! 2! = 2 *1! 1! = 1 Função fatorial recursiva – execução Chamadas de função e uso da memória temp= 5*4! fat(5) temp= 5*4! fat(5) temp= 4*3! fat(4) temp= 5*4! fat(5) temp= 4*3! fat(4) temp= 3*2! fat(3) temp= 4*3! fat(4) temp= 5*4! fat(5) temp= 3*2! fat(3) temp= 2*1! fat(2) Uso de memória e chamadas de função Tempo de execução temp= 4*3! fat(4) temp= 5*4! fat(5) temp= 3*2! fat(3) temp= 2*1! fat(2) retorna 1 fat(1) Cada chamada de função é denominada ATIVAÇÃO da função Conceito computacional de recursão Um procedimento recursivo é aquele que chama a si mesmo direta ou indiretamente - ou, de forma equivalente: Uma função é recursiva se uma nova ativação pode começar antes que uma ativação anterior da mesma função tenha terminado Ativação de função e pilha de memória Toda vez que uma função é ativada: ela é carregada na memória e espaço é reservado para variáveis locais, parâmetros da função, variáveis temporárias criadas automaticamente pelo sistema para armazenar cálculos intermediários, endereço de retorno (quem chamou?) o estado corrente da computação deve ser armazenado para permitir a volta da chamada recursiva: prosseguimento da execução do programa requer uma chamada de sistema (sistema operacional) => maior tempo de execução No caso da função recursiva: Para cada chamada, um novo espaço é alocado na memória! pilha de recursão: cada ativação é empilhada na memória O custo das chamadas é elevado: custo da recursão tempo de execução e espaço em memória Condição de parada ou terminação Deve ser cuidadosamente explicitada para evitar infinitas chamadas recursivas na prática: o programa finalizaria por falta de memória Análise do número de chamadas recursivas Profundidade da recursão: número máximo de chamadas de função empilhadas durante a execução da função Exemplo: fatorial (n): n chamadas da função empilhadas temp= 4*3! fat(4) temp= 5*4! fat(5) temp= 3*2! fat(3) temp= 2*1! fat(2) retorna 1 fat(1) Série de Fibonacci: versão iterativa unsigned long fib (int n) { unsigned long atual, menos1=1, menos2=0; if (n <=1) return n; else for (int i=2; i <= n; i++) { atual = menos1 + menos2; menos2 = menos1; menos1 = atual; } return atual; } Série de Fibonacci: 0,1,1,2,3,5,8,13, 21,34,55,89,144 ... Série de Fibonacci: versão recursiva unsigned long fibrec(int n) { if (n <= 1) return n; else return fibrec(n-1) + fibrec(n-2); } Árvore de recursão Observem a repetição de cálculos de termos Fibonacci recursivo: número de chamadas e profundidade da recursão 0, 1, 1, 2, 3, 5 chamada # 1 chamada # 2 chamada # 3 chamada # 4 chamada # 5 chamada # 6 chamada # 7 chamada # 8 chamada # 9 chamada # 10 chamada # 11 chamada # 12 chamada # 13 chamada # 14 chamada # 15 O valor final e' 5 Série de Fibonacci: número de somas termo valor #somas #somas rec 0 0 = 0 0 0 1 1 = 1 0 0 2 1 = 1 1 1 3 2 = 2 2 2 4 3 = 3 3 4 5 5 = 5 4 7 6 8 = 8 5 12 7 13 = 13 6 20 8 21 = 21 7 33 9 34 = 34 8 54 10 55 = 55 9 88 11 89 = 89 10 143 12 144 = 144 11 232 13 233 = 233 12 376 14 377 = 377 13 609 15 610 = 610 14 986 16 987 = 987 15 1596 17 1597 = 1597 16 2583 18 2584 = 2584 17 4180 19 4181 = 4181 18 6764 20 6765 = 6765 19 10945 ... 37 24157817 = 24157817 36 39088168 38 39088169 = 39088169 37 63245985 39 63245986 = 63245986 38 102334154 40 102334155 = 102334155 39 165580140 Número de somas executadas nas versões recursiva e não recursiva da função Vantagens e desvantagens da recursão Vantagens técnica de programação versátil e poderosa permite definir um conjunto infinito de objetos com um comando finito ajuda a descrever estruturas de dados eficientes e elegantes pode oferecer uma maneira elegante e concisa para expressar uma solução recursiva para um problema a versão recursiva de um programa pode ser muito mais simples do que a versão não recursiva Desvantagens custo: chamadas de função são demoradas se a recursão for eliminada o programa pode executar mais rápido custo: gasto adicional de memória o programa pode terminar por falta de memória condição de terminação pode ser causa de erros um algoritmo recursivo pode ser terrivelmente ineficiente Aplicação da recursão aplicação: para problemas e/ou estruturas de dados de natureza recursiva, que não tenham implementação simples não recursiva nunca deve ser usada no lugar de um laço simples ajuda a descrever algoritmos e estruturas de dados de natureza recursiva, de maneira eficiente e elegante ex: árvores – estruturasde dados e algoritmos muitos algoritmos interessantes são expressos de forma bastante simples usando a recursão ex: quicksort Implementação da recursão Para evitar laços infinitos definir bem as condições de finalização da recursão Problema da terminação: como garantir a finalização da recursão definir uma condição de terminação em geral, um procedimento recursivo P recebe um parâmetro n e é chamado recursivamente para P(m) onde m < n deve-se garantir que a condição de terminação será alcançada Estimar o número de chamadas recursivas o número de vezes que a função recursiva será chamada deve ser um número pequeno e conhecido Conclusão: prefira iteração sempre que possível long fatorial(int n) { int k; long resultado = 1; for (k = 2; k <= n; k++) resultado = resultado * k; return resultado; } As funções fatorial e Fibonacci NUNCA devem ser implementadas de maneira recursiva As implementações recursivas de fatorial e Fibonacci são usadas somente para fins didáticos Quando não usar a recursão quando um laço pode substitui-la perfeitamente exemplos: função fatorial e função de Fibonacci atenção para recursão de cabeça e cauda a chamada recursiva vem no início ou no fim do código: pode ser facilmente transformada em laço Análise de complexidade de algoritmos recursivos O tempo de execução de um algoritmo recursivo pode ser expresso por uma relação de recorrência Uma relação de recorrência é uma equação ou inequação que descreve uma função em termos de seus valores sobre entradas menores Uma relação de recorrência é composta por duas partes caso base expressa a condição de terminação da definição recursiva é o resultado da relação para valores pequenos de n, quando nenhuma chamada recursiva é feita caso indutivo parte em que os termos da sequência são definidos em função dos termos menores expressa as chamadas recursivas da função Expressando o custo em relações de recorrência O tempo de execução de um algoritmo recursivo é usualmente denominado T(n) Exemplo: para a função fatorial seja T(n) o número de multiplicações executado pela função fatorial recursiva o custo da execução é expresso pela relação de recorrência Mais exemplos de relações de recorrência Analisando algoritmos recursivos Etapas da análise de complexidade de algoritmos recursivos 1) defina T(n) como a função de complexidade de tempo do algoritmo, para uma entrada de tamanho n 2) analise o algoritmo e encontre o custo para o caso base 3) analise o algoritmo e encontre o custo para a parte recursiva 4) apresente a relação de recorrência 5) resolva a relação de recorrência A solução de uma relação de recorrência é uma fórmula fechada para qualquer valor de n, o custo pode ser calculado com um número fixo de operações elementares há várias técnicas para resolver relações de recorrência ex: método da iteração: expandir T(n) até identificar um padrão Exemplo de solução 1 Somar os termos lado a lado Fórmula fechada Exemplo de solução 2 Exemplo de solução 3 Exemplo de solução 4 Recursão em jogos Jogo das Torres de Hanói Sites sobre recursão: divirta-se Veja algoritmos animados de recursão e páginas sobre recursão na Web e no Youtube Recursion Animated recursion Animation of recursion Recursive functions Slide 1 Slide 2 Slide 3 Slide 4 Slide 5 Slide 6 Slide 7 Slide 8 Slide 9 Slide 10 Slide 11 Slide 12 Slide 13 Slide 14 Slide 15 Slide 16 Slide 17 Slide 18 Slide 19 Slide 20 Slide 21 Slide 22 Slide 23 Slide 24 Slide 25 Slide 26 Slide 27 Slide 28 Slide 29 Slide 30 Slide 31 Slide 32