Determinando a complexidade para funções recursivas (notação Big O)

267

Eu tenho um curso de Ciência da Computação amanhã e preciso de ajuda para determinar a complexidade dessas funções recursivas. Sei como resolver casos simples, mas ainda estou tentando aprender como resolver esses casos mais difíceis. Esses foram apenas alguns dos problemas de exemplo que não consegui descobrir. Qualquer ajuda seria muito apreciada e ajudaria muito em meus estudos, obrigado!

int recursiveFun1(int n)
{
    if (n <= 0)
        return 1;
    else
        return 1 + recursiveFun1(n-1);
}

int recursiveFun2(int n)
{
    if (n <= 0)
        return 1;
    else
        return 1 + recursiveFun2(n-5);
}

int recursiveFun3(int n)
{
    if (n <= 0)
        return 1;
    else
        return 1 + recursiveFun3(n/5);
}

void recursiveFun4(int n, int m, int o)
{
    if (n <= 0)
    {
        printf("%d, %d\n",m, o);
    }
    else
    {
        recursiveFun4(n-1, m+1, o);
        recursiveFun4(n-1, m, o+1);
    }
}

int recursiveFun5(int n)
{
    for (i = 0; i < n; i += 2) {
        // do something
    }

    if (n <= 0)
        return 1;
    else
        return 1 + recursiveFun5(n-5);
}
Michael_19
fonte
4
Se você não deseja passar pela análise todas as vezes, existe uma técnica de caixa preta chamada método Master. Mas com a suposição de que todas as divisões recursivas de entradas são de tamanho igual em cada instância.
Vivek Krishna

Respostas:

345

A complexidade do tempo, na notação Big O, para cada função, está em ordem numérica:

  1. A primeira função está sendo chamada recursivamente n vezes antes de atingir o caso base O(n), o que é frequentemente chamado de linear .
  2. A segunda função é chamada n-5 para cada vez; portanto, deduzimos cinco de n antes de chamar a função, mas n-5 também é O(n). (Realmente chamado de ordem de n / 5 vezes. E, O (n / 5) = O (n)).
  3. Essa função é log (n) base 5, pois toda vez que dividimos por 5 antes de chamar a função, sua O(log(n))(base 5), geralmente chamada logarítmica e, na maioria das vezes, a análise de notação e complexidade Big O, usa a base 2.
  4. No quarto, é O(2^n), ou exponencial , uma vez que cada chamada de função chama-se duas vezes menos que tenha sido recursed n vezes.
  5. Quanto à última função, o loop for leva n / 2, pois estamos aumentando em 2, e a recursão leva n-5, e, como o loop for é chamado recursivamente, a complexidade do tempo é (n-5) * (n / 2) = (2n-10) * n = 2n ^ 2-10n, devido ao comportamento assintótico e às considerações de pior cenário, ou ao limite superior pelo qual o grande O está se esforçando, estamos interessados ​​apenas no maior termo O(n^2).

    Boa sorte em seus períodos intermediários;)

codificador
fonte
você está certo sobre o quinto, o n diminuirá para o loop for, mas para o quarto eu não acho que seja n ^ 2 por ser como uma árvore cada vez que você chamar a recursão duas vezes, então deve ser 2 ^ n mais esse foi o seu responda no comentário anterior.
Codificador
2
@MJGwater Deixe o tempo de execução do loop ser m. Quando o tempo de execução recursiva 1 leva m para executar o loop. Quando a recursiva é executada 2 vezes, o loop também é executado 2 vezes, por isso leva 2m ... e assim por diante. Então é '*', não '^'.
BJC
3
@ codificador A explicação para 5 parece estranha. Se incrementar em 2 resulta em n/2iterações do forloop, por que diminuir em 5 não resultaria em n/5chamadas recursivas? Isso ainda resultaria em, O(n^2)mas parece uma explicação mais intuitiva. Por que misturar subtração e divisão quando são essenciais para fazer a mesma coisa?
Jack
1
@ codificador para o # 4, se houvesse 3 chamadas recursivas na definição da função, teria uma complexidade de tempo de O (3 ^ n)? E para 5 chamadas recursivas, seria O (5 ^ n), correto?
rmutalik 10/04
1
@ Jack Sim, eu também estava pensando o mesmo. Deve ser n/5não n-5. E, finalmente, todo se resume a O(N^2).
Anuj
128

