Você está na página 1de 39

ESTUDO DA

COMPLEXIDADE DE
ALGORITMOS
Ariel Da Silva Dias
Sumário
INTRODUÇÃO������������������������������������������������� 3

CONTAGEM DE INSTRUÇÕES������������������������ 7
Contando as instruções������������������������������������������������������� 8

CONSUMO DE TEMPO ASSINTÓTICO��������� 19


Melhor caso, pior caso e caso médio�������������������������������� 21
Notação Big-O��������������������������������������������������������������������� 25
Notação Θ (Teta)����������������������������������������������������������������� 29
Notação Ω (Ômega)������������������������������������������������������������ 29

HABILIDADES MATEMÁTICAS
NECESSÁRIAS��������������������������������������������� 31

REVISÃO DOS ALGORITMOS DE


ORDENAÇÃO E MÉTODOS DE PESQUISA���� 34
Bubble Sort�������������������������������������������������������������������������� 34
Merge Sort��������������������������������������������������������������������������� 35
Quick Sort���������������������������������������������������������������������������� 35

CONSIDERAÇÕES FINAIS���������������������������� 36

REFERÊNCIAS BIBLIOGRÁFICAS &


CONSULTADAS�������������������������������������������� 37

2
INTRODUÇÃO
Todos os dias nos deparamos com muitos proble-
mas e encontramos uma ou mais soluções para
solucioná-los. Dentre essas possíveis soluções,
algumas podem ser mais eficientes do que outras,
entretanto, sempre buscamos pela solução mais
eficiente.

Por exemplo, ao ir de sua casa para seu escritório,


escola ou faculdade, pode haver “n” número de
caminhos, entretanto, você escolhe apenas um
caminho para ir ao seu destino, normalmente o
caminho mais curto.

Aplicamos a mesma ideia no caso dos problemas


computacionais ou resolução de problemas via
computador. Temos um problema computacio-
nal e podemos projetar várias soluções, ou seja,
algoritmos, e escolhemos o mais eficiente dentre
os algoritmos desenvolvidos. Um bom algoritmo
é aquele que pode ser passado para outra pessoa
seguir sem a necessidade de nenhuma explicação
extra. O mundo está cheio de algoritmos: receitas
culinárias, montagem de móveis, trocar o pneu de
um carro, escovar os dentes, comprar um produto
em um mercado, entre outros.

Quanto mais você pensa sobre algoritmos, mais


começa a perceber que tende a seguir muitos

3
conjuntos de instruções todos os dias. Alguns
exemplos são claramente sinalizados, enquanto
outros são mais subtendidos, por exemplo, manter
o limite de velocidade ao dirigir seu carro, subir
ou descer uma escada ou seguir um código de
conduta em uma empresa.

Às vezes, há mais de um algoritmo (ou mais de


uma maneira) possível para se resolver um pro-
blema. Pense no seguinte caso: Ana é residente
na cidade de São Paulo e Beatriz é residente na
cidade de Fortaleza, no estado de Ceará. Em um
dado dia, Ana resolveu ir passar férias na cada de
Beatriz. Como ela pode chegar a Fortaleza? Note,
temos um problema e os possíveis algoritmos para
resolver este problema são:

1. Ana pode se deslocar até o aeroporto de Gua-


rulhos, em São Paulo, e embarcar em um voo com
destino a Fortaleza. Do aeroporto, Ana pode pegar
um táxi até a casa de Beatriz;
2. Ana pode pegar seu carro e sair da cidade de São
Paulo rumo à Fortaleza, começando pela rodovia
Fernão Dias, cruzando os estados de Minas Gerais,
Bahia, Piauí e, por fim, chegar no Ceará;
3. Ana também pode ir pelo mesmo caminho do
algoritmo 2, porém utilizando sua motocicleta,
observando a paisagem até chegar em Fortaleza;

4
4. Por fim, Ana pode ir a pé pela rodovia dos Ban-
deirantes, no estado de São Paulo, até chegar na
casa de sua amiga;

Observe por este exemplo que existem quatro


modos possíveis para Ana chegar até a casa de
sua amiga, ou seja, quatro algoritmos para resolver
o mesmo problema. Cada um desses algoritmos
possui situações diferentes de deslocamento, bem
como tempos diferentes. No primeiro algoritmo
Ana irá de Avião e o tempo total de deslocamento
até a casa de Beatriz será de aproximadamente
quatro horas. Por outro lado, no quarto algoritmo
Ana irá a pé, economizará dinheiro de passagem,
porém levará 586 horas para chegar até a casa de
Beatriz – isso se Ana não dormir e não parar em
nenhum momento!!!

Este é um exemplo simples para demonstrar que


um mesmo problema pode ter muitas soluções
diferentes e que uma solução se difere da outra,
seja em tempo ou em custo. Nos algoritmos 3 e
4, por exemplo, Ana fará o mesmo caminho, po-
rém o desgaste em se deslocar com o carro será
menor que o desgaste de se deslocar com uma
motocicleta.

Dito tudo isso, torna-se necessário que o desen-


