Ordenação por Seleção

Hoje vou apresentar mais um algoritmo de ordenação de vetores. É a Ordenação por Seleção (ou Selection Sort). Sem mais papo e antes mesmo da explicação, vamos ao seu pseudocódigo:

para i \leftarrow{} 1 até tamanho-1, faça
minimo \leftarrow{} i
para j \leftarrow{} i+1 até tamanho, faça
  se vetor[j] < vetor[minimo], então
   minimo \leftarrow{} j
  fim-se
fim-para
temp \leftarrow{} vetor[i]
vetor[i] \leftarrow{} vetor[minimo]
vetor[minimo] \leftarrow{} temp
fim-para

tamanho = comprimento do vetor

Funcionamento

A idéia é sempre procurar o menor elemento do vetor e inseri-lo no início do vetor. Procuramos o menor valor do vetor e colocamos ele em vetor[1]. Procuramos o menor valor do vetor excluindo o já colocado e colocamos ele em vetor[2]. E assim vamos indo até termos todo o vetor ordenado.

Partindo sempre a partir do último elemento reordenado (a partir do i), o programa procura o menor elemento no vetor e o substitue pelo elemento i atual.

Exemplo de Funcionamento

O programa recebe o seguinte vetor.

v[1] v[2] v[3] v[4] v[5] v[6]
5 3 7 8 2 5

Aí ele começa com i=1i=1. Vou sempre marcar ii com a cor preta e minmin com a cor cinza.

v[1] v[2] v[3] v[4] v[5] v[6]
5 3 7 8 2 5

Ele marca o próprio índice i como a variável minimo, que é sempre o menor elemento do vetor. Então, ele faz um para de j=2j=2 até o comprimento do vetor, com o objetivo de descobrir qual o menor elemento.

j=2j=2v[j]=3<v[minimo]=v[1]=5v[j] = 3 < v[minimo] = v[1] = 5, portanto minimo=j=2minimo = j = 2.

v[1] v[2] v[3] v[4] v[5] v[6]
5 3 7 8 2 5

j=3j=3v[j]=3<v[minimo]=v[1]=5v[j] = 3 < v[minimo] = v[1] = 5, portanto minimo=j=2minimo = j = 2.

v[1] v[2] v[3] v[4] v[5] v[6]
5 3 7 8 2 5

j=3j=3v[j]=7>v[minimo]=v[2]=3v[j] = 7 > v[minimo] = v[2] = 3, portanto não mexemos em nada.

j=4j=4v[j]=8>v[minimo]=v[2]=3v[j] = 8 > v[minimo] = v[2] = 3, portanto não mexemos em nada.

j=5j=5v[j]=2<v[minimo]=v[2]=3v[j] = 2 < v[minimo] = v[2] = 3, portanto minimo=j=5minimo = j = 5.

v[1] v[2] v[3] v[4] v[5] v[6]
5 3 7 8 2 5

j=6j=6v[j]=2<v[minimo]=v[2]=3v[j] = 2 < v[minimo] = v[2] = 3, portanto minimo=j=5minimo = j = 5.

v[1] v[2] v[3] v[4] v[5] v[6]
5 3 7 8 2 5

j=6j=6v[j]=5>v[minimo]=v[5]=2v[j] = 5 > v[minimo] = v[5] = 2, portanto não mexemos em nada.

Agora substituímos o v[minimo] pelo v[i], formando com isto o novo vetor:

v[1] v[2] v[3] v[4] v[5] v[6]
2 3 7 8 5 5

E assim vamos fazendo com os outros elementos até que todo o vetor esteja ordenado.

Custo

Este algoritmo não tem um melhor/pior caso, porque todos os elementos são varridos, sempre. Medir seu custo é simples. Custo de linha por linha…

n = tamanho do vetor

  1. nn
  2. nn
  3. _j=1nn\sum\_{j=1}^{n} n
  4. _j=1n1\sum\_{j=1}^{n} 1
  5. _j=1n1\sum\_{j=1}^{n} 1
  6. 00
  7. 00
  8. n1n-1
  9. n1n-1
  10. n1n-1
  11. 00

