Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
Tempo x Espaço
É possível utilizar o Big O para medirmos quanto de espaço que um determinado
algoritmo ocupa. Na Wikipedia existe inúmeros wikis sobre algoritmos famosos, e
grande parte deles apresenta a seguinte estrutura:
Exemplo de card do Wikipedia mostrando os tempos de um algoritmo
O Merge sort, um dos mais famosos algoritmos de ordenação, além de ter uma
performance média de O (n log n), em seu pior cenário ocupa O(n) de espaço.
Onde n corresponde ao tamanho da entrada do algoritmo. Logo, se você passar
um array de 10 posições para o Merge sort ordenar, ele ocupará outras 10 posições
ao fim do processo.
Em contrapartida, o Bubble sort, também famoso mas nada performático (O(n2)),
ocupa O(1) (não necessitando de novas posições na memória para fazer a
ordenação).
Se você estiver projetando a sua solução para um ambiente limitado, será necessário
levar em consideração o espaço, mas é muito comum nos tempos atuais sacrificarmos
memória em prol do tempo de execução.
1 swap(num1, num2):
2 temp_num = num1
3 num1 = num2
4 num2 = temp_num
Podemos, em teoria, contar cada atribuição de valor executada pelo algoritmo como
um “passo”. Teríamos complexidade = 3, e esse resultado nunca mudará, não
importa qual valor que passe de entrada. Logo, é possível dizer que a complexidade
desse algoritmo é constante, representada por O(1).
Pode parecer confuso não utilizar O(3), mas seguindo a definição matemática
apresentada anteriormente, quando eu assumo que meu algoritmo tem
complexidade O(1), estou dizendo que o seu limite assintótico superior é menor ou
igual a k * f(n). Se considerarmos o k como constante representando a quantidade
de atribuições do nosso algoritmo (3) e f(n) como o running time (1), temos
como upper bound o valor 3.
Em outras palavras: Se o tempo de execução do seu algoritmo é constante, a maneira
ideal de representá-lo é através de O(1).
Complexidade linear: O(n)
Quando a entrada do algoritmo é variável em tamanho, temos um comportamento
diferente:
1 soma(array):
2 total = 0
3 for num in array:
Geralmente quando temos algum loop, e ele está ligado ao input, dificilmente
chegamos a um algoritmo de complexidade constante. No caso acima, podemos
contar os passos da seguinte forma:
n = tamanho(array)
total = 0: 1 operação
atribuição de valor a num: n operações
total = total + num: n * 2 operações
Para cada elemento do array, executaremos uma soma (total + num) e uma
atribuição (total = <resultado>). Chegamos à conclusão que complexidade = 1 + (n
* 2). Mas como chegamos a O(n)?
Deixando a parte matemática de lado, quando trata-se de análise assintótica, estamos
mais interessados no que realmente interfere na performance do algoritmo. Ou seja,
os valores constantes (1 e 2) nessa análise são detalhes se comparados ao impacto
que n causa ao tempo de execução. Portanto, uma das maneiras de encarar a
mensuração do Big O é simplesmente ignorando as constantes e focando no que é
dinâmico, nos levando a complexidade = n e em consequência ao O(n).
Considerações finais
E como num passe de mágica, depois de certa intimidade com o Big O Notation, você
passa a assumir a complexidade de um algoritmo com uma breve “olhadela”. Ao ver
um loop assume que é n, ao ver loop dentro de loop, que é n2, e assim por diante…
Nos próximos posts vamos explorar algoritmos de diferentes complexidades, entrando
em detalhes para entender os seus tempos de execução e alternativas otimizadas.