volvedor aprenda a comparar o desempenho de
diferentes algoritmos e escolher o melhor para

5
resolver um determinado problema. Ao analisar um
algoritmo, consideramos principalmente a com-
plexidade do tempo e a complexidade do espaço:

• A complexidade de tempo de um algoritmo quan-


tifica a quantidade de tempo que ele levará para ser
executado em função do comprimento da entrada.
• A complexidade do espaço de um algoritmo quan-
tifica a quantidade de espaço ou memória ocupada
por um algoritmo para ser executado em função do
comprimento da entrada.

A complexidade de tempo e espaço depende de


muitas coisas como hardware, sistema operacional,
processadores, entre outros. No entanto, durante o
estudo será considerado apenas o tempo de exe-
cução de um algoritmo, afinal, há um tempo atrás
a memória era um componente muito caro para
um computador, logo, havia a preocupação com a
complexidade de espaço, porém, com a evolução
da tecnologia, a memória ficou mais barata e hoje
é o menos preocupante.

6
CONTAGEM DE
INSTRUÇÕES
A complexidade de tempo de um programa é a
quantidade de tempo que o computador precisa
para concluir a execução de determinado progra-
ma. O tempo, T(p), tomado por um programa p, é
a soma do tempo de compilação e do tempo de
execução das instruções. O tempo de compilação
não depende das características da instância.
Além disso, podemos assumir que um programa
compilado será executado várias vezes sem re-
compilação. Logo, não consideraremos o tempo
de compilação, vamos nos preocupar apenas com
o tempo de execução do programa. O tempo de
execução é definido por tp (características da
instância).

O tempo de execução de um algoritmo é obtido


contando o número de instruções que ele realiza,
deste modo entrará em nossa contagem:
y Atribuição de valores a variáveis;
y Chamadas de métodos;
y Operações lógicas e aritméticas;
y Comparação de dois números;
y Acesso a elemento de um array;
y Seguir uma referência de objeto (acesso a
objeto);
y Retorno de um método.

7
Mas quanto tempo leva para uma instrução ser
executada? Depende, pois cada arquitetura com-
putacional possui um desempenho diferente. Logo,
uma instrução A em um processador X pode levar
0,01 nanossegundo para executar, enquanto a
mesma instrução A em um processador Y pode
levar 1 milissegundo.

Deste modo, neste estudo assumiremos que as


instruções possuem o mesmo custo, o que chama-
remos de 1UT ou 1 Unidade de Tempo. Por outro
lado, algumas instruções terão tempo 0, como as
instruções de seleção.

CONTANDO AS INSTRUÇÕES
Agora que o conceito de contagem de instruções
foi definido e que uma instrução leva 1UT para
ser executada, vamos verificar alguns exemplos
práticos de contagem de instruções. O primeiro
exemplo está presente na tabela 1: a linguagem é
parecida com C, porém a mesma análise vale para
qualquer outra linguagem.

Tabela 1: código com quatro instruções


1 a=2
2 b=4
3 total = a + b
4 printf(‘%d’, total)

Fonte: elaborado pelo autor.

8
No código da tabela 1 temos quatro instruções,
sendo que cada linha é uma instrução diferente.
Anteriormente foi definido que cada instrução leva
1UT. Logo, temos que T(p) = 1 + 1 + 1 + = 4. Ou
seja, este algoritmo leva 4UT para ser executado.

Vamos então para um segundo exemplo, neste


teremos uma estrutura condicional. Observe então
a tabela 2 com código em JavaScript – lembre-se,
não importa a linguagem, a análise está relacionada
na instrução!

Tabela 2: código com condicional (versão 1).


1 numero = read();
2 if (numero % 2 == 0)
3 console.log(“par”);
4 if (numero % 2 != 0)
5 console.log(“impar”);
6
7 console.log(numero);

Fonte: elaborado pelo autor.

Observe no código da tabela 2 que a variável núme-


ro recebe uma entrada na linha 1. Em seguida, na
linha 2, temos uma condicional if que verifica se o
resto da divisão do número por 2 é igual a zero, ou
seja, se o valor é par. Se for par, será executada a
instrução da linha 3. Já na linha 4, será verificado
se o número é ímpar, caso seja, será executada

9
a instrução da linha 5. Por fim, será executada a
instrução da linha 7.

Agora vamos analisar esse código e obter a quan-


tidade de instruções que serão executadas. A ta-
bela 3 a seguir trata da contagem das instruções.
Observe com atenção.

Tabela 3: contagem das instruções do código com condi-


cional (versão 1)
Quantidade de
Código Custo
execuções
numero = read(); c1 1
if (numero % 2 == 0) c2 1
console.log(“par”); c3 1
if (numero % 2 != 0) c4 1
console.log(“impar”); c5 0
0 0
console.log(numero); c6 1

Fonte: elaborado pelo autor.

Na tabela 3 temos a coluna com o código, uma