Você pode estar se perguntando porque eu coloquei este custo para a linha 5. Afinal, a linha 5 diria que este programa tem um melhor/pior caso, porque ela não seria executada se o se retornar falso. Mas o caso é que ela é desprezível. Uma soma como estas para o custo geral do nosso algoritmo não vai influenciar em nada. Quer ver? Vamos somar os custos com esta linha valendo 00 (como se nenhum se entrasse) e depois com ela valendo _j=1n\sum\_{j=1}^{n}.

Primeiro cálculo

T(n)=n+(n1)+(_j=1nn)+(j=1n1)×2+0×3+(n1)×3+0T(n) = n + (n-1) + (\sum\_{j=1}^n n) + \\ (\sum\\_{j=1}^n 1) \times 2 + 0 \times 3 + (n-1) \times 3 + 0 T(n)=n2+6n3=Θ(n2)T(n) = n^{2} + 6n - 3 = \Theta{}(n^2)

Segundo cálculo

T(n)=n+(n1)+(_j=1nn)+(j=1n1)×3+0×2+(n1)×3+0T(n) = n + (n-1) + (\sum\_{j=1}^n n) + \\ (\sum\\_{j=1}^n 1) \times 3 + 0 \times 2 + (n-1) \times 3 + 0 T(n)=1,5n2+6,5n3=Θ(n2)T(n) = 1,5 n^{2} + 6,5 n - 3 = \Theta{}(n^2)

Conclusão

Como vocês puderam ver, não faz diferença alguma o n2+n2\frac{n^{2} + n}{2} que aquela somatória nos proporciona. Já que todo o cálculo de algoritmos é baseado apenas no maior expoente de n e desprezamos todas as constantes (inclusive as que multiplicam o n de maior expoente, muitos passos são desprezíveis.

Ordenação por Inserção

Também conhecida como Insertion Sort, a Ordenação por Inserção consiste em inserir um elemento nn num vetor já ordenado de n1n-1 elementos. Neste artigo, apresento-lhes este simples algoritmo para ordenação de vetores.

O pseudocódigo da ordenação por inserção é o seguinte:

para j \leftarrow{} 2 até comprimento do vetor, faça
elemento \leftarrow{} vetor[j]
i \leftarrow{} j - 1
enquanto i > 0 e vetor[i] > elemento, faça
  vetor[i + 1] \leftarrow{} vetor[i]
  i \leftarrow{} i - 1
fim-enquanto
vetor[i + 1] \leftarrow{} elemento
fim-para

Para explicar eu vou fazer uma coisa que sempre faço para corrigir meus algoritmos, fingir que sou o programa, executando cada passo manualmente (claro que geralmente faço mais rápido, porque não preciso escrever tudo que faço). Vamos iniciar com o seguinte vetor v.

v[1] v[2] v[3] v[4] v[5] v[6]
5 3 7 8 2 5

Aí o código me manda começar com j=2j=2 e iterar até o comprimento do vetor (6). A primeira ordem que ele me dá é para armazenar o elemento a[j]a[j] (a[2]a[2]) na variável elemento. Para facilitar toda a explicação eu vou sempre pintar de cinza o v[j]v[j] onde eu estou (no caso, o segundo elemento do vetor, 3) e de preto o vetor ainda não ordenado (elementos j\geq{}j).

v[1] v[2] v[3] v[4] v[5] v[6]
5 3 7 8 2 5

Então ele me diz que ij1i \leftarrow{} j-1. Portanto, i=1i=1. E agora ele me faz um enquanto (que poderia ser substituído por para) onde meu i deverá ir diminuindo. Vamos entrar no loop…

Bom, meu i=1i = 1 é maior que 0. v[1]=5v[1]=5 é maior que o elemento=3elemento=3? Sim, então vamos entrar no corpo do enquanto… Aqui ele me manda fazer um vetor[i+1]=vetor[i]vetor[i+1] = vetor[i], que nesse caso é fazer um v[2]=v[1]=5v[2]=v[1]=5.

v[1] v[2] v[3] v[4] v[5] v[6]
5 5 7 8 2 5

E agora subtrai de i um valor. Portanto, i=0i=0. Ele retorna ao enquanto, mas agora não satisfazemos a condição i>0i>0, por isso saímos do enquanto. Então ele pede para vetor[i+1]=elementovetor[i+1]=elemento (v[1]=elementov[1]=elemento). Portanto, o vetor fica assim:

v[1] v[2] v[3] v[4] v[5] v[6]
3 5 7 8 2 5

E incrementamos o j, agora j=3j=3.

v[1] v[2] v[3] v[4] v[5] v[6]
3 5 7 8 2 5

elemento=7elemento = 7

i=31=2i = 3-1 = 2

i>0i > 0… E 5>75 > 7? Não! Portanto, não entramos no enquanto.

v[3]=elementov[3] = elemento (nenhuma mudança)

E lá vamos para j=4j=4 e continuando até que vamos ter o vetor ordenado:

v[1] v[2] v[3] v[4] v[5] v[6]
2 3 5 5 7 8

Qual a lógica?

Como eu já disse na introdução, mas lá sem grandes explicações, a Ordenação por Inserção divide o vetor em dois. O primeiro (de elementos <j< j) contém todos seus valores ordenados e o segundo (de elementos j\geq{} j) contém os valores que devem ser inseridos no primeiro vetor já ordenado (por isso ele se chama Algoritmo de Inserção). A chave do algoritmo é o enquanto que acontece para ir deslocando todos os elementos para seu índice +1+1) contém todos seus valores ordenados e o segundo (de elementos j\geq{}j) contém os valores que devem ser inseridos no primeiro vetor já ordenado (por isso ele se chama Algoritmo de Inserção).

