Arquivo da tag: análise de algoritmos

Ordenação por Seleção

ATENÇÃO: Este conteúdo foi publicado há 12 anos. Eu talvez nem concorde mais com ele. Se é um post sobre tecnologia, talvez não faça mais sentido. Mantenho neste blog o que escrevo desde os 14 anos por motivos históricos. Leia levando isso em conta.

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:

1. para i \leftarrow{} 1 até tamanho-1, faça
2.  minimo \leftarrow{} i
3.  para j \leftarrow{} i+1 até tamanho, faça
4.      se vetor[j] < vetor[minimo], então
5.          minimo \leftarrow{} j
6.      fim-se
7.  fim-para
8.  temp \leftarrow{} vetor[i]
9.  vetor[i] \leftarrow{} vetor[minimo]
10. vetor[minimo] \leftarrow{} temp
11. 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=1. Vou sempre marcar i com a cor preta e min 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=2 até o comprimento do vetor, com o objetivo de descobrir qual o menor elemento.

j=2v[j] = 3 < v[minimo] = v[1] = 5[/latex], portanto [latex]minimo = j = 2[/latex].  <table> <thead> <tr> <td>v[1]</td> <td>v[2]</td> <td>v[3]</td> <td>v[4]</td> <td>v[5]</td> <td>v[6]</td> </tr> </thead> <tr> <td class="preto">5</td> <td class="cinza">3</td> <td>7</td> <td>8</td> <td>2</td> <td>5</td> </tr> </table>  [latex]j=3 ... v[j] = 7 > v[minimo] = v[2] = 3, portanto não mexemos em nada.

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