coluna chamada custo, com valores c1, c2, c3,...,
entre outros, e a coluna que marca a quantidade de
execuções da instrução. Observe que neste caso
específico consideramos que a análise está sendo
feita para quando o usuário digita um número par,
logo, a linha 5 não será executada.

Temos, então, o seguinte resultado da soma de


tempo:

10
T(p) = 1 * c1 * c2 + 1 * c3 + 1 * c4 + 0 * c5 + 0 + 1 *
c6
Conforme já foi exposto anteriormente, vamos
assumir que o custo computacional é 1 para todos.
Logo temos:
T(p)=1 * 1 + 1 * 1 + 1 * 1 + 1 * 1 + 0 * 1 + 0 + 1 *
1 = 5 UT
Logo, nosso algoritmo leva 5 UT para ser executado.
Apesar de tratar-se de um bom tempo, é possível
melhorar a execução desse algoritmo? Ou seja,
é possível diminuir o número de instruções em
execução? Sim! Assim como no exemplo da Ana
que estava indo para Fortaleza encontrar a amiga,
aqui também temos mais de um caminho. Observe
então o código da tabela 4.
Tabela 4: código com condicional (versão 2)
1 numero = read();
2 if (numero % 2 == 0)
3 console.log(“par”);
4 else
5 console.log(“impar”);
6
7 console.log(numero);

Fonte: elaborado pelo autor.

Observe no código da tabela 4 que adicionamos


um else na linha 4 e removemos a condicional if.
Isso foi feito pois um número é par ou ímpar, não

11
existe uma terceira opção. Logo, o else fará com
que reduza o número de instruções. Observe agora
como fica a tabela de contagem de instruções a
seguir.

Tabela 5: contagem das instruções do código com condi-


cional (versão 2)
Código Custo Quantidade de
execuções
numero = read(); c1 1
if (numero % 2 == 0) c2 1
console.log(“par”); c3 1
else 0 0
console.log(“impar”); c4 0
0 0
console.log(numero); c5 1

Fonte: elaborado pelo autor.

Analisando a tabela 5, temos então o seguinte


resultado da soma de tempo:
T(p)=1*c1+1*c2+1*c3+0+0*c4+1*c5
Conforme já foi exposto anteriormente, vamos
assumir que o custo computacional é 1 para todos.
Logo temos:
T(p)=1*1+1*1+1*1+0+0+1=4 UT
Logo, nosso novo algoritmo leva 4 UT para ser
executado, uma melhora de 1UT, o que pode ser
muito significativo em algoritmos maiores. Neste
caso, o que ocorreria se o número fosse ímpar? O

12
resultado seria o mesmo, afinal, o else não é uma
instrução, portanto, se o número for ímpar, a exe-
cução sairá da linha 2 para a linha 5 diretamente.

Até aqui você viu a contagem de instruções em dois


cenários distintos: em um algoritmo sequencial sem
condicional e em um algoritmo com condicional.
Agora vamos contar as instruções de um laço de
repetição for. Observe o código da tabela 6 a seguir.

Tabela 6: código com estrutura de repetição (versão 1)


1 for(j=0; j<0; j++)
2 printf(‘a’);

Fonte: elaborado pelo autor.

Vamos analisar com cuidado o código da tabela 6,


afinal, temos muitas instruções na linha 1:
y Assim que o laço for iniciar, a primeira instrução
será a atribuição j = 0;
y Em seguida, o laço for executará a instrução j < 0;
y Como essa instrução resultará em falso, então
o algoritmo será finalizado.

Portanto, para o algoritmo da tabela 6 tivemos


duas instruções, ou seja, T(p) = 2UT. Deste modo
é possível concluir que, mesmo que o laço for não
tenha seu escopo executado, ele executa ao menos
duas instruções, sendo uma de atribuição e outra
de comparação.

13
Agora o código da tabela 6 foi modificado e ele
pode ser observado na tabela 7 a seguir.

Tabela 7: código com estrutura de repetição (versão 2).


1 for(j=0; j<10; j++)
2 printf(‘a’);

Fonte: elaborado pelo autor.

Vamos analisar com cuidado o código da tabela 7,


afinal, temos muitas instruções na linha 1:
y Assim que o laço for iniciar, a primeira instrução
será a atribuição j = 0;
y Em seguida, o laço for executará a instrução j < 0;
y Como essa instrução resultará em verdadeiro
(zero é menor que dez), teremos a seguinte se-
quência de instruções sendo executada 10 vezes
(ou seja, enquanto a variável j for menor que dez):

– instrução da linha 2 será executada;


– instrução j ++ será executada;
– instrução j < 10 será executada.

y Quando a instrução j < 10 resultar em false, o


algoritmo será finalizado.

Observe que nesse algoritmo teremos 32 instruções


sendo executadas, que serão divididas assim:
y duas instruções quando entrar no laço for;
y a instrução printf(‘a’) será executada 10 vezes;
y a instrução j ++ será executada 10 vezes;

14
y a instrução j < 10 será executada 10 vezes.

No código da tabela 8 a seguir, foi adicionada uma