A variável elemento só serve para não perdermos o valor de v[j]v[j] (porque depois fazemos v[i+1]=v[i]v[i+1]=v[i] quando i=j1i=j-1)

Acredito que não tenham restado dúvidas. Dê mais uma olhada no algoritmo e tente implementar. Se tiver dificulade, coloque mensagens de debug estratégicas para entender o algoritmo. (ex.: no início do para coloque para imprimir o valor de j e no início de cada enquanto coloque para imprimir os valores elemento, i e v[i])

Custo

Você deve ter percebido que este algoritmo não tem um custo fixo. Se todo o vetor estiver ordenado, ele nunca precisará iterar o ii e portanto será executado bem mais rápido do que se o vetor estiver inteiro em ordem decrescente (quando ele sempre precisará iterar ii até o fim - 0). Então, neste artigo, gostaria-lhes de apresentar a análise de algoritmos baseada em casos. Para este programa, dizemos que:

  • Melhor caso é quando todos os elementos já estão ordenados. Custo: Θ(n)\Theta{}(n)
  • Pior caso é quando os elementos estão em ordem decrescente. Custo: Θ(n2)\Theta{}(n^{2})

Em alguns programas o caso médio é importante também, mas não é o caso da ordenação por inserção. Vemos que há uma diferença bem grande entre o custo dos dois casos. Por isso, precisamos conhecer onde que nosso algoritmo será implementado e quais as chances de ele ser o melhor ou pior caso. Em geral, o pior caso é o mais comum… Por isso, diremos que o custo deste algoritmo é Θ(n2)\Theta{}(n^{2}).

Recursão

A recursão é uma das técnicas mais simples e úteis que existem para usarmos em nossos algoritmos. Consiste em uma função (denominada recursiva) chamar a si mesmo, até que o retorno seja trivial. Resolvi abordá-la aqui porque alguns algoritmos que estudaremos mais para frente usam funções recursivas.

Gostaria de, antes de falar sobre o assunto, contar um pouco da minha história com recursão, porque foi meu início no mundo dos algoritmos.

Junho de 2004, tinha eu 13 anos e fui premiado com medalha de ouro na modalidade iniciação da Olimpíada de Informática. Fui convidado para o curso de introdução a programação na UNICAMP e, extremamente geek como eu era (e ainda sou), falei para os professores que já programava em C e então eles sugeriram que eu fosse para o curso de programação avançada.

