ESTRUTURAS DE INFORMAÇÃO E ANÁLISE DE ALGORITMOS Estrutura de informação é uma maneira sistemática de organizar e aceder a dados e algoritmo é um procedimento passo a passo para realizar uma tarefa num intervalo de tempo finito. Estes conceitos são fundamentais em computação e dedicaremos a nossa atenção à discussão de princípios e paradigmas para que o projecto e a implementação de boas estruturas e algoritmos formem uma ferramenta para o desenvolvimento de soluções de importantes problemas através do computador. Um algoritmo deve apresentar as seguintes características: Ter entrada e saída Ser finito Ser definido Além disso, ao projectarmos estruturas e algoritmos teremos que ter sempre presente dois objectivos principais: Correcção Eficiência Correcção significa trabalhar correctamente quaisquer que sejam os dados de entrada dentro de um certo domínio. Assim, uma estrutura que deve guardar uma colecção de números ordenadamente, nunca deve permitir que sejam guardados números fora de ordem. De igual modo um algoritmo para listar valores por ordem nunca deve fazer a saída de valores fora dessa ordem. Quanto à eficiência significa que quer estruturas quer algoritmos devem ser rápidos e nunca usar recursos do computador superiores ao necessário. Estes objectivos acarretam a produção de software de qualidade e por isso podemos acrescentar que haverá também objectivos a cumprir nas implementações de estruturas e de algoritmos, tais como: Robustez Adaptabilidade Reutilização Por robustez entende-se a possibilidade de dar resposta à manipulação de entradas não esperadas. Claro que a robustez não surge automaticamente, tem que ser projectada desde o início. Departamento de Engª Informática do ISEP 1
Adaptabilidade significa permanecer embora se alterem as condições do seu ambiente, por exemplo não deveria ser problema a chegada do novo milénio e os efeitos nos cálculos de datas se o software fosse adaptável Reutilização significa que o mesmo código seja um componente de diferentes sistemas em vários domínios de aplicação. No entanto esta reutilização deve ser usada com cuidado pois é uma das fontes de erros. A reutilização traduz-se numa importante poupança de tempo e consequentemente de dinheiro. Tem havido muita investigação no campo da engenharia de software e de linguagens de programação para o desenvolvimento de metodologias para o projecto de software que sejam simples e poderosas. Actualmente uma que se mostra bastante promissora é baseada na orientação para objectos. Os princípios orientadores desta aproximação são: Abstracção Encapsulamento Modularidade O conceito de abstracção existe quando somos capazes de destacar num sistema as suas partes fundamentais e descrevê-las numa linguagem simples e precisa. Por exemplo, no caso de um editor de texto, no "editar" da barra de menu, temos geralmente as operações de "cortar" e "colar". Podemos dizer que "cortar" retira o texto seleccionado e coloca-o num buffer externo e que o "colar" insere o conteúdo desse buffer numa posição específica do texto. A definição destas operações foi feita de uma maneira clara e simples, sem necessidade de entrar em pormenores, isto é, introduzindo abstracção. Usando este paradigma para projectarmos estruturas de informação atingimos o que chamamos Tipos Abstractos de Dados (ADT). Um Tipo Abstracto de Dados é um modelo matemático duma estrutura de dados que especifica o tipo de dados guardados e as operações que se podem realizar sobre eles. Um Tipo Abstracto de Dados será modelado por uma linguagem orientada por objectos através de uma classe. O conceito de encapsulamento define que os diferentes componentes de um sistema de software devem implementar uma abstracção como descrito acima sem revelarem pormenores internos da sua implementação. Nós percebemos como usar as operações de "cortar" e "colar" sem precisarmos de saber exactamente como está implementado. Uma das vantagens do encapsulamento é que dá ao programador toda a liberdade na implementação de pormenores do sistema. A modularidade refere-se a uma organização estrutural em que os diferentes componentes dum sistema de software estão divididos em unidades funcionais que podem interactuar de forma bem definida. Além disso esta forma organizacional está estritamente ligada à noção de hierarquia, Departamento de Engª Informática do ISEP 2
devem-se estruturar os diferentes módulos de uma forma hierárquica, agrupando funcionalidades comuns ao mais alto nível e comportamentos específicos serão extensões dos mais gerais. Esta forma de estruturar o projecto de software,recorrendo á modularidade e hierarquia vai facilitar a reutilização. Estes princípios agora descritos (abstracção, encapsulamento e modularidade) para projectarmos o software numa aproximação orientada para objecto, exigem técnicas específicas que se traduzem em : Classes e objectos Interfaces e tipificação Herança e Polimorfismo. Estes conceitos já foram estudados em disciplinas anteriores e portanto podem ser consultados por exemplo nos apontamentos de LPII. Enumeramos um conjunto de características a que deve obedecer o projecto de sofware, nomeadamente de estruturas de informação e algoritmos no paradigma de orientação para objectos. Para que o entendimento seja mais completo e consigamos saber porquê usar uma determinada estrutura ou um determinado algoritmo precisamos de ver como è que podemos analisar algoritmos. A Análise de algoritmos pode ser feita atendendo a diferentes aspectos. Assim temos análise envolvendo : Funcionalidade (tracing / teste ) Correcção ( técnicas matemáticas ) Complexidade (espacial e temporal ) É sobre a análise de complexidade de algoritmos que nos vamos debruçar. ANÁLISE de COMPLEXIDADE é um dos critérios para avaliar algoritmos segundo o qual é avaliado o índice de crescimento do tempo (temporal) ou do espaço (espacial) exigido para resolver instâncias do problema cada vez maiores. TEMPO de COMPLEXIDADE de um ALGORITMO é o tempo exigido para a execução de um algoritmo, expresso como uma função do tamanho do problema. TAMANHO do PROBLEMA é o inteiro associado a um problema que é a medida da quantidade dos dados de entrada. Departamento de Engª Informática do ISEP 3
ESPAÇO de COMPLEXIDADE de um ALGORITMO é o espaço exigido para a execução de um algoritmo, expresso como uma função do tamanho do problema. COMPLEXIDADE TEMPORAL O tempo para correr completamente um programa, é a soma do tempo de compilação e do tempo de execução. O tempo de compilação depende das máquinas, compiladores e não pròpriamente das características do algoritmo. Analisaremos então o tempo de execução. Sabemos que o valor exacto do tempo de execução de um algoritmo também depende da linguagem e da máquina utilizada, logo não é isto que nos interessa, mas uma ordem de grandeza desse tempo em função da quantidade dos dados de entrada. Pretendemos saber como se comporta um algoritmo quanto ao tempo de execução, isto é como varia o tempo de execução do algoritmo atendendo só à variação da quantidade de dados a processar. Por exemplo, ordenar um vector com 10 elementos não leva o mesmo tempo que ordenar um vector com 1000 elementos. O que nos interessa é relacionar a variação que existe no tempo de ordenação com a variação do número de elementos. Interessa-nos portanto prever o crescimento do tempo de execução conforme mudam as características da instância do problema, para podermos comparar dois programas que realizam a mesma função ou para ajuizarmos se um determinado programa pode ser usado para fornecer respostas em tempo real. São várias as razões que levam à necessidade deste tipo de estudo. O gráfico abaixo mostra algumas das funções características associadas à complexidade temporal de algoritmos Departamento de Engª Informática do ISEP 4
140 120 n! 2 n Função Complexidade 100 80 60 40 n*log n n 2 20 n log n 0 1 2 3 4 5 6 7 8 9 10 Dados Nomeadamente: n n n! 2 n, são exemplos de funções exponenciais e há ainda funções do tipo polinomial como por exemplo : n 2 n 3 n n log n log n Conforme se pode verificar no gráfico anterior os algoritmos com complexidade do tipo exponencial são muito lentos, uma vez que a um aumento pequeno na dimensão implica um enorme aumento no tempo de execução, só serão aplicáveis quando existem poucos dados de entrada. No caso de problemas de grandes dimensões deverá sempre que possível fazer-se a substituição de algoritmos exponenciais por algoritmos polinomiais. É evidente que quando a dimensão dos dados de entrada aumenta qualquer algoritmo polinomial é mais eficiente que um algoritmo exponencial FUNÇÂO VALORES APROXIMADOS n 10 100 1000 n log n 33 664 9966 n 3 1000 1000000 10 9 2 n 1024 1.27 * 10 30 1.05 * 10 301 n! 3628800 10 158 4 * 10 2567 Departamento de Engª Informática do ISEP 5
INTRODUÇÃO à NOTAÇÃO O( ) ( big Oh ) Este tipo de notação é assimptótica porque vai ser definida para um comportamento limite, quando aumenta o tamanho do problema. Para introduzirmos esta questão começaremos por exemplificar como fazer a análise da complexidade temporal de um algoritmo Consideremos um algoritmo que determina a quantidade de 1s existentes na representação binária de um número n. algoritmo: 1. ler(n) 2. conta? 0 3. Enquanto n > 0 fazer 4. conta? conta + resto de n por 2 5. n? n / 2 Fim enquanto Seja t1 o tempo necessário para a execução dos passos 1. e 2. t2 do passo 3. t3 dos passos 4. e 5. O tempo total para execução do algoritmo, atendendo a estes tempos e ao número de vezes que os diferentes passos se repetem será: T(n) = t1 * 1 + t2 * (K+1) + t3 * K, em que K será o número de iterações do ciclo. O valor de K irá depender do valor de n, quanto maior for o número n, mais iterações terão que ser feitas, isto é mais divisões por 2 serão efectuadas até o valor de n ser nulo. Temos agora que relacionar este K com n. Se atendermos à construção da representação binária de números verificamos que o número de divisões por 2 se relaciona com o log 2 n,isto é, a função inversa da potência de base 2 que se aplica para o cálculo do valor decimal de um número na sua representação binária. Com efeito verifica-se: n K função Departamento de Engª Informática do ISEP 6
5 3 1 + log 2 5 7 3 1 + log 2 7 8 4 1 + log 2 8 18 5 1 + log 2 18 NOTA 1: log 2 n design-se por chão do log 2 n e significa o maior valor inteiro contido em log 2 n. Ex: 3.1 = 3-2.2 = -3 NOTA 2 : X representa-se o tecto de X, isto é, o menor inteiro não inferior a X Ex: 3.1 = 4-2.2 = -2 Retomando a expressão que nos determina o tempo total de execução do algoritmo temos: T(n) = t1 * 1 + t2 * (K+1) + t3 * K T(n) = t1 + t2 + k * (t2 + t3), substituindo K por (1 + log 2 n ) vem, T(n) = t1 + t2 + (1 + log 2 n )*(t2 + t3) como t1 e t2 e t3 são constantes para um dado computador, é possível encontrar uma constante C tal que se verifique, T(n) < C * (1 + log 2 n ) representando (1 + log 2 n ) por g (n) teremos: T(n) < C * g(n), para n > 1. Assim dizemos que o tempo de execução é de ordem g(n) = 1 + log 2 n e representamos por t(n) = O (g(n)) Dá-nos ideia de como é o crescimento do tempo de execução com a variação de n. Para diferentes computadores os tempos t1, t2, t3 são diferentes mas g(n) é constante. A notação das ordens de grandeza permite representar algo independente do computador. Em geral dizemos que f(n) é da ordem de g(n) quando existe um K tal que : n >= n 0? f(n) < =K * g(n) ou seja, g(n) multiplicado por uma constante apropriada majora f(n) para todos os n > n 0. Departamento de Engª Informática do ISEP 7
No exemplo, g(n) = 1 + log 2 n < 2 * log 2 n < 2 * log 2 n, n > 2. Podemos então dizer que o algoritmo estudado é da ordem ( ou complexidade temporal ) de log 2 n porque, t(n) < C * g(n) < 2C * log 2 n Se tivéssemos : g(n) < 1000 n 3 diríamos que era de ordem n 3? O (n 3 ) g(n) < 5n2 + 3n + 7 ordem n 2? O (n 2 ) g(n) < 1032 ordem 1? O (1 ) g(n) < n + log n ordem n? O (n) g(n) < 1 + 2 + 3 +4 +... + n ordem n 2? O (n 2 ) NOTA : 1 + 2 + 3 +4 +... + n = n ( n +1 )/2 2 0 + 2 1 + 2 2 + 2 3 +... + 2 n = 2 n+1 1 Interessa também referir o seguinte: Se T1(n)=O(f(n)) e T2(n)=O(g(n)) então a) T1(n) + T2(n) = max(o(f(n), O(g(n)) b) T1(n) * T2(n)= O(f(n) * g(n)). Podemos analisar o algoritmo seguindo outras ordens, nomeadamente a ordem omega e a ordem teta. Assim, diz-se que o algoritmo A é de ordem? (g(n)) se existe uma constante C, para valores de n>=n0 tal que se verifica a relação T(n)> C* g(n). Diz-se que o algoritmo A é de ordem? (g(n)) se é de O(g(n)) e de ordem? (g(n)). Faremos nesta introdução só a análise big oh. O exemplo apresentado anteriormente, era determinístico, isto é, não foi preciso atender à forma de distribuição dos dados para obter o tempo de execução. Geralmente tal não se passa assim, para ser possível fazer a análise de complexidade é necessário supor determinada distribuição dos dados. E nesse caso há a considerar : Departamento de Engª Informática do ISEP 8
Caso mais desfavorável Caso mais favorável Caso médio Vamos então analisar um caso não determinístico, seja por exemplo a pesquisa sequencial de um valor X num vector com n elementos. Algoritmo: Inicio enc? 0 i? 1 Enquanto i <= n e não enc se v( i) = X então enc? 1 Fim enquanto............ senão i? i+1 Para fazermos a análise de complexidade devemos primeiro identificar qual(ais ) a operação activa(s), aquela que vai ser preponderante na execução do algoritmo, como estamos a fazer a análise big oh, podemos desprezar os termos de ordem baixa, uma vez que big oh é um limite superior. Neste caso, a operação activa será: se v( I) = X e vamos analisar o número de vezes que esta operação será executada em função do número de elementos do vector, tamanho do problema. Como fàcilmente se conclui, agora não é só o tamanho do problema que vai influenciar o tempo de execução, mas também é fundamental, a posição do elemento a pesquisar no vector. Assim se o valor X se encontrar no início do vector, o tempo de execução não é o mesmo que no caso de ele se encontrar no final. Teremos então de considerar os 3 casos anteriormente enunciados. Caso mais favorável: X encontra-se no início do vector, só é feita uma comparação. Complexidade O ( 1 ). Departamento de Engª Informática do ISEP 9
Caso menos favorável X encontra-se no fim do vector, são feitas n comparações. Complexidade O ( n ). Caso médio : Para analisarmos esta situação, uma vez que não sabemos em que posição se vai encontrar X, teremos que calcular o tempo médio supondo que se verificam ni ocorrências em cada uma das posições e depois fazer uma média pesada. Assim, ocorreu: n 1 vezes na posição 1 e levou o tempo t 1 n 2 vezes na posição 2 e levou o tempo t 2 n 3 vezes na posição 3 e levou o tempo t 3... n n vezes na posição n e levou o tempo t n n k vezes não ocorreu e levou o tempo t k Sendo N = n 1 + n 2 + n 3 +... + n n + n k O tempo médio será: T m = (n 1 * t 1 + n 2 * t 2 + n 3 * t 3 +... +n n * t n + n k * t k ) /N T m = n 1 /N* t 1 + n 2 /N * t 2 + n 3 /N * t 3 +... +n n /N * t n + n k /N * t k Quando o número de ocorrências for infinito ni /N tenderá a ser a probabilidade de ocorrer a situação i. Assim, Tm =? (probabilidade de ocorrer a situação i) * (o tempo da ocorrência i) Sendo, p a probabilidade de X existir no vector, será: Probabilidade de X não existir no vector Probabilidade de X estar na posição i 1 - p p / n Aplicando a fórmula deduzida para Tm, teremos: Departamento de Engª Informática do ISEP 10
Tm = prob. de X estar em 1 * 1 + prob. de X estar em 2 * 2 +...... + prob.de X estar em n * n + prob. de X não estar no vector * n Tm = p /n* 1 + p/n *2 + p/n * 3 +... +p/n * n + (1-p) * n Tm = p /n * (1 + 2 + 3 +... + n) + (1- p) * n Tm = p * (1 /n + 2/n + 3/n +... + n/n) + (1- p) * n Tm = p * (n + 1) /2 + (1- p) * n Se p = 1? Tm = ( n + 1)/2? O (n) Se p = 1/2? Tm = 3 /4 n? O (n) Assim, qualquer que seja o valor de p (entre 0 e 1, uma vez que é uma probabilidade) podemos considerar que Tm é de ordem n. É vulgar acontecer como neste exemplo, o caso médio coincidir com o caso mais desfavorável. Se os computadores são mais rápidos podemos no mesmo tempo tratar problemas maiores, mas é a complexidade do algoritmo que determina o aumento do tamanho do problema que pode conseguir-se com esse aumento de velocidade, como já foi referido. Descrevemos abaixo uma tabela que evidencia a variação do tamanho do problema a tratar para o mesmo tempo de processamento de acordo com a complexidade temporal do algoritmo. ALGORITMO COMPLEXIDADE 1 segundo 1 minuto 1 hora A1 n 1000 6*10 4 3.6*10 6 A2 n log n 140 4893 2*105 A3 n 2 31 244 1897 A4 n 3 10 39 153 A5 2 n 9 15 21 Supondo que a velocidade da máquina aumentava 10 vezes, vejamos o que se passaria no aumento do tamanho do problema para algumas complexidades temporais mais vulgares : Departamento de Engª Informática do ISEP 11
ALGORITMO COMPLEXIDADE TEMPORAL TAMANHO MAX na máquina inicial TAMANHO MAX na máq. 10 x + rápida A1 n s1 10 * s1 A2 n log n s2 10* s2 aprox. s2 grd A3 n 2 s3 3.16 * s3 A4 n 3 s4 2.15 * s4 A5 2 n s5 3.3 + s5 Façamos mais outro exemplo, consideremos agora o algoritmo de multiplicação de 2 matrizes, seja C(m, p) = A(m, n) x B(n, p) Algoritmo Inicio Para i =1 até m Para j = 1 até p 1. s? 0 Para k = 1 até n 2. s? s + A(i, k) * B(k, j) Fim para (k) 3. C(i, j)? s Fim para (j) Fim para (i)......... Supondo as matrizes quadradas com m = p = n teremos: A linha 1. tem como tempo de execução t1 e é executada n 2 vezes Departamento de Engª Informática do ISEP 12
A linha 2. tem como tempo de execução t2 e é executada n 3 vezes A linha 3. tem como tempo de execução t3 e é executada n 2 vezes Logo o tempo de execução será: T(n) = (t1+t3) * n 2 + t2 * n 3 ou seja T(n) = K * n 2 + K * n 3? O (n 3 ) Nesta análise podemos atender às seguintes regras:. O tempo de execução de um ciclo é no máximo o tempo das instruções dentro do ciclo, vezes o número de iterações.. No caso de ciclos encaixados, o tempo de execução total de uma instrução dentro de um grupo de ciclos encaixados é o tempo de execução da instrução multiplicada pelo produto do tamanho de todos os ciclos.. Instruções consecutivas adicionam-se.. No caso de uma instrução se o tempo de execução nunca é maior do que o tempo de execução SE (cond) Então C1 Senão C2 do teste mais o maior dos tempos de execução de C1 e de C2.. Se houver chamadas a funções, obviamente que estas terão que ser analisadas primeiro.. No caso de recursividade, se for simples e for possível transformar num ciclo será uma análise trivial. No caso em que a recursividade é difícil de converter em ciclo, há que resolver a relação de recorrência com atenção( exemplo nas aulas práticas). LIMITAÇÔES DA ANÁLISE DA COMPLEXIDADE TEMPORAL Não podemos esquecer que esta análise da complexidade de algoritmos sofre de limitações, nomeadamente quando se determina a complexidade ignorando-se as constantes. Assim, um algoritmo cujo índice de crescimento seja 75000 g(n) é ainda considerado de complexidade g(n). Atendamos também ao seguinte é que na análise temporal só se mede o tempo, podendo haver eventualmente outras propriedades mais importantes, por exemplo, se a compreensão do algoritmo for mais importante então o tempo de complexidade deverá ficar para trás. De modo idêntico, em algoritmos numéricos a precisão é muitas vezes mais importante do que a complexidade temporal e podemos dar preferência a um algoritmo mais lento, mas mais preciso em vez de um mais rápido mas menos preciso. Por vezes há que também balancear entre o tempo de execução de um algoritmo e o espaço por ele ocupado. Assim, algoritmos que precisem de correr em processadores embebidos, tais como pequenos computadores que controlam automóveis ou telemóveis, o espaço é mais precioso do Departamento de Engª Informática do ISEP 13
que o tempo e preferem-se algoritmos mais lentos que ocupem menos espaço do que mais rápidos mas que exijam mais espaço. Do mesmo modo, podemos preferir entre dois algoritmos que apresentem em caso médio uma complexidade temporal diferente, aquele que possui a mais desfavorável desde que, por exemplo, as aplicações visem preponderância para execução do algoritmo nas condições do caso mais favorável e nesse caso a complexidade deste seja melhor, embora o não seja no caso médio. Em conclusão, temos que ser cuidadosos quando escolhemos um algoritmo em detrimento de outro. Como vimos a análise de algoritmos tem limitações, contudo, um analista prevenido quanto a estas limitações, tem na análise da complexidade uma ferramenta importante e extremamente útil sempre que precise de comparar algoritmos e recomendar um em vez de outro. COMPLEXIDADE ESPACIAL Aquilo que dissemos anteriormente para o tempo de execução de um algoritmo repetimo-lo mas agora considerando o espaço ocupado pelo algoritmo durante a respectiva execução. O espaço de complexidade de um programa é o total de memória que é necessário para correr completamente. Interessa conhecer a função que define o comportamento do algoritmo relacionando o espaço de memória ocupado com o número de dados que têm que ser manipulados. Intervêm neste espaço os seguintes componentes: Espaço de Instruções: espaço necessário para guardar a versão compilada das instruções do programa. Este espaço é constante para um dado programa referente a um dado compilador, com determinadas opções de compilação. Cada compilador para a construção de código máquina da mesma instrução, precisa de diferente espaço, cada um gera o seu código. Ainda o mesmo compilador pode gerar diferente código de acordo com o uso ou não de opções de compilação. O próprio computador também pode afectar o espaço de código, por exemplo se possui hardware com vírgula flutuante ou não. No primeiro caso as instruções com vírgula flutuante são traduzidas numa instrução máquina por operação. Caso contrário, ter-se-á que gerar código que simule a operação em vírgula flutuante. Não é este espaço que nos interessa analisar. Espaço de Dados: espaço onde estão definidas as variáveis globais e que varia de acordo com os dados que tem o nosso problema. Espaço de Stack: usado para guardar a informação necessária para resumir a execução de funções. Aí, de uma forma genérica podemos dizer que são guardados os endereços de retorno das funções, variáveis locais e parâmetros formais. Este espaço tal como o anterior varia com o tipo de algoritmo usado e portanto será também analisado para avaliação da complexidade espacial. Departamento de Engª Informática do ISEP 14
Estes dois espaços serão aqueles que nos debruçaremos para analisar o espaço de complexidade. Exemplo1: analisar quanto à complexidade espacial, o algoritmo da função que faz a pesquisa sequencial de um valor x, num vector a, com n elementos. int pesquisa_sequencial( a[], &x, n) Inicio //devolve a posição do array onde se encontra x, devolverá -1 se não existir Para(i=0; i<n && a[i]!=x;i++); Se(i==n) Então devolve -1; Senão devolve i; Fim Espaço total : endereço de retorno 2 bytes+ apontador a 2 + apontador para parâmetro actual de x 2 + valor do parâmetro formal n 2 + variável local i 2 10bytes Nota: o espaço para o array não foi alocado nesta função, logo não foi contabilizado. Assim esta função tem um espaço de complexidade constante, é de O (1). Exemplo2:Deduzir a ordem da complexidade temporal dos algoritmos que determinam o factorial de um valor n, algoritmo recursivo e iterativo. int factorialrecursivo( n) Inicio se(n<=1) então devolve 1; senão devolve n * factorialrecursivo( n-1); Fim A profundidade da recursividade é n e de cada vez que a funçaõ é invocada, o espaço de stack ocupado é dado por: endereço de retorno 2 bytes+ valor do parâmetro formal n 2 será n*4, ou seja O(n). No caso de função iterativa para o cálculo de factorial teremos: 4bytes isto isto é, o espaço total Departamento de Engª Informática do ISEP 15
int factorialiterativo ( int n) Inicio fact=1; enquanto(n>1) fact=n*fact; n--; Fenquanto devolve fact; Fim Assim, o espaço de stack será dado por: Espaço total : endereço de retorno 2 bytes+ valor do parâmetro formal n 2 + variável local fact 2 6 bytes, neste caso o espaço é constante, não depende do valor de n, logo a complexidade espacial é de O(1); Atendendo à complexidade espacial o algoritmo iterativo para cálculo de factoriais é mais favorável do que o recursivo. Departamento de Engª Informática do ISEP 16