variável chamada n, vamos considerar que há um
valor 10 atribuído a ela, definindo assim o máximo
que um laço for será executado.

Tabela 8: código com estrutura de repetição (versão 3)


1 for(j=0; j<n; j++)
2 printf(‘a’);

Fonte: elaborado pelo autor.

O que modifica do código da tabela 7 para o código


da tabela 8 é que substituímos a instrução j < 10
por j < n, em que n vale 10.

Vamos então contar as instruções deste código


da tabela 8:
y 2 instruções assim que o laço for começar na
linha 2, sendo j = 0 e j < n;
y 30 instruções, sendo 10 instruções para a linha
2, 10 instruções na linha 1 com j < n e 10 instruções
na linha 1 para j++.

Com isso, a complexidade será 32UT, ou seja, exa-


tamente o que já tínhamos anteriormente.

Observe que, quando a variável n valia 10, as


instruções printf(), j < n e j++ foram executadas
10 vezes. Mas, e se a variável n valer 20? Nesse
caso teremos:

15
y duas instruções assim que o laço for começar
na linha 2, sendo j = 0 e j < n (como já era antes);
y 60 instruções, sendo 20 instruções para a linha
2, 20 instruções na linha 1 com j < n e 20 instruções
na linha 1 para j++.

Com isso, a complexidade será 62UT. Mas, e se a


variável n valer 30? Bem, serão as duas instruções
iniciais do for somadas ao valor 90, totalizando
92UT. Logo, para esse algoritmo da tabela 8 temos
a seguinte equação:
T (n) = 3n + 2
Observe que o 3 desta equação se refere às ins-
truções de atribuição, comparação e o printf(). O
número 2, por sua vez, se refere à atribuição e à
comparação inicial.

Para finalizarmos essa análise, considere o códi-


go-fonte da tabela 9 a seguir, nele temos também
uma estrutura de repetição for, porém com uma
pequena modificação.

Tabela 9: código com estrutura de repetição (versão 3)


1 for(j=1; j<n; j++)
2 printf(‘a’);

Fonte: elaborado pelo autor.

Com o código da tabela 9, o laço for iniciará com


j=1 e não mais com zero, ou seja, partindo do prin-

16
cípio que n = 10, o nosso laço executará 9 vezes.
De igual modo, se n = 100, o nosso laço executará
99 vezes, sendo assim, não executaremos o laço
n vezes, mas sim, n-1 vezes.

Deste modo, teremos a seguinte contagem de


instruções:
y Ao iniciar o algoritmo teremos suas instruções,
que são j = 1 e j < n;
y A instrução da linha 2 será executada n - 1 vezes;
y A instrução j < n será executada n - 1 vezes;
y A instrução j++ será executada n - 1 vezes.

Logo, a seguinte equação define o algoritmo da


tabela 9:
T(n) = 3*(n - 1)+ 2 = 3n - 3 + 2 = 3n - 1
Podemos afirmar que o algoritmo da tabela 9
possui 3n - 1 instruções. Sendo assim, quando n
= 0, teremos -1 instrução, o que é verdade. Se o
valor de n for igual a zero, o laço for não executará.
O laço somente executará a linha 2 quando j for
menor do que n, o que ocorre com n = 2. Logo, se
jogarmos 2 na equação, temos:
T(2) = 3*2 - 1 = 6 - 1 = 5
De acordo com a equação, para n = 2, teremos
cinco instruções. Vamos verificar? Se conside-
rarmos então o valor de n = 2, teremos a seguinte
sequência de instruções:

17
y ao iniciar o laço for, teremos duas instruções,
sendo a atribuição e a comparação;
y a instrução da linha 2 executará apenas 1 vez,
pois o valor de j começou com 1;
y a instrução j ++ será executada apenas 1 vez;
y a instrução j < n será executada apenas 1 vez.

Logo, serão cinco instruções conforme já foi apon-


tado pela equação anterior.

18
CONSUMO DE TEMPO
ASSINTÓTICO
Antes de abordar o consumo de tempo assintótico,
cabe relembrar que a complexidade de tempo é o
número de operações que um algoritmo executa
para completar sua tarefa em relação ao tamanho
da entrada (considerando que cada operação
leva o mesmo tempo), por isso o algoritmo que
executa a tarefa no menor número de operações
é considerado o mais eficiente.

O tempo gasto por um algoritmo depende da velo-


cidade de computação do sistema que você está
usando, mas ignoremos esses fatores externos
e nos preocupemos apenas com o número de
vezes que uma determinada instrução está sendo
executada em relação ao tamanho da entrada.
Digamos que o tempo gasto para executar uma
instrução é de um segundo. A partir disso, qual é
o tempo necessário para executar n instruções?
Levará n segundos.

Algo muito importante que deve ser notado aqui


é que estamos encontrando o tempo gasto por
diferentes algoritmos para a mesma entrada, por-
que se alterarmos a entrada, o algoritmo eficiente
pode levar mais tempo em comparação ao menos

19
eficiente, porque o tamanho da entrada é diferente
para ambos os algoritmos.