No entanto, eu ainda achava muito complicado os algoritmos da modalidade de programação da OBI, por isso pedi pra ficar num “meio-termo”. Eles toparam numa boa e com isso passei a ter aulas com um monitor excelente (aluno da UNICAMP) chamado Ribamar. Esse cara foi extremamente importante para minha iniciação na programação de verdade.

Na primeira tarde que tive aula com ele, ele perguntou se eu sabia o que era recursão. Respondi que não e, a partir daquele dia e depois de ele me ensinar também de grafos e programação dinâmica, além de me apresentar o Slackware, me tornei um amante de algoritmos. A recursão, portanto, mesmo sendo algo simples, é um assunto especial pra mim porque representa a mudança de “nível”.

Então, mãos à obra!


Em matemática, o número fatorial de nn é igual a: n×n1×n2×2×1n \times{} n-1 \times{} n-2 \times{} \ldots{} 2 \times{} 1.

Logo, por exemplo, 5!5! (cinco fatorial) seria igual a: 5×4×3×2×1=1205 \times{} 4 \times{} 3 \times{} 2 \times{} 1 = 120.


Um exemplo bom e simples de recursão é um algoritmo para determinar números fatoriais:

função fatorial (n)
se n=1n = 1, então
  retorna 11
senão
  retorna n×fatorial(n1)n \times{} fatorial(n-1)
fim-se
fim-função

Domínio de nossa função: nN/n1n \in{} \mathbf{N} / n \geq{} 1.

Qual o custo desse algoritmo?

Vamos abrir um grande parênteses aqui até a próxima linha horizontal para descobrir qual o custo do nosso algoritmo antes de continuar com a conversa sobre recursão e relembrar/reforçar o post sobre Análise de Algoritmos. Vou colocar o número de vezes que cada instrução é executada, usando o esquema que será o padrão para as próximas vezes que veremos custos:

Número da linha: Número de vezes que é executada..

  1. nn
  2. n1n-1
  3. 11
  4. n1n-1
  5. n1n-1

T(n)=(n)+(n1)+(1)+(n1)+(n1)=4n2T(n) = (n) + (n-1) + (1) + (n-1) + (n-1) = 4n - 2

Uma função linear, o tipo de algoritmo mais simples que podemos encontrar, com excessão dos que são uma função constante. Mas o parênteses na verdade não serviu só pra isso. Eu queria aproveitar pra escovar uns bits de nosso código. Você percebeu que o primeiro condicional é executado n1n-1 vezes, mas só entramos nele uma vez? Então vamos inverter nosso condicional.

função fatorial (n)
se n2n \geq{} 2, então
  retorna n×fatorial(n1)n \times{} fatorial(n-1)
senão
  retorna 11
fim-se
fim-função

Novo custo

  1. nn
  2. n1n-1
  3. n1n-1
  4. 11
  5. 11

T(n)=(n)+(n1)+(n1)+(1)+(1)=3nT(n) = (n) + (n-1) + (n-1) + (1) + (1) = 3n

Claro que continua uma função linear, não houve nenhuma grande mudança. Os dois continuam com a mesma ordem de crescimento e tal… 4n+24n+2 comparado com 3n3n é uma diferença pequena, mas essa solução ficou bem mais elegante. ;) Poxa, diminuímos o custo do algoritmo em 14\frac{1}{4}! Hehehe…

Agora, antes de continuar, só vamos definir a notação assintótica desse nosso novo custo!

A fórmula do Θ\Theta{} é: 0c_1g(n)f(n)c_2g(n)0 \leq{} c\_{1} g(n) \leq{} f(n) \leq{} c\_{2} g(n)