j=5 ... v[j] = 2 < v[minimo] = v[2] = 3[/latex], portanto [latex]minimo = j = 5[/latex].  <table> <thead> <tr> <td>v[1]</td> <td>v[2]</td> <td>v[3]</td> <td>v[4]</td> <td>v[5]</td> <td>v[6]</td> </tr> </thead> <tr> <td class="preto">5</td> <td>3</td> <td>7</td> <td>8</td> <td class="cinza">2</td> <td>5</td> </tr> </table>  [latex]j=6 ... v[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. n
  2. n-1
  3. \sum_{j=1}^{n} + n
  4. \sum_{j=1}^{n}
  5. \sum_{j=1}^{n} ???
  6. 0
  7. 0
  8. n-1
  9. n-1
  10. n-1
  11. 0

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 0 (como se nenhum se entrasse) e depois com ela valendo \sum_{j=1}^{n}.

Primeiro cálculo

T(n) = (n) + (n-1) + (\sum_{j=1}^{n} + n) + (\sum_{j=1}^{n}) + (0) + (0) + (0) + (n-1) + (n-1) + (n-1) + (0)

T(n) = n^{2} + 6n -3 \Theta{}(n^{2}) = f(n)

Segundo cálculo

T(n) = (n) + (n-1) + (\sum_{j=1}^{n} + n) + (\sum_{j=1}^{n}) + (\sum_{j=1}^{n}) + (0) + (0) + (n-1) + (n-1) + (n-1) + (0)

T(n) = 1,5 n^{2} + 6,5 n - 3 \Theta{}(n^{2}) = f(n)

Conclusão

Como vocês puderam ver, não faz diferença alguma o \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

ATENÇÃO: Este conteúdo foi publicado há 12 anos. Eu talvez nem concorde mais com ele. Se é um post sobre tecnologia, talvez não faça mais sentido. Mantenho neste blog o que escrevo desde os 14 anos por motivos históricos. Leia levando isso em conta.

Também conhecida como Insertion Sort, a Ordenação por Inserção consiste em inserir um elemento n num vetor já ordenado de n-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:

1. para j \leftarrow{} 2 até comprimento do vetor, faça
2.  elemento \leftarrow{} vetor[j]
3.  i \leftarrow{} j - 1
4.  enquanto i > 0 e vetor[i] > elemento, faça
5.      vetor[i + 1] \leftarrow{} vetor[i]
6.      i \leftarrow{} i - 1
7.  fim-enquanto
8.  vetor[i + 1] \leftarrow{} elemento
9. 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=2 e iterar até o comprimento do vetor (6). A primeira ordem que ele me dá é para armazenar o elemento a[j] (a[2]) na variável elemento. Para facilitar toda a explicação eu vou sempre pintar de cinza o v[j] onde eu estou (no caso, o segundo elemento do vetor, 3) e de preto o vetor ainda não ordenado (elementos \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 i \leftarrow{} j-1. Portanto, i=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 =1 é maior que 0. v[1]=5 é maior que o elemento=3? Sim, então vamos entrar no corpo do enquanto… Aqui ele me manda fazer um vetor[i+1] = vetor[i], que nesse caso é fazer um v[2]=v[1]=5.

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

E agora subtrae de i um valor. Portanto, i=0. Ele retorna ao enquanto, mas agora não satisfazemos a condição i>0, por isso saímos do enquanto. Então ele pede para vetor[i+1]=elemento (v[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=3.

v[1] v[2] v[3] v[4] v[5] v[6]
3 5 7 8 2 5
elemento = 7 i = 3-1 = 2

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

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

E lá vamos para j=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[/latex]) contém todos seus valores ordenados e o segundo (de elementos [latex]\geq{}j[/latex]) contém os valores que devem ser <strong>inseridos</strong> no <em>primeiro</em> vetor já ordenado (por isso ele se chama <strong>Algoritmo de Inserção</strong>).   A chave do algoritmo é o <strong>enquanto</strong> que acontece para ir deslocando todos os elementos para seu índice [latex]+1 enquanto o elemento for menor que eles (para depois o elemento ser colocado onde parou).

A variável elemento só serve para não perdermos o valor de v[j] (porque depois fazemos v[i+1]=v[i] quando i=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 i e portanto será executado bem mais rápido do que se o vetor estiver inteiro em ordem decrescente (quando ele sempre precisará iterar i 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: \Theta{}(n)
  • Pior caso é quando os elementos estão em ordem decrescente. Custo: \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 é \Theta{}(n^{2}).

Recursão

ATENÇÃO: Este conteúdo foi publicado há 12 anos. Eu talvez nem concorde mais com ele. Se é um post sobre tecnologia, talvez não faça mais sentido. Mantenho neste blog o que escrevo desde os 14 anos por motivos históricos. Leia levando isso em conta.

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 n é igual a: n \times{} n-1 \times{} n-2 \times{} \ldots{} 2 \times{} 1.

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


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

1. função fatorial (n)
2.  se n = 1, então
3.      retorna 1
4.  senão
5.      retorna n \times{} fatorial(n-1)
6.  fim-se
7. fim-função

Domínio de nossa função: n \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. n
  2. n-1
  3. 1
  4. n-1
  5. n-1
T(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 n-1 vezes, mas só entramos nele uma vez? Então vamos inverter nosso condicional.

1. função fatorial (n)
2.  se n > 1, então
3.      retorna n \times{} fatorial(n-1)
4.  senão
5.      retorna 1
6.  fim-se
7. fim-função

Novo custo

  1. n
  2. n-1
  3. n-1
  4. 1
  5. 1
T(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+2 comparado com 3n é uma diferença pequena, mas essa solução ficou bem mais elegante. ;) Poxa, diminuímos o custo do algoritmo em \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{} é: 0 \leq{} c_{1} g(n) \leq{} f(n) \leq{} c_{2} g(n)

Substituindo pela nossa função, temos: c_{1}n \leq{} 3n \leq{} c_{2}n. É trivial, que podemos escolher para as duas constantes c_{1}=c_{2}=3 e para n_{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 \Theta{}(n) e uma função quadrática pertence sempre a notação \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 n a cada iteração, até que chegue ao valor mínimo 1 aonde a resposta é trivial: 1.

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 é n \in{} \mathbf{N} / n \geq{} 1. Se o cara colocasse n=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! = 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

1. função fibonacci (n)
2.  se n \geq{} 3, então
3.      retorna fibonacci(n-1) + fibonacci(n-2)
4.  senão
5.      retorna 1
6.  fim-se
7. fim-função

Domínio de fibonacci(n): n \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

1. função imprime_ate (n)
2.  imprima n
3.  se n>1, então
4.      imprime_ate(n-1)
5.  fim-se
6. fim-função

Domínio de imprime_ate(n): n \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

ATENÇÃO: Este conteúdo foi publicado há 12 anos. Eu talvez nem concorde mais com ele. Se é um post sobre tecnologia, talvez não faça mais sentido. Mantenho neste blog o que escrevo desde os 14 anos por motivos históricos. Leia levando isso em conta.

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):

1. para i \leftarrow{} 1 até n, faça
2.  para j \leftarrow{} 1 até i, faça
3.      imprima i \times{} j \times{} n
4.  fim-para
5. fim-para

O que este algoritmo faz é, depois de receber a entrada n do usuário, imprimir o produto de n com todos dois números i e j, tal que j \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 + 1 vezes.
  • Linha 2: Será executada n \times{} \sum_{i=1}^{n} + n vezes.
  • Linha 3: Será executada n \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 n 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 n. Por isso, ele será executado n+1 vezes, ao invés de simplesmente n.

Linha 2

Este loop para será executado um número de vezes variável (i), que irá de 1 a n. Portanto, ele será executado duas vezes (1 mais “o último condicional”) no primeiro loop de i, 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 1 a n, mais n que são “os últimos condicionais”.

Linha 3

Exatamente o mesmo número que a Linha 2, mas sem “os últimos condicionais” (-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 n. 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) + (\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) = 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 n nele, neste caso, n^{2}. Se tivéssemos 2 n^{2}, por exemplo, também usaríamos apenas n^{2} porque o 2 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{}
  • O
  • \Omega{}
  • o
  • \omega{}

Embora essas notações sejam conjuntos, usamos o sinal de igualdade (=) para expressar que 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”.

\Theta{}(g(n)) = f(n), se existem constantes positivas c_{1}, c_{2} e n_{0} tais que 0 \leq{} c_{1} g(n) \leq{} f(n) \leq{} c_{2} g(n) para todo n \geq{} n_{0}.

A notação O

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

O(g(n)) = f(n), se existem constantes positivas c e n_{0} tais que 0 \leq{} f(n) \leq{} cg(n) para todo n \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.

\Omega{}(g(n)) = f(n), se existem constantes positivas c e n_{0} tais que 0 \leq{} cg(n) \leq{} f(n) para todo n \geq{} n_{0}.

A notação o

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

o(g(n)) = f(n), se para qualquer constante c > 0, existe uma constante n_{0} > 0 tal que 0 \leq{} f(n) \leq{} cg(n) para todo n \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). Utiliza-se a notação \omega{} para denotar um limite inferior que não é assintoticamente restrito.

\omega{}(g(n)) = f(n), se para qualquer constante c > 0, existe uma constante n_{0} > 0 tal que 0 \leq{} cg(n) \leq{} f(n) para todo n \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) = n^{2} + 3n.

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

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

c_{1}n^{2} \leq{} n^{2} + 3 n \leq{} c_{2} n^{2}

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

c_{1} \leq{} 1 + \frac{3}{n} \leq{} c_{2}

Agora separaremos as inequações.

Inequação 1: c_{1} \leq{} 1 + \frac{3}{n}

Inequação 2: 1 + \frac{3}{n} \leq{} c_{2}

Para satisfazer a Inequação 1, podemos quase automaticamente perceber que para qualquer n \geq{} 1, é válido c_{1} = 1 (ora, por mais que \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 n \geq{} 1, é válido c_{2} = 4 (a função só tende a diminuir a partir que n vai aumentando e com n=1, c_{2}=4). Com isso, agora chegamos as três constantes que precisávamos.

n_{0} (o menor valor de n) = 1; c_{1} = 1; c_{2} = 4.

Logo, concluímos que 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(n^{2}) e \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

ATENÇÃO: Este conteúdo foi publicado há 12 anos. Eu talvez nem concorde mais com ele. Se é um post sobre tecnologia, talvez não faça mais sentido. Mantenho neste blog o que escrevo desde os 14 anos por motivos históricos. Leia levando isso em conta.

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)