Observe então que não podemos julgar um algoritmo


calculando o tempo gasto durante sua execução
em um determinado sistema, logo, precisamos
de alguma notação padrão para analisá-lo. Para
isso, usamos a notação assintótica, que serve para
analisar qualquer algoritmo e, com base nisso,
encontramos o algoritmo mais eficiente.

Em notação assintótica não consideramos a


configuração do sistema, mas sim a ordem de
crescimento da entrada. Tentamos descobrir
como o tempo ou o espaço ocupado pelo algorit-
mo aumenta/diminui após aumentar/diminuir o
tamanho da entrada.

Existem três principais notações assintóticas que


são usadas para representar a complexidade de
tempo de um algoritmo. São elas:
y Notação Θ
y Notação O (lê-se: Big-O)
y Notação Ω

Antes de aprender sobre essas três notações


assintóticas, devemos aprender sobre o melhor
caso, o pior caso e o caso médio de um algoritmo.

20
MELHOR CASO, PIOR CASO E CASO
MÉDIO
Considere um vetor arr com n valores inteiros que
estão dispostos aleatoriamente e sem repetição
nesse vetor. Precisamos descobrir se um valor k
está presente nesse vetor arr (essas são as nossas
entradas). O algoritmo para resolver esse problema
deve retornar o número 1 caso encontre o valor k,
e o valor 0 caso não encontre o valor k (essa é a
nossa saída).

Observe que uma solução possível é a busca linear


ou sequencial, que é composta por uma estrutura
de repetição for que percorrerá todo o vetor verifi-
cando se o número k está ou não contido. A tabela
10 representa uma possível solução.

Tabela 10: código para encontrar o valor k no vetor


1 int pesquisarK(int arr[], int k) {
2 for(int i=0; i<n; i++)
3 if(arr[i] == k)
4 return 1;
5 return 0;
6 }

Fonte: elaborado pelo autor.

Observe neste código que temos a seguinte con-


tagem de instruções:

21
y A linha 1 não é contada como uma instrução;
y i = 0 será executada 1 vez;
y i < n será executada n+1 vezes;
y i ++ será executado n vezes;
y if(arr[i] == k) será executado n vezes, pois está
dentro do for;
y return 1 será executado 1 vez se o valor k es-
tiver no vetor;
y return 0 será executado 1 vez se o valor k es-
tiver no vetor.

Como apresentado anteriormente, cada declara-


ção no código leva um tempo constante, digamos
“C”, em que “C” é alguma constante. Portanto, se
uma instrução estiver demorando “C” tempo e
for executada “n” vezes, levará um tempo C*n UT
(unidade de tempo).

Agora, considere que o vetor arr[] é formado pelos


valores [1,2,3,4,5]. Então temos três casos:
y Caso 1: Se quisermos descobrir se o número
1 está presente na matriz ou não, a condição if da
linha 3 será executada apenas 1 vez e encontrará o
elemento 1 na primeira posição. Logo, a condição
if levará 1 UT;
y Caso 2: Se quisermos descobrir se o número
3 está presente na matriz ou não, a condição if
da linha 3 será executada 3 vezes e encontrará o

22
elemento 3 na terceira posição. Logo, a condição
if levará 3 UT;
y Caso 3: Se quisermos descobrir se o número
6 está presente na matriz ou não, a condição if da
linha 3 será executada 5 vezes e não encontrará
o elemento e, assim, retornará o valor 0. Logo, a
condição if levará 5 UT;

Note que para o mesmo vetor de entrada, temos


tempos diferentes para valores diferentes de “k”,
o que definimos como:
y Melhor caso: Esse é o limite inferior no tempo
de execução de um algoritmo e foi apresentado
no Caso 1. Para isso, devemos conhecer o caso
que faz com que o número mínimo de operações
seja executado. No exemplo apresentado, nosso
vetor era [1,2,3,4,5] e estamos descobrindo se “1”
está presente no vetor ou não. Então aqui, após
apenas uma comparação, você notará que seu
elemento está presente no vetor. Então, este é o
melhor caso do seu algoritmo.
y Caso médio: Calculamos o tempo de execução
para todas as entradas possíveis, somamos todos
os valores calculados e dividimos a soma pelo
número total de entradas. Devemos conhecer (ou
prever) a distribuição dos casos.
y Pior caso: Esse é o limite superior no tempo
de execução de um algoritmo. Devemos conhecer
o caso que faz com que o número máximo de

23
operações seja executado. Em nosso exemplo, no
caso 3, o pior caso pode ser se o array fornecido
for [1,2,3,4,5] e tentarmos descobrir se o elemento
“6” está presente no array ou não. Aqui, a condição
if do nosso loop será executada 5 vezes e então o
algoritmo retornará “0” como saída.

Nesse exemplo, se procurarmos o valor 1, teremos


o melhor caso, se procurarmos o valor 5 ou qual-
quer outro valor que não esteja no vetor, teremos
o pior caso. Por fim, se procurarmos os valores 2,
3 ou 4, teremos o caso médio.