Substituindo pela nossa função, temos: c_1n3nc_2nc\_{1}n \leq{} 3n \leq{} c\_{2}n. É trivial, que podemos escolher para as duas constantes c_1=c_2=3c\_{1}=c\_{2}=3 e para n_0=0n\_{0}=0. Com isso pretendi mostrar-lhes uma conclusão óbvia que no outro artigo não tinha mostrado para não complicar muito: uma função reta (linear) pertence sempre a notação Θ(n)\Theta{}(n) e uma função quadrática pertence sempre a notação Θ(n2)\Theta{}(n^{2}) (ora, façam um gráfico das funções e vejam se isso não é óbvio!). Mas vamos aprendendo mais sobre análise de algoritmos com o tempo…


Bom… Continuando com a recursão… Nossa função cria um loop consigo mesma, ao invés de usar um para (for) ou enquanto (while). Ela se repete diminuindo um de nn a cada iteração, até que chegue ao valor mínimo 11 aonde a resposta é trivial: 11.

O que é necessário para criarmos uma recursão? Apenas um ponto de parada! Temos que tomar cuidado para não criarmos loops infinitos cuidando sempre com cada entrada que o usuário pode colocar. Nesse caso, eu determinei que o domínio da função é nN/n1n \in{} \mathbf{N} / n \geq{} 1. Se o cara colocasse n=0n=0, minha função iria diminuindo infinitamente… e nunca iria retornar nada!

Para fazer a recursão portanto, precisamos analisar o domínio de nossa função e mais: precisamos conhecer um valor (que vai ser o limite; no caso do fatorial, o valor que sabíamos é que 1!=11! = 1).

Acredito que vocês tenham achado tudo simples e que não tenham problema com isso. Funções recursivas vão ser extremamente úteis para nós nos próximos artigos. Vou finalizar mostrando-lhes alguns casos básicos de algoritmos em que podemos usar a recursão:

Números de Fibonacci

função fibonacci (n)
se n3n \geq{} 3, então
  retorna fibonacci(n1)+fibonacci(n2)fibonacci(n-1) + fibonacci(n-2)
senão
  retorna 11
fim-se
fim-função

Domínio de fibonacci(n): nN/n1n \in{} N / n \geq{} 1

Depois descobriremos como calcular os números de Fibonacci mais rápido, mas por enquanto nosso objetivo é a recursão!

Substituir um loop

Vamos supor que você quer imprimir os números de n a 1 e esqueceu a sintaxe do para… :D

função imprimeate_ (n)
imprima nn
se n>1n>1, então
  imprime_ate(n-1)
fim-se
fim-função

Domínio de imprime_ate(n): nN/n1n \in{} N / n \geq{} 1

Todo loop pode ser uma recursão e tem alguns que ficam bem mais fáceis se forem! Nesse caso, é claro que seria mais simples usarmos um para!

Outros exemplos, para concluir

  • Ordenação por Intercalação (Merge Sort)
  • Busca em Profundidade (Depth-First Search) em Grafos
  • Union/Find
  • … entre vários outros casos!

Análise de Algoritmos

Analisar um algoritmo é prever o que o algoritmo irá precisar. Às vezes o hardware é importante, mas acho que o que acontece com mais freqüência, ao menos em olimpíadas, maratonas e problemas em casa, é precisarmos medir o tempo que ele irá demorar.

Eu expliquei em algum dos artigos anteriores que o tempo de um algoritmo depende geralmente do tamanho de sua entrada. Com este artigo, pretendo explicar como analisamos um algoritmo baseado nesse tamanho de sua entrada para compará-lo com outros algoritmos e ter uma noção de quanto tempo ele vai demorar.

Para o entendimento ficar mais fácil, vamos partir do seguinte algoritmo (que vamos chamar de Algoritmo 1):

para i \leftarrow{} 1 até n, faça
para j \leftarrow{} 1 até i, faça
  imprima i ×\times{} j ×\times{} n
fim-para
fim-para

O que este algoritmo faz é, depois de receber a entrada nn do usuário, imprimir o produto de nn com todos dois números ii e jj, tal que jinj \leq{} i \leq{} n.