Para o caso em que n <= 0, T(n) = O(1). Portanto, a complexidade do tempo dependerá de quando n >= 0.

Vamos considerar o caso n >= 0na parte abaixo.

1

T(n) = a + T(n - 1)

onde a é alguma constante.

Por indução:

T(n) = n * a + T(0) = n * a + b = O(n)

onde a, b são constantes.

2)

T(n) = a + T(n - 5)

onde a é constante

Por indução:

T(n) = ceil(n / 5) * a + T(k) = ceil(n / 5) * a + b = O(n)

onde a, b são constantes e k <= 0

3)

T(n) = a + T(n / 5)

onde a é constante

Por indução:

T(n) = a * log5(n) + T(0) = a * log5(n) + b = O(log n)

onde a, b são constantes

4)

T(n) = a + 2 * T(n - 1)

onde a é constante

Por indução:

T(n) = a + 2a + 4a + ... + 2^(n-1) * a + T(0) * 2^n 
     = a * 2^n - a + b * 2^n
     = (a + b) * 2^n - a
     = O(2 ^ n)

onde a, b são constantes.

5)

T(n) = n / 2 + T(n - 5)

onde n é alguma constante

Reescreva n = 5q + ronde q e r são inteiros er = 0, 1, 2, 3, 4

T(5q + r) = (5q + r) / 2 + T(5 * (q - 1) + r)

Temos q = (n - r) / 5, e como r <5, podemos considerá-lo uma constante, entãoq = O(n)

Por indução:

T(n) = T(5q + r)
     = (5q + r) / 2 + (5 * (q - 1) + r) / 2 + ... + r / 2 +  T(r)
     = 5 / 2 * (q + (q - 1) + ... + 1) +  1 / 2 * (q + 1) * r + T(r)
     = 5 / 4 * (q + 1) * q + 1 / 2 * (q + 1) * r + T(r)
     = 5 / 4 * q^2 + 5 / 4 * q + 1 / 2 * q * r + 1 / 2 * r + T(r)

Como r <4, podemos encontrar b constante para que b >= T(r)

T(n) = T(5q + r)
     = 5 / 2 * q^2 + (5 / 4 + 1 / 2 * r) * q + 1 / 2 * r + b
     = 5 / 2 * O(n ^ 2) + (5 / 4 + 1 / 2 * r) * O(n) + 1 / 2 * r + b
     = O(n ^ 2)
nhahtdh
fonte
1
Recentemente, falhei em uma pergunta da entrevista (e estendi a entrevista) que tem a ver com a análise da complexidade de tempo e espaço de uma função recursiva de fibonacci. Esta resposta é épica e ajudou muito, eu adoro, gostaria de poder votar em você duas vezes. Sei que é antigo, mas você tem algo semelhante para calcular o espaço - talvez um link, qualquer coisa?
Dimitar Dimitrov
Para o No.4, mesmo que o resultado seja o mesmo, a indução não deve ser a seguinte? Qual é a raiz quadrada de 2 (2) = (2) = (2) = (2) = (1) = (1) = (1). A
soma de dois números naturais é igual a
27

Uma das melhores maneiras que encontro para aproximar a complexidade do algoritmo recursivo é desenhar a árvore da recursão. Depois de ter a árvore recursiva:

Complexity = length of tree from root node to leaf node * number of leaf nodes
  1. A primeira função terá comprimento ne número de nós de folha, 1portanto a complexidade serán*1 = n
  2. A segunda função terá o comprimento n/5e o número de nós de folhas novamente, 1assim será a complexidade n/5 * 1 = n/5. Deve ser aproximado paran

  3. Para a terceira função, como nestá sendo dividido por 5 em cada chamada recursiva, o comprimento da árvore recursiva será log(n)(base 5)e o número de nós das folhas novamente 1, para que a complexidade seja reduzida.log(n)(base 5) * 1 = log(n)(base 5)

  4. Para a quarta função, uma vez que cada nó terá dois nós filhos, o número de nós folha será igual (2^n)e o comprimento da árvore recursiva será na complexidade (2^n) * n. Mas, como né insignificante na frente (2^n), pode ser ignorado e só se pode dizer que é complexidade (2^n).

  5. Para a quinta função, existem dois elementos que introduzem a complexidade. Complexidade introduzida pela natureza recursiva da função e complexidade introduzida pelo forloop em cada função. Fazendo o cálculo acima, a complexidade introduzida pela natureza recursiva da função será ~ ne a complexidade será devido ao loop for n. Total complexidade será n*n.

Nota: Essa é uma maneira rápida e suja de calcular a complexidade (nada oficial!). Gostaria de ouvir comentários sobre isso. Obrigado.

Shubham
fonte
Excelente resposta! Eu tenho uma pergunta sobre a quarta função. Se tivesse três chamadas recursivas, a resposta seria (3 ^ n). Ou você ainda diria (2 ^ n)?
Ben Forsrup
@ Shubham: # 4 não parece certo para mim. Se o número de folhas for 2^n, a altura da árvore deve ser n, não log n. A altura seria apenas log nse nrepresentasse o número total de nós na árvore. Mas isso não acontece.
Julian A.
@ BenForsrup: será 3 ^ n porque cada nó terá três nós filhos. A melhor maneira de ter certeza disso é desenhar a árvore recursiva com valores fictícios.
Shubham
# 2 deve ser n-5 não n / 5
Fintasys
7

Podemos provar matematicamente o que estava faltando nas respostas acima.

Isso pode ajudar muito a entender como calcular qualquer método. Eu recomendo lê-lo de cima para baixo para entender completamente como fazê-lo:

  1. T(n) = T(n-1) + 1Isso significa que o tempo que leva para o método terminar é igual ao mesmo método, mas com n-1 que é T(n-1)e agora adicionamos + 1porque é o tempo que leva para que as operações gerais sejam concluídas (exceto T(n-1)). Agora, vamos encontrar T(n-1)as seguintes: T(n-1) = T(n-1-1) + 1. Parece que agora podemos formar uma função que pode nos dar algum tipo de repetição para que possamos entender completamente. Vamos colocar o lado direito T(n-1) = ...em vez de T(n-1)dentro do método T(n) = ...que nos dará: T(n) = T(n-1-1) + 1 + 1que é T(n) = T(n-2) + 2ou em outras palavras, precisamos encontrar o nosso faltando k: T(n) = T(n-k) + k. O próximo passo é tomar n-ke afirmar que, n-k = 1porque no final da recursão, será necessário exatamente O (1) quandon<=0. A partir desta equação simples, sabemos agora isso k = n - 1. Vamos colocar kno nosso método final: o T(n) = T(n-k) + kque nos dará: o T(n) = 1 + n - 1que é exatamente nou O(n).
  2. É o mesmo que 1. Você pode testar por si próprio e ver que consegue O(n).
  3. T(n) = T(n/5) + 1como antes, o tempo para o término desse método é igual ao tempo do mesmo método, mas com o n/5qual é limitado T(n/5). Vamos encontrar T(n/5)como em 1: T(n/5) = T(n/5/5) + 1qual é T(n/5) = T(n/5^2) + 1. Vamos lugar T(n/5)dentro T(n)para o cálculo final: T(n) = T(n/5^k) + k. Novamente, como antes, n/5^k = 1que é n = 5^kexatamente o mesmo que perguntar qual é o poder de 5, nos dará n, a resposta é log5n = k(logaritmo da base 5). Vamos colocar nossas descobertas em T(n) = T(n/5^k) + kcomo segue: T(n) = 1 + logno que éO(logn)
  4. T(n) = 2T(n-1) + 1o que temos aqui é basicamente o mesmo de antes, mas desta vez estamos invocando o método 2 vezes recursivamente, multiplicando-o por 2. Vamos descobrir T(n-1) = 2T(n-1-1) + 1qual é T(n-1) = 2T(n-2) + 1. Nosso próximo lugar como antes, vamos colocar nossa descoberta: o T(n) = 2(2T(n-2)) + 1 + 1que é T(n) = 2^2T(n-2) + 2isso que nos dá T(n) = 2^kT(n-k) + k. Vamos descobrir kreivindicando o n-k = 1que é k = n - 1. Vamos colocar da kseguinte forma: T(n) = 2^(n-1) + n - 1que é aproximadamenteO(2^n)
  5. T(n) = T(n-5) + n + 1É quase o mesmo que 4, mas agora adicionamos nporque temos um forloop. Vamos descobrir T(n-5) = T(n-5-5) + n + 1qual é T(n-5) = T(n - 2*5) + n + 1. Vamos colocá-lo: T(n) = T(n-2*5) + n + n + 1 + 1)qual é T(n) = T(n-2*5) + 2n + 2)e para o k: T(n) = T(n-k*5) + kn + k)novamente: o n-5k = 1que é n = 5k + 1isso aproximadamente n = k. Isso nos dará: o T(n) = T(0) + n^2 + nque é mais ou menos O(n^2).