FIQUE
SAIBAATENTO
FIQUE ATENTO
REFLITA
MAIS

O pior caso pode ocorrer de duas maneiras: o valor pro-


curado não está presente, como foi o caso do exemplo
apresentado ou o valor procurado está na última posição
do vetor. Isso ocorreria se estivéssemos procurando o
número 5, por exemplo.

Quando o tempo de execução depende do com-


primento da entrada, chamamos de ordem de
crescimento. No exemplo anterior com o algoritmo
de busca, podemos observar claramente que o
tempo de execução depende linearmente do com-
primento do vetor, ou seja, a ordem de crescimento
nos ajudará a calcular o tempo de execução com
facilidade. Iremos ignorar os termos de ordem

24
inferior, uma vez que os termos de ordem inferior
são relativamente insignificantes para grandes
entradas, além disso, usamos uma notação dife-
rente para descrever o comportamento limitante
de uma função.

NOTAÇÃO BIG-O
A notação Big-O (ou simplesmente O) define o
limite superior de qualquer algoritmo, ou seja, seu
algoritmo não pode demorar mais do que esse
tempo. Em outras palavras, podemos dizer que
a notação O denota o tempo máximo gasto por
um algoritmo ou a complexidade de tempo do
pior caso de um algoritmo. Assim, a notação O
é a notação mais usada para a complexidade de
tempo de um algoritmo. Logo, se uma função é
g(n), então a representação O de g(n) é mostrada
como O(g(n)) e a relação é mostrada como:
O(g(n)) = { f(n): existem constantes positivas c e n0
tal que 0≤f(n)≤g(n) para todo n≥0 }
A expressão pode ser lida como Big-O de g(n) é
definido como um conjunto de funções f(n) para
as quais existem algumas constantes c e n0 tais
que f(n) é maior ou igual a 0 e f(n) é menor ou igual
a c*g(n) para todo n maior ou igual a n0.

A notação Big-O é a notação mais usada para ex-


pressar a complexidade de tempo de um algoritmo

25
e você vai perceber isso a partir de um exemplo
a seguir, em que teremos dois algoritmos para
encontrar a soma dos n primeiros números.

FIQUE
SAIBAATENTO
FIQUE ATENTO
REFLITA
MAIS

Nos exemplos aqui utilizado estamos considerando


valores pequenos para n. Entretanto, em casos reais,
os valores para n serão muito grandes: imagine, por
exemplo, um sistema para análise de crédito de todos
os clientes de um banco! São milhares de clientes, ou
seja, o valor de n (número de clientes) em um banco de
dados pode ser superior a casa de seis dígitos. Desse
modo então, na análise assintótica, geralmente lidamos
com tamanho de entrada n muito grande!

Nesse exemplo, temos que encontrar a soma dos


primeiros n números. Por exemplo, se n = 4, nossa
saída deve ser 1 + 2 + 3 + 4 = 10. Se n = 5, então a
saída deve ser 1 + 2 + 3 + 4 + 5 = 15. Vamos tentar
duas soluções de algoritmo para resolver esse
problema e comparar esses códigos.

Tabela 11: solução O(1)


1 int soma(int n) {
2 return n * (n+1)/2;
3 }

Fonte: elaborado pelo autor.

26
No código da tabela 11, há apenas uma instrução
e sabemos que uma instrução leva um tempo
constante para sua execução. A ideia básica é: se a
instrução estiver demorando um tempo constante,
levará a mesma quantidade de tempo para todo o
tamanho de entrada e denotamos isso como O(1).

Em outras palavras, se fizermos soma(5000)


receberemos como resultado a soma dos 5000
primeiros números em um tempo constante, afi-
nal, temos apenas uma única instrução que será
executada apenas uma vez. Observe então que,
independentemente do valor de n, o tempo sempre
será o mesmo, não tem como ser pior do que esse
limite constante.

Agora observe com atenção o código da tabela 12,


que possui uma estrutura de repetição.

Tabela 12: solução O(n)


1 int soma(int n) {
2 int total = 0;
3 for(int i=1; i<=n; i++)
4 total = total + i;
5 return total;
6 }

Fonte: elaborado pelo autor.

Nessa solução da tabela 12, executaremos um loop


for de 1 a n e adicionaremos esses valores a uma

27
variável chamada “total”. Vamos obter a equação
desse algoritmo contando as instruções:
y Linha 2: apenas uma instrução, que chamare-
mos de c1;
y Linha 3: a atribuição de i=1 e a comparação
serão duas instruções iniciais e, por ser uma cons-
tante, chamaremos de c2;
y Linha 3: a comparação e o incremento serão
executados n vezes. Logo, na linha 3 teremos c2*n;
y Linha 4: será executada n vezes também;
y Linha 5: será executada apenas uma vez e, por
ser uma constante, chamaremos de c3.

Observe que temos três declarações de tempo cons-