Para medir o custo do algoritmo, nossa análise consistirá em ver quantas vezes cada passo é executado. Mediremos o custo de cada linha (cada passo a ser executado), sempre em função de n, que para este algoritmo é a variável mais importante (aliás, a única variável). Por isso o pseudocódigo do Algoritmo 1 está com suas linhas numeradas. Vamos analisar…

  • Linha 1: Será executada n+1n + 1 vezes.
  • Linha 2: Será executada n×_i=1n+nn \times{} \sum\_{i=1}^{n} + n vezes.
  • Linha 3: Será executada n×_i=1nn \times{} \sum\_{i=1}^{n} vezes.
  • Linhas 4 e 5: Não tem custo. :)

Por que estes números de execução?

Se você já entendeu por que cada passo é executado este número de vezes, pode pular essa parte (continuar a ler a partir da linha horizontal).

Linha 1

O loop para voltará para si mesmo nn vezes, isso é, testará novamente sua condicional e incrementará um. Por sempre testar um condicional, no final ele terá que testar novamente para dizer que já passou de nn. Por isso, ele será executado n+1n+1 vezes, ao invés de simplesmente nn.

Linha 2

Este loop para será executado um número de vezes variável (ii), que irá de 11 a nn. Portanto, ele será executado duas vezes (1 mais “o último condicional”) no primeiro loop de ii, três (2 mais “o último condicional”) no segundo, e por aí vai. Com isso, ele será executado o número de vezes que equivale a soma de 11 a nn, mais nn que são “os últimos condicionais”.

Linha 3

Exatamente o mesmo número que a Linha 2, mas sem “os últimos condicionais” (n-n).


Imprimir algo na tela pode demorar mais do que fazer uma operação, mas a análise de algoritmos é uma coisa bem rústica. Desprezamos todas as constantes, com isso só levando a sério a informação importante: neste caso, apenas nn. Então agora, vamos escrever o tempo de execução do algoritmo, que é a soma dos tempos de execução para cada instrução executada.

T(n)=(n+1)+(_i=1n+n)+(_i=1n)T(n) = (n + 1) + (\sum\_{i=1}^{n} + n) + (\sum\_{i=1}^{n})

Os parênteses não são necessários, mas coloquei para ajudar na visualização separando o custo de cada instrução.

Simplificando esta operação, teremos:

T(n)=n2+3nT(n) = n^{2} + 3n, uma função quadrática.

Ordem de Crescimento

Como eu já disse antes, descobrir o custo de um algoritmo é uma coisa feita sem precisão, porque para entradas realmente grandes (que são casos onde precisamos do computador!) as constantes não importam. Agora vamos determinar a ordem de crescimento de um algoritmo resgatando do nosso algoritmo apenas o valor mais importante, o maior expoente de nn nele, neste caso, n2n^{2}. Se tivéssemos 2n22 n^{2}, por exemplo, também usaríamos apenas n2n^{2} porque o 22 que multiplica também é desprezível!

Vamos agora aprender como representar o custo desse algoritmo usando notações assintóticas com a ordem de crescimento do algoritmo.

Se você não entendeu alguma coisa aí em cima, sugiro reler antes de continuar…

Notações Assintóticas

Sugestão

Principalmente para pessoas pouco habituadas com matemática, essa parte é difícil e cansativa. Quando eu comecei a aprender isto, talvez por causa da matemática tão básica que é ensinada na escola, eu não entendia nada… Mas só quero dar uma dica: se você não entender direito ou achar muito complicado, pule para a próxima linha horizontal ao invés de desistir e dizer que “algoritmos são muito difíceis”. Tentei fazer o artigo para você poder pular essa parte e mesmo assim não parar no estudo dos algoritmos… Depois, com o tempo, você vai aprendendo isso.

As notações que usamos para descrever o tempo de execução de um algoritmo são cinco:

  • Θ\Theta{}
  • OO
  • Ω\Omega{}
  • oo
  • ω\omega{}

Embora essas notações sejam conjuntos, usamos o sinal de igualdade (=) para expressar que f(n)f(n) pertence a algum deles, ao invés de usar o sinal de pertinência (\in{}).

