domingo, 4 de outubro de 2009

Ordenação – Métodos e análises: BubbleSort – Parte 2

No último post, comecei a falar sobre o BubbleSort, e apresentei uma análise do algoritmo e uma implementação. Mas até ali, fizemos algo muito simples. O que vamos fazer agora é melhorar um pouco aquela implementação.
Como vocês bem se lembram, o nosso algoritmo executava sempre n² operações. O que vamos fazer é acrescentar algumas verificações que permitam que, na maioria dos casos, o algoritmo termine antes disso.

Para isso, vamos lembrar do algoritmo (apenas a função que implementa o BubbleSort:


void bubbleSort( int *vet, int n )
{
int i, j, aux;

for( i=0; i < n; i++ )
{
for( j = 0; j < n-1; j++ )
{
if( vet[j] > vet[j+1] )
{
aux = vet[j];
vet[j] = vet[j+1];
vet[j+1] = aux;
}
}
}
}


E as duas primeiras iterações dele no nosso vetor de exemplo:

[ 5, 13, 2, 7, 11, 14, 6]
[ 5, 13, 2, 7, 11, 14, 6]
[ 5, 2, 13, 7, 11, 14, 6]
[ 5, 2, 7, 13, 11, 14, 6]
[ 5, 2, 7, 11, 13, 14, 6]
[ 5, 2, 7, 11, 13, 14, 6]

[ 5, 2, 7, 11, 13, 6, 14]
[ 2, 5, 7, 11, 13, 6, 14]
[ 2, 5, 7, 11, 13, 6, 14]
[ 2, 5, 7, 11, 13, 6, 14]
[ 2, 5, 7, 11, 13, 6, 14]
[ 2, 5, 7, 11, 6, 13, 14]


Da primeira iteração a gente não consegue escapar, pois ainda não fizemos nenhuma varredura por todo o vetor, e ainda não temos noção do que podemos encontrar. Mas a partir daí, podemos perceber, tanto pelo código quanto pelos exemplos que o maior valor do vetor está subindo até o final.

Qual seria o sentido então de permitir que, por exemplo, na 2ª iteração, o 13 que está subindo seja comparado com o 14 que foi colocado no fim do vetor na iteração anterior? Ele jamais será maior. (Veja a iteração sublinhada)

Espertos que somos, vamos ensinar o nosso programa a perceber e ignorar esses passos redundantes. Tente fazer por conta própria. Depois olhe abaixo para verificar se você fez certo ou para entender como se faz.

No programa, temos a variável i que controla o loop externo e “burramente” determina que o loop seja executado n vezes. O que faremos é: Em vez de ela simplesmente controlar o loop, vamos atribuir também a ela o papel de determinar até aonde no vetor o loop interno ainda precisa realizar as suas verificações.

Para isso, em vez de inicializál-la em 0 e fazê-la crescer até n, vamos inicializá-la em n, e decrementá-la até que alcancemos o valor 0. E que tal agora se fizermos o loop interno, que varre o vetor até a posição n-1, varrer apenas até a posição i-1? Será que funciona? É fácil perceber que sim (talvez você queira voltar alguns parágrafos, onde eu explico o porque).

Agora, nosso código ficará com a seguinte cara:


void bubbleSort( int *vet, int n )
{
int i, j, aux;

for( i = n-1; i > 0; i-- )
{
for( j = 0; j < i; j++ )
{
if( vet[j] > vet[j+1] )
{
aux = vet[j];
vet[j] = vet[j+1];
vet[j+1] = aux;
}
}
}
}


Será que obtivemos alguma melhora com isso? Vamos voltar ao nosso pior caso (um vetor em ordem decrescente) e pensar a respeito.

Na primeira iteração, teremos n-1 operações. Já na segunda, teremos no máximo n-2, já que o limite superior da parte do vetor a ser avaliada foi reduzido de 1. Generalizando esse raciocínio, teremos o seguinte número de operações:

`(n-1) + (n-2) + \ldots` `+ 1`

Pra quem se lembra, a soma dessa PA nos dará

`S = [(n-1) * ((n-1) + 1)] / [2]`
`S = [n * (n-1)]/[2]`

O que quer dizer que reduzimos o numero máximo de iterações a menos da metade! Ou seja: para um BubbleSort, essa melhoria é bastante significativa. (Não acredite 100% nisso ainda. Vamos falar mais sobre essa “melhoria” em alguns instantes).

Ainda temos mais um acerto para fazer, e a idéia é a seguinte. Será que precisamos continuar o algoritmo se, em alguma iteração, não acontecer nenhuma troca? Espero que você consiga perceber que não. O que vamos fazer então é adicionar uma flag ao programa, que começa como false a cada iteração do loop externo e é setada para true assim que ocorre uma troca no loop interno. Ao fim do loop interno, caso a flag ainda seja false, o algoritmo pode encerrar.

Com isso, nosso código ficará assim:


void bubbleSort( int *vet, int n )
{
int i, j, aux;
int mexeu;

for( i = n-1; i > 0; i-- )
{
for( j = 0, mexeu = 0; j < i; j++ )
{
if( vet[j] > vet[j+1] )
{
aux = vet[j];
vet[j] = vet[j+1];
vet[j+1] = aux;
mexeu = 1;
}
}

if( mexeu == 0 ) break;
}
}


Repare que esta última modificação não melhora a performance, mas vai permitir que, na maior parte das vezes, que economizemos algumas operações.

Agora sim, temos uma versão aceitável para o BubbleSort!


Agora, retomemos a discussão sobre a melhoria que geramos com a primeira modificação: Na pratica, nós teremos sempre o número de operações reduzidas por um fator de aproximadamente ½. Apesar disso, quando falamos de complexidade de um algoritmo, esta melhoria é irrelevante.

Você deve estar se perguntando: "Mas, irrelevante? Por quê?" (Eu também me perguntei...)

Mesmo com a melhoria, ainda temos lá no resultado do numero de operações um termo `n^2`. Como estamos analisando um algoritmo que resolve um problema para vetores de qualquer tamanho, talvez você queira se perguntar como a função

`f(n) = [n*(n-1)] / [2]`

se comporta conforme n cresce arbitrariamente.

O que acontece é que a diminuição gerada pelo fator `½` vai se tornando cada vez mais irrelevante, pois é uma constante. Se você quiser, pode pensar em uma analogia (que na verdade não é uma analogia) com o cálculo.

`lim_(n->\infty)(n²) = ?`    ou    `lim_(n->\infty)((n² - n) / 2) = ?`

Este é o principio que se aplica aqui. Em ambos os casos, o resultado é `\infty`.

O que estou querendo dizer é: Quando se fala de complexidade de algoritmos, multiplicar, somar, dividir ou qualquer outra operação que envolva uma constante é irrelevante.

Temos então que: `O( [n*(n-1)]/[2] ) = O( [(n^2 - n)] / [2] ) = O(n^2)`

E na verdade, acabamos não ganhando nada em termos teóricos com essas melhorias. (Mas não se esqueça: De fato o número de operações diminui!).

Bem, isso é o que eu sei sobre BubbleSort. Na seqüência da série, falaremos sobre o MergeSort, um algoritmo que tem uma cara bem diferente do que vimos até aqui. No entanto, acho que vou fazer uma pausa antes, e falar sobre alguns assuntos que precisamos conhecer antes de falar sobre o MergeSort.

É isso aí, até mais!
Xico.

sexta-feira, 2 de outubro de 2009

Ordenação – Métodos e análises: BubbleSort – Parte 1

Nesta seqüência de 3 (ou mais) posts, minha intenção é falar um pouco sobre algoritmos de ordenação. Mais do que isso, queria focar um pouco na análise, mas também sem esquecer da implementação, para a qual usarei a linguagem C. Como eu disse no post de reapresentação, todo o conteúdo deste Blog, pelo menos por hora, é introdutório, logo voltado para pessoas que estão começando a se enveredar pelo mundo dos algoritmos.

O problema é muito simples, e é algo que nós fazemos o tempo todo, naturalmente: Dada uma seqüência de elementos comparáveis (ou seja, para os quais se tenha a noção de ordem), ordená-los da forma mais eficiente possível. Isto é: Dada uma seqüência de elementos

`a_1,\quad a_2,\quad a_3,\quad \ldots\quad, a_n`

Queremos um rearranjo desses elementos tal que

`a_1 \quad\le\quad a_2 \quad\le\quad a_3 \quad\le\quad \ldots \quad\le\quad a_n`

Em todos os exemplos, vamos mexer com vetores de números inteiros, mas lembre-se que os conceitos que vamos trabalhar são mais gerais do que isso, podendo ser aplicados para ordenar qualquer coisa.

O problema da Ordenação (ou sorting, em inglês) é um problema sobre o qual se quebrou muito a cabeça na computação, mas que hoje (na verdade há um bom tempo) é um assunto pra lá de resolvido. Apesar disso, ainda são desenvolvidos novos algoritmos de ordenação com propósitos específicos, mas que não vem ao caso para nós.

Vamos ao que interessa: Existem dezenas de diferentes métodos de ordenação, mas vou falar especificamente de 3, que pra mim sintetizam alguns dos conceitos mais importantes, para resolver este problema (tecnicamente) simples, e que posteriormente podem ser usados em muitas outras situações.

Vamos começar (obviamente) do começo. O BubbleSort, ou “método da Bolha”, na tradução que comumente se vê por aí. Ele é um algoritmo baseado em trocas (swapping) e é o assunto deste primeiro post, por ser o mais simples de todos. Esse algoritmo foi proposto em 1956, o que nos permite dizer que ele é um senhor algoritmo (piadinha infame...).

Qual é a idéia: “Varrer” (percorrer) o vetor várias vezes. A cada vez que varremos o vetor, vamos levando o maior elemento que encontrarmos pelo caminho para o final, até que nenhum deles precise mais mudar de posição. Na analogia da bolha, os números vão “subindo” (como uma bolha no fundo de um recipiente com água) até o fim do vetor, em ordem decrescente de tamanho.

Um pseudo-código, sem nenhum tipo de otimização para resolver o problema, poderia ser:


BubbleSort( vetor v, tamanho n) : Retorna Vetor ordenado.
Repita n vezes
Repita a partir do inicio do vetor, até o seu final
Se vetor[c] > vetor[c+1] , troque-os de posição.


Vejamos como seria o passo-a-passo desse algoritmo para ordenar a seguinte lista de elementos:
[5, 13, 2, 7, 11, 14, 6]


[ 5, 13, 2, 7, 11, 14, 6]
[ 5, 13, 2, 7, 11, 14, 6]
[ 5, 2, 13, 7, 11, 14, 6]
[ 5, 2, 7, 13, 11, 14, 6]
[ 5, 2, 7, 11, 13, 14, 6]
[ 5, 2, 7, 11, 13, 14, 6]

[ 5, 2, 7, 11, 13, 6, 14]
[ 2, 5, 7, 11, 13, 6, 14]
[ 2, 5, 7, 11, 13, 6, 14]
[ 2, 5, 7, 11, 13, 6, 14]
[ 2, 5, 7, 11, 13, 6, 14]
[ 2, 5, 7, 11, 6, 13, 14]

[ 2, 5, 7, 11, 6, 13, 14]
[ 2, 5, 7, 11, 6, 13, 14]
[ 2, 5, 7, 11, 6, 13, 14]
[ 2, 5, 7, 11, 6, 13, 14]
[ 2, 5, 7, 6, 11, 13, 14]
[ 2, 5, 7, 6, 11, 13, 14]

[ 2, 5, 7, 6, 11, 13, 14]
[ 2, 5, 7, 6, 11, 13, 14]
[ 2, 5, 7, 6, 11, 13, 14]
[ 2, 5, 6, 7, 11, 13, 14] -- Coincidentemente, aqui o vetor já está ordenado.
[ 2, 5, 6, 7, 11, 13, 14]
[ 2, 5, 6, 7, 11, 13, 14]

Obs: O negrito vermelho sinaliza que, na próxima iteração as referidas posições terão sido trocadas, e apenas negrito uma comparação para a qual os elementos já estão em ordem. Note também que o algoritmo não parou assim que o vetor ficou ordenado, pois computador não tem como saber que essa condição foi alcançada (não com o algoritmo que fornecemos para ele). Ele continuará a fazer o que você o instruiu através do pseudo-código.

E.... isso é um BubbleSort! Parece bonitinho e tal, mas já vamos analisar um pouco melhor como esse algoritmo se comporta. Enquanto isso, vamos pensar em algumas coisas importantes. Tente responder essas perguntas para si mesmo, ou escreva as respostas em papel:

1 – E se o vetor fornecido para o algoritmo estivesse em ordem decrescente?
2 – Quantas vezes passamos pela linha de código “Se vetor[c] > vetor[c+1] ...”?

Pense nessas perguntas por uns dois minutos, ou até que você encontre a resposta (esse seria o ideal. Dê a si mesmo a chance de usar o cérebro em vez de ficar só lendo.

Tic-Tac
.
.
.
Tic-Tac


Então... Como você deve ter percebido, se os números forem fornecidos em ordem decrescente, cada uma das comparações feitas dentro do loop interno irá gerar uma troca, e embora isso possa parecer inevitável, é extremamente ineficiente.


Agora sim, respondendo às perguntas:

1 – O programa vai entrar na condição ‘Se’ todas as vezes que passar lá.
2 – Temos um “Repita n vezes”, e dentro dele um loop que também ocorre n vezes. Logo, passaremos por essa linha n . n = n² vezes.

Obviamente, este é o pior caso possível para o nosso BubbleSort (no que diz respeito a executar o “Se”, já que inevitavelmente passaremos lá), pois é quando faremos todas as trocas possíveis. Agora, pare e pense um instante: n é o tamanho do vetor (quantos elementos há nele). Isso quer dizer que, se quisermos ordenar, digamos, 100 números com o BubbleSort, corremos o risco de ter que fazer até 10000 operações de troca!!!
O que se diz nesses casos é que o algoritmo tem complexidade (ou seja, executa um numero de operações) que é da ordem de n², onde n é o tamanho da entrada, e que no nosso caso é o tamanho do vetor. Mais comumente em computação, expressamos essa constatação dizendo que:

A complexidade de tempo do BubbleSort é O(n²).


Mais detalhes da “notação O”, eu fico devendo para uma próxima, embora eu vá mencioná-la em outros momentos. Por hora, basta saber que ele representa uma estimativa da ordem de grandeza do tempo de execução de um algoritmo qualquer (mas também existe notação O para expressar a complexidade de outras coisas, como espaço em memória que o algoritmo precisa pra "rodar").

Como vamos ver nos próximos posts, esse valor, mesmo levando-se em consideração o pior caso possível, pode ser melhorado (e muito!). Pra isso, precisamos “botar a caixola pra funcionar” e pensar em diferentes formas de atacar o problema.

Para terminar, mostro uma implementação em C relativa ao pseudo-código que apresentei antes, sem tirar nem pôr. No entanto, fiz um programa completo que você pode salvar e executar sem problemas. Lembrando que ele pode ser melhorado, e que eu o farei no próximo post (é, ainda tem mais!).


/*
Autor: Francisco Viégas Vianna
Data: 04/10/2009
Descrição: Impementação do método de ordenação BubbleSort.

Por favor, mantenha esse comentário.
*/

#include < stdio.h >

#define MAXT 1000

void bubbleSort( int *vet, int n )
{
int i, j, aux;

for( i=0; i < n; i++ )
{
for( j = 0; j < n-1; j++ )
{
if( vet[j] > vet[j+1] )
{
aux = vet[j];
vet[j] = vet[j+1];
vet[j+1] = aux;
}
}
}
}

int main()
{
int vetor[MAXT];
int n, i;

printf( "Quantos elementos (MAX = %d)? ", MAXT );
scanf( "%d", &n );

for( i=0; i < n; i++ )
{
scanf("%d", &( vetor[i]) );
}

bubbleSort( vetor, n );

printf( "\nOrdenando o vetor:\n" );
for( i=0; i < n; i++ )
{
printf( "%d ", vetor[i] );
}
return 0;
}


É isso aí. Até a próxima!

quinta-feira, 1 de outubro de 2009

Testando expressões Matemáticas no Blogger

Tá, eu realmente estou me divertindo adicionando essas funcionalidades ao Blog.
Mas pô, não é à toa, pode vir a ser útil!

Enfim, a bola da vez é o ASCIIMathML, que pelo que eu pude entender (confesso que não dediquei muita da minha atenção a isso) usa a engine do LaTeX, e depois gera HTML, tudo através de... Javascript! Bem legal, né?

Então, vamos ver uns exemplos pra testar:

`[[a,b],[c,d]]`

`\sqrt{\frac{1}{1+x^2}}`

`P_n(x)=a_0 + a_1 x +\ldots + a_n x^n, a_i\in R, i=1,2,\ldots, n`

Lá no 1° link tem tudo explicadinho, inclusive mostrando a sintaxe para você criar as suas próprias expressões. Se você não conseguir fazer funcionar, posta aqui que eu ajudo. Mas esse é bem simples mesmo. E o resultado é bem satisfatório!

Ah, já ia esquecendo: Uma das coisas mais legais é que, depois que a fórmula foi gerada e colocada em uma página HTML, se você parar o mouse em cima dela aparecesse uma caixinha daquelas de 'hint' com a expressão que deu origem a ela!!!


Por hoje é só!
Abraço a todos.

Testando os scripts de Syntax Highlighting

Como muito do que eu quero postar aqui tem código-fonte, pus-me a fuçar algo que me ajudasse a formatá-los, pra melhorar o look do Blog:

Encontrei o SyntaxHighlighter, e com uma ajudinha do amigo Buss, cheguei no lugar certo, esse aí do link.
Com um pouquinho mais de paciência, achei esse tutorial.


Agora, vamos testar:

SQL:

SELECT *
FROM users
WHERE user_id = 1212;

C/C++:

#include < stdio.h >

int main( void )
{
printf( "Hello, world!" );

return 0;
}

Java:

public class Hello {
public static void main(String[] args) {
System.out.println("Olá Mundo!");
}
}

Python:

print "Hello, World!"

Acho que eu habilitei algumas outras além dessas, dentre as mais de 20 opções, mas agora não me lembro quais.
BTW, créditos pro cara. Muito maneiro e fácil de usar, além de ser o tipo da coisa simples, útil e bem-feita (pelo menos ainda não peguei bugs).


Enjoy!