tante, logo, podemos substituir esses três valores
constantes apenas pela c. Temos também duas
instruções executando n vezes. Logo, a equação
pode ser descrita da seguinte forma:
f(n)=c*n+c
Deste modo, a notação Big-O do código da tabela
12 será O(cn) + O(c), em que c é constante. Assim,
a complexidade de tempo total pode ser escrita
como O(n). Em outras palavras, o tempo de exe-
cução deste algoritmo nunca será pior que tempo,
independentemente do valor de entrada n.

Dentre os algoritmos apresentados, qual preferivel-


mente iremos usar quando estivermos encontrando
a soma dos primeiros “n” números? Preferimos a

28
solução O(1) porque o tempo gasto pelo algoritmo
será constante, independentemente do tamanho
da entrada.

NOTAÇÃO Θ (TETA)
A notação Θ é usada para encontrar o limite médio
de um algoritmo, ou seja, define um limite superior
e um limite inferior, e seu algoritmo ficará entre
esses níveis. Então, se uma função é g(n), então a
representação teta é mostrada como e a relação
é mostrada como Θ(g(n)) a expressão a seguir:

Θ(g(n)) = { f(n): existem constantes positivas c1,c2 e n0


tal que 0 ≤ c1g(n)≤ f(n)≤ c2g(n)para todo n ≥ n0 }
A expressão apresentada pode ser lida como
teta de g(n) é definida como o conjunto de todas
as funções f(n) para as quais existem algumas
constantes positivas c1, c2 e n0 tais que c1*g(n)
é menor ou igual a f(n), e f(n) é menor ou igual a
c2*g(n) para todo n que é maior ou igual a n0.

NOTAÇÃO Ω (ÔMEGA)
A notação Ω denota o limite inferior de um algo-
ritmo, ou seja, o tempo gasto pelo algoritmo não
pode ser inferior ao que foi determinado. Em outras
palavras, este é o tempo mais rápido em que o
algoritmo retornará um resultado, é o tempo gasto
pelo algoritmo quando fornecido com sua entrada

29
de melhor caso. Então, se uma função é g(n), então
a representação ômega é mostrada como Ω(g(n))
e a relação é mostrada como:
Ω(g(n)) = { f(n): existem constantes positivas c e n0
tal que 0 ≤ cg(n) ≤ f(n) para todo n ≥ n0 }
A expressão apresentada pode ser lida como ôme-
ga de g(n) é definida como o conjunto de todas
as funções f(n) para as quais existem algumas
constantes c e n0 tais que c*g(n) é menor ou igual
f(n) a , para todo n maior ou igual a n0.

30
HABILIDADES
MATEMÁTICAS
NECESSÁRIAS
Existem muitas habilidades que serão necessárias
para a análise dos algoritmos. Algumas delas
são triviais e serão relembradas durante o estudo
deste tema. Entretanto, uma das habilidades que
mais prende a atenção dos alunos é o estudo dos
logaritmos.

Em matemática, antes da descoberta do cálculo,


muitos estudiosos usavam logaritmos para trans-
formar problemas de multiplicação e divisão em
problemas de adição e subtração. Em logaritmos, a
potência é elevada a alguns números (geralmente,
número base) para obter algum outro número. É
uma função inversa da função exponencial. A Ma-
temática e a Computação lidam constantemente
com as grandes potências dos números, sendo os
logaritmos os mais importantes e úteis.

Da mesma forma, todas as funções exponenciais


podem ser reescritas na forma de funções logarít-
mica. Os logaritmos são realmente úteis para nos
permitir trabalhar com números muito grandes
enquanto manipulamos números de tamanho
muito mais gerenciável.

31
A função logarítmica é definida como:
para x>0,a>0 e a≠1,
𝑦𝑦 = log' x se ,somente se, x = a0

A base do logaritmo é a. Isso pode ser lido como


log base a de x. As duas bases mais comuns usa-
das em funções logarítmicas são a base 10 e a
base e. Sendo que a função logarítmica com base
10 é chamada de função logarítmica comum e é
denotada por log 10 ou simplesmente log.

Nos exemplos a seguir da tabela 13, no lado es-


querdo temos uma equação exponencial e do lado
direito temos sua forma logarítmica equivalente.

Tabela 13: equação exponencial para forma logarítmica


Equação exponencial Forma logarítmica
3 =9
2
2 = log3 9
2 =8
3
3 = log2 8
103 = 1000 3 = log10 1000
x16 = 64 16= logx 64
81 = x
4
4 = log81 x

Fonte: elaborado pelo autor.

As funções logarítmicas possuem algumas das


propriedades que permitem simplificar os logarit-
mos quando a entrada está na forma de produto,
quociente ou o valor elevado à potência. Algumas
das propriedades estão listadas abaixo.

32
Regra do produto
logb MN = logb M + logb N
A multiplicação de dois logaritmos de mesma
base é igual à soma desses logaritmos de base
igual. Exemplo:

log 30 + log 2 = log (30.2) = log60

Regra do quociente

M
log$ = log$ M − log$ N
N

A divisão de dois logaritmos de mesma base é


igual à subtração desses logaritmos de base igual.
Exemplo:

56
log$ 56 − log$ 7 = log$ = log$ 8 = 1
7

Entender o comportamento das funções logarít-


micas é fundamental para as análises de alguns
tipos de algoritmos, como é o caso dos algoritmos
de ordenação e busca binária.

33
REVISÃO DOS
ALGORITMOS DE
ORDENAÇÃO E MÉTODOS
DE PESQUISA
Os Algoritmos de Ordenação são métodos de
reorganização de um grande número de itens em
alguma ordem específica, como do maior para o
menor, ou vice-versa, ou mesmo em alguma ordem
alfabética.

Esses algoritmos pegam uma lista de entrada, a


processam (ou seja, executam algumas operações
nela) e produzem a lista ordenada.

Neste primeiro momento, apresentaremos os


principais algoritmos de ordenação e métodos
de pesquisa, não entrando (ainda) em detalhes
sobre sua complexidade, logo, trata-se de uma
breve revisão.

BUBBLE SORT
Bubble sort é um algoritmo de ordenação sim-
ples que percorre repetidamente a lista, compara
elementos adjacentes e os troca se estiverem na
ordem errada. Esse é o algoritmo mais simples e
ineficiente ao mesmo tempo. No entanto, é muito

34
necessário aprender sobre isso, pois representa
os fundamentos básicos da classificação.

MERGE SORT
O Merge sort é um dos algoritmos de ordenação
mais eficientes e funciona no princípio da Divisão
e Conquista. O merge sort divide repetidamente
uma lista em várias sublistas até que cada uma
delas consista em um único elemento, além disso,
ele mescla essas sublistas de uma maneira que
resulte em uma lista classificada.

QUICK SORT
O nome “Quick Sort” vem do fato de que ele é ca-
paz de ordenar uma lista de elementos de dados
significativos mais rapidamente (duas ou três vezes
mais rápido) do que qualquer um dos algoritmos
de ordenação. Trata-se de um dos algoritmos de
ordenação mais eficientes e baseia-se na divisão de
um array em outros menores e na troca com base
na comparação com o elemento ‘pivô’ selecionado.
Devido a isso, o quick sort também é chamada de
classificação “Partition Exchange”. Assim como a
ordenação por merge sort, o quick sort também
se enquadra nas categorias de abordagem de
divisão e conquista da metodologia de resolução
de problemas.

35
CONSIDERAÇÕES FINAIS
Neste e-book você pôde compreender o conceito de
algoritmos como uma ferramenta para resolução
de problemas. Conforme foi apresentado, existem
diversos algoritmos para resolver um mesmo pro-
blema, conforme ilustrado no exemplo de Ana que
deseja ir visitar sua amiga em Fortaleza, partindo
de São Paulo.

Você também conheceu os conceitos de melhor


caso, pior caso e caso médio, que são os possíveis
casos que um algoritmo pode se enquadrar. Por
exemplo, tendo um molho com 10 chaves, encontrar
“de primeira” a chave que abre a gaveta é o melhor
caso, por outro lado, encontrar na décima tentativa
a chave que abre uma gaveta ou não encontrar a
chave seriam os piores casos. Todas as outras
possibilidades se enquadram no caso médio.

Você também pôde compreender o que é notação


assintótica e o crescimento assintótico de um al-
goritmo, que pode ser realizado após a contagem
de instruções de um dado código.

Por fim, pôde relembrar os conceitos de logarit-


mos e também rever os principais algoritmos de
ordenação.

36
Referências Bibliográficas
& Consultadas
BORIN, V. P. Estrutura de dados. Curitiba:
Contentus, 2020. [Biblioteca Virtual].

CAMPOS FILHO, F. F. Algoritmos numéricos:


uma abordagem moderna de cálculo numérico.
3. ed. Rio de Janeiro: LTC, 2018. [Minha
Biblioteca].

CORMEN, T. H. Desmistificando algoritmos.


1. ed. Rio de Janeiro: LTC, 2014. [Minha
Biblioteca].

DROZDEK, A. Estrutura de dados e


algoritmos em C++. 4. ed. Cengage Learning,
2017. [Minha Biblioteca].

GOLDBARG, M. C.; GOLDBARG, E. G., LUNA,


H. P. L. Otimização combinatória e meta-
heurísticas: Algoritmos e Aplicações. 1. ed.
Rio de Janeiro: GEN / Elsevier, 2016. [Minha
Biblioteca].

PINTO, R. A.; PRESTES, L. P.; SERPA, M. S.;


COUTO, J. M. C.; BIANCO, C. M.; NUNES, P.
C. M. Estrutura de dados. Porto Alegre: Sagah,
2019. [Minha Biblioteca].
TOSCANI, L. V., VELOSO, P. A. S.
Complexidade de algoritmos. 3. ed. Porto
Alegre: Bookman, 2012. [Minha Biblioteca].

WAZLAWICK, R. S. Introdução a algoritmos


e programação com Python: uma abordagem
dirigida por testes; 1. ed. Rio de Janeiro:
Elsevier, 2017. [Minha Biblioteca].

Você também pode gostar