Vou explicá-las, omitindo alguns fatos para tentar facilitar o entendimento, porque eu acho que analisar algoritmos é meio complicado e nessa parte é extremamente difícil ser didático. Mas se você realmente se interessar, você pode me enviar um comentário pedindo mais um artigo sobre isso (e eu terei o prazer de até pesquisar para informar-lhes mais) ou então leia o Capítulo 3 do livro Algoritmos: Teoria e Prática, que acredito que seja bem completo. Gostaria de enfatizar aqui que meu objetivo com essa série é tornar uma introdução a algoritmos simples e não ser uma referência, como é o objetivo, por exemplo, do livro do Cormen [et al].

A notação Θ\Theta{}

Lê-se “theta de gê de ene”.

Θ(g(n))=f(n)\Theta{}(g(n)) = f(n), se existem constantes positivas c_1c\_{1}, c_2c\_{2} e n_0n\_{0} tais que 0c_1g(n)f(n)c_2g(n)0 \leq{} c\_{1} g(n) \leq{} f(n) \leq{} c\_{2} g(n) para todo nn_0n \geq{} n\_{0}.

A notação OO

Lê-se “ó maiúsculo de gê de ene”. Para quando há apenas um limite assintótico superior.

O(g(n))=f(n)O(g(n)) = f(n), se existem constantes positivas cc e n_0n\_{0} tais que 0f(n)cg(n)0 \leq{} f(n) \leq{} cg(n) para todo nn_0n \geq{} n\_{0}.

A notação Ω\Omega{}

Lê-se “omega maiúsculo de gê de ene”. Para quando há apenas um limite assintótico inferior.

Ω(g(n))=f(n)\Omega{}(g(n)) = f(n), se existem constantes positivas cc e n_0n\_{0} tais que 0cg(n)f(n)0 \leq{} cg(n) \leq{} f(n) para todo nn_0n \geq{} n\_{0}.

A notação oo

Lê-se “ó minúsculo de gê de ene”. Para quando há apenas um limite assintótico superior, sem permitir que f(n)=cg(n)f(n) = cg(n). Utiliza-se a notação oo para denotar um limite superior que não é assintoticamente restrito.

o(g(n))=f(n)o(g(n)) = f(n), se para qualquer constante c>0c > 0, existe uma constante n_0>0n\_{0} > 0 tal que 0f(n)cg(n)0 \leq{} f(n) \leq{} cg(n) para todo nn_0n \geq{} n\_{0}.

A notação ω\omega{}

Lê-se “omega minúsculo de gê de ene”. Para quando há apenas um limite assintótico inferior, sem permitir que cg(n)=f(n)cg(n) = f(n). Utiliza-se a notação ω\omega{} para denotar um limite inferior que não é assintoticamente restrito.

ω(g(n))=f(n)\omega{}(g(n)) = f(n), se para qualquer constante c>0c > 0, existe uma constante n_0>0n\_{0} > 0 tal que 0cg(n)f(n)0 \leq{} cg(n) \leq{} f(n) para todo nn_0n \geq{} n\_{0}.

Para analisar problemas mais complexos como, por exemplo, recorrências, existem métodos bastante interessantes, como o Teorema Mestre que o Cormen apresenta no Capítulo 4. É uma boa leitura pra quem se interessou.


Podemos criar várias comparações entre estas funções, mas isto não vem ao caso. O importante é saber em que notação a nossa função se encontra. Com o tempo vamos compreendendo melhor essas fórmulas.

Vamos relembrar o custo de nosso algoritmo… T(n)=n2+3nT(n) = n^{2} + 3n.

Vamos ver em que notação ele pode se encaixar, sabendo que g(n)g(n) seria a ordem de crescimento (parte importante) do nosso custo; no caso, n2n^{2}.

Testamos primeiro se ele encaixa na função Θ(n2)\Theta{}(n^{2}). Vamos substituir f(n)f(n) e g(n)g(n) (naquela função ali em cima, onde diz A notação Θ\Theta{}) pelos valores que conhecemos.

c_1n2n2+3nc_2n2c\_{1}n^{2} \leq{} n^{2} + 3 n \leq{} c\_{2} n^{2}

Se dividirmos tudo por n2n^{2}, obteremos:

c_11+3nc_2c\_{1} \leq{} 1 + \frac{3}{n} \leq{} c\_{2}

Agora separaremos as inequações.

Inequação 1: c_11+3nc\_{1} \leq{} 1 + \frac{3}{n}

Inequação 2: 1+3nc_21 + \frac{3}{n} \leq{} c\_{2}

Para satisfazer a Inequação 1, podemos quase automaticamente perceber que para qualquer n1n \geq{} 1, é válido c_1=1c\_{1} = 1 (ora, por mais que 3n\frac{3}{n} chegue perto de 0, sempre ainda vamos ter a constante 1 adicionada a ele). Para satisfazer a Inequação 2, podemos perceber facilmente que para qualquer n1n \geq{} 1, é válido c_2=4c\_{2} = 4 (a função só tende a diminuir a partir que nn vai aumentando e com n=1n=1, c_2=4c\_{2}=4). Com isso, agora chegamos as três constantes que precisávamos.

n_0n\_{0} (o menor valor de nn) =1= 1; c_1=1c\_{1} = 1; c_2=4c\_{2} = 4.

Logo, concluímos que f(n)=n2+3n=Θ(n2)f(n) = n^{2} + 3n = \Theta{}(n^{2}). Uma função que pertence a Θ\Theta{}, tem um limite assintótico superior e inferior e, portanto, pertenceria também a O(n2)O(n^{2}) e Ω(n2)\Omega{}(n^{2}), mas nem é necessário testar os outros valores porque já identificamos nossa função como “theta de ene ao quadrado”, que é a função mais “retinha” que podemos esperar.

Bom… Nos próximos artigos, veremos que um mesmo problema pode ter várias soluções diferentes com custos ainda mais diferentes! Por isso, é crucial sabermos analisar, mesmo que por cima, qual o algoritmo que é mais eficiente. Vou ficando por aqui…

Sobre as mudanças

Estou em Florianópolis e passarei aqui mais uma semana, o que é excelente. Agora estou na casa da minha tia, que, além de ficar do lado da praia, tem ADSL (e uma placa de rede sobrando no computador para o laptop aproveitar a internet… :))

Eu planejava postar um artigo por dia da Série Algoritmos, mas o artigo que estou escrevendo agora é a parte de algoritmos que eu considero a mais complicada (muito mais complicada que os algoritmos em si que vamos estudar depois): análise de algoritmos. E estou fazendo um esforço para deixar a parte matemática bem leve e introduzir essa matéria de maneira bem simples, por isso a demora.

Aí eu resolvi escrever um artigo inteiro só pra comentar sobre as mudanças no blog.

Leitores do meu feed, me desculpem pelas várias atualizações de ontem. Além de mudar a estrutura da página (tirando, por exemplo, os comentários da página inicial), eu estou reorganizando os artigos a partir de agora, separando-os em categorias (cada um com só uma categoria) e usando o plugin Simple Tags para a tagsonomia.

Se vocês observarem, incluí links para a validação de WAI e da Section 508 na minha sidebar. Meu site tá bem acessível com esse novo design e estrutura. :) Além disso, enviei-o para algumas galerias e por isso as visitas estão mais em alta do que já estavam antes nessa semana.

Hmmm… Para os que ainda não perceberam, troquei as antigas páginas Biografia e Portifólio por apenas uma seção, denominada Currículo. Essa mudança foi para que eu não precisasse de grandes atualizações em textos (o que é uma coisa muito chata de fazer!) e ter minhas informações profissionais separadas em um só lugar. Embora eu não tenha quase experiência alguma (quer dizer, experiência até eu tenho, mas não tenho cursos), espero que o currículo também me ajude com os negócios.

Adicionei um link para meus feeds no Bloglines também e dei uma arrumada na seção de problemas lógicos.

Espero que estejam gostando das mudanças… Agora estou me preparando para mudanças mais radicais, como transformar o site inteiro em Ajax (o que não é difícil no WordPress, mas vou esperar no mínimo esperar a série de algoritmos pra não ficar atrasado com ela)

© 2005–2020 Tiago Madeira