Agora, recomendo a leitura do restante das respostas, que agora oferecem uma perspectiva melhor. Boa sorte em ganhar os grandes O's :)

OhadM
fonte
1

A chave aqui é visualizar a árvore de chamadas. Feito isso, a complexidade é:

nodes of the call tree * complexity of other code in the function

o último termo pode ser calculado da mesma maneira que fazemos para uma função iterativa normal.

Em vez disso, o total de nós de uma árvore completa é calculado como

                  C^L - 1
                  -------  , when C>1
               /   C - 1
              /
 # of nodes =
              \    
               \ 
                  L        , when C=1

Onde C é o número de filhos de cada nó e L é o número de níveis da árvore (raiz incluída).

É fácil visualizar a árvore. Comece na primeira chamada (nó raiz) e desenhe um número de filhos igual ao número de chamadas recursivas na função. Também é útil escrever o parâmetro passado para a sub-chamada como "valor do nó".

Então, nos exemplos acima:

  1. a árvore de chamadas aqui é C = 1, L = n + 1. A complexidade do restante da função é O (1). Portanto, a complexidade total é L * O (1) = (n + 1) * O (1) = O (n)
n     level 1
n-1   level 2
n-2   level 3
n-3   level 4
... ~ n levels -> L = n
  1. árvore de chamada aqui é C = 1, L = n / 5. A complexidade do restante da função é O (1). Portanto, a complexidade total é L * O (1) = (n / 5) * O (1) = O (n)
n
n-5
n-10
n-15
... ~ n/5 levels -> L = n/5
  1. A árvore de chamadas aqui é C = 1, L = log (n). A complexidade do restante da função é O (1). Portanto, a complexidade total é L * O (1) = log5 (n) * O (1) = O (log (n))
n
n/5
n/5^2
n/5^3
... ~ log5(n) levels -> L = log5(n)
  1. árvore de chamada aqui é C = 2, L = n. A complexidade do restante da função é O (1). Desta vez, usamos a fórmula completa para o número de nós na árvore de chamadas porque C> 1. Portanto, a complexidade total é (C ^ L-1) / (C-1) * O (1) = (2 ^ n - 1 ) * O (1) = O (2 ^ n) .
               n                   level 1
      n-1             n-1          level 2
  n-2     n-2     n-2     n-2      ...
n-3 n-3 n-3 n-3 n-3 n-3 n-3 n-3    ...     
              ...                ~ n levels -> L = n
  1. árvore de chamada aqui é C = 1, L = n / 5. A complexidade do restante da função é O (n). Portanto, a complexidade total é L * O (1) = (n / 5) * O (n) = O (n ^ 2)
n
n-5
n-10
n-15
... ~ n/5 levels -> L = n/5
higlu
fonte