Você está na página 1de 16

Radix Sort

Estive lendo sobre alguns algoritmos de organizao e me deparei com o Radix Sort. Pela definio da NIST: Definition: A multiple pass distribution sort algorithm that distributes each item to a bucket according to part of the item's key beginning with the least significant part of the key. After each pass, items are collected from the buckets, keeping the items in order, then redistributed according to the next most significant part of the key. Traduo livre: Definio: Um algortmo de organizao de vrios passos que distribui cada item para um bucket de acordo com parte da chave do item, comeando pela parte menos significativa da chave. Aps cada passo, os items so recolhidos dos buckets, mantendo os itens em ordem e ento redistribuindo de acordo com a prxima parte mais significativa da chave. Pelas leituras que fiz por a, um mtodo que me pareceu bastante interessante aplicar esse algortmo a cada bit da nossa informao. claro que isso vai depender da aplicao do algortmo. No caso que irei trabalhar aqui, estaremos ordenando um vetor com nmeros inteiros positivos. Vamos supor o seguinte array: int vetor = {12,22,13,16,31} Descrevendo cada um desses valores em binrio: 12 - 01100 22 - 10110 13 - 01101 16 - 10000 31 - 11111 Pela descrio do algortmo e pelo mtodo adotado, devemos comear organizando nossa informao pelo bit menos

significativo de cada um dos nmeros em nosso array, ou seja, pegamos o bit mais a direita do array e distribumos os valores em 2 buckets. O bucket onde o bit que estamos olhando 0 ou ou bucket onde o bit que estamos olhando 1. Ilustrando isso, temos: 01100 10110 01101 10000 11111 12 22 13 16 31

Comeamos olhando para os bits que esto em negrito. Quando encontrarmos um bit 0, jogamos aquele nmero para o bucket0. Quando encontrarmos um bit 1, jogamos aquele nmero para o bucket1. Nossos buckets esto dessa forma agora ento: bucket0 = {12, 22, 16} bucket1 = {13, 31} Agora ns devolvemos os valores para dentro do nosso vetor na ordem em que se encontram dentro dos bukets. Primeiro colocamos dentro do vetor todos os valores do bucket0 e depois todos os valores do bucket1. Nosso vetor ficar da seguinte maneira: vetor = {12, 22, 16, 13, 31} Agora ns repetimos o processo para o nosso novo vetor, utilizando o prximo bit mais significativo. 01100 10110 10000 01101 11111 12 22 16 13 31

Nossos buckets ficam: bucket0 = {12, 16, 13} bucket1 = {22, 31} Nosso novo vetor:

vetor = {12, 16, 13, 22, 31} Repetimos o processo para o prximo bit mais significativo em nosso novo vetor: 01100 10000 01101 11010 11111 12 16 13 22 31

Nossos buckets ficam: bucket0 = {16, 22} bucket1 = {12, 13, 31} Nosso novo vetor: vetor = {16, 22, 12, 13, 31} Repetimos o processo para o prximo bit mais significativo em nosso novo vetor: 10000 11010 01100 01101 11111 16 22 12 13 31

Nossos buckets ficam: bucket0 = {16} bucket1 = {22, 12, 13, 31} Nosso novo vetor: vetor = {16,22,12,13,31} At ento no parece nada arrumado no mesmo? Mas o timo passo vai nos revelar algo fantstico! :D Repetimos o processo para o prximo bit mais significativo em nosso novo vetor:

10000 11010 01100 01101 11111

16 22 12 13 31

Nossos buckets ficam: bucket0 = {12, 13} bucket1 = {16, 22, 31} Nosso novo (e ltimo) vetor: vetor = {12, 13, 16, 22, 31} Aha! Fantstico! O mais interessante disso tudo a velocidade desse algortmo. Em alguns testes que fiz aqui, consegui organizar 20 milhoes de nmeros em apenas 9,3 segundos! Irei demonstrar a seguir uma verso um pouco mais lenta do algortmo que desenvolvi porm de mais fcil entendimento. Logo aps irei demonstrar a verso do mesmo algortmo que foi capaz de organizar 100 milhes de nmeros em 15,56 segundos. O algortmo 'simples'
#include <stdio.h> #include <stdlib.h> #include <time.h> typedef struct lista { int num; struct lista *prox; } lista; void radix_sort(int *, int); void insere(lista **, int); int pop(lista **); void imprime(int *vetor, int tamanho); int main() { int tamanho = 20; int vetor[tamanho]; int i; srand(time(NULL)); for(i = 0; i < tamanho; i++) { vetor[i] = rand() % tamanho; } imprime(vetor, tamanho); printf("\n");

radix_sort(vetor, tamanho); imprime(vetor, tamanho); printf("\n"); return 0; } void insere(lista **l, int valor) { lista *aux = *l; if(*l == NULL) { *l = (lista *)malloc(sizeof(lista)); (*l)->num = valor; (*l)->prox = NULL; } else { while(aux->prox != NULL) aux = aux->prox; aux->prox = (lista *)malloc(sizeof(lista)); aux = aux->prox; aux->num = valor; aux->prox = NULL; } } int pop(lista **l) { int ret; lista *aux; ret = (*l)->num; aux = *l; *l = (*l)->prox; free(aux); return ret; } void radix_sort(int *vetor, int tamanho) { lista *bucket0 = NULL; lista *bucket1 = NULL; int i, j; int numero_bits; numero_bits = sizeof(int) * 8; for(i = 0; i < numero_bits; i++) { for(j = 0; j < tamanho; j++) { if((((vetor[j] >> i) & 0xf) % 2) == 0) insere(&bucket0, vetor[j]); else insere(&bucket1, vetor[j]); } j = 0; while(bucket0 != NULL) { vetor[j] = pop(&bucket0); j++; } while(bucket1 != NULL) { vetor[j] = pop(&bucket1); j++; }

} } void imprime(int *vetor, int tamanho) { int i; for(i = 0; i < tamanho; i++) printf("%d ", vetor[i]); }

[radix.c] Essa a verso 'simples' do algortmo. Ele utiliza listas para ficar mais simples a insero e remoo dos registros de nossos buckets. O problema com isso que conforme aumentamos a quantidade de elementos que queremos organizar o programa fica mais lento. A verdadeira funo de ordenao aqui a funo 'radix_sort'. As outras funes fazem parte da implementao da lista. Vamos ento dar uma olhada no programa.
... int tamanho = 20; int vetor[tamanho]; int i; srand(time(NULL)); for(i = 0; i < tamanho; i++) { vetor[i] = rand() % tamanho; } ...

Simplesmente declaramos um array com 20 posies e colocamos nmeros randomicos nele que vo de 0 a 19. Esse o vetor que desejamos ordenar.
1 void radix_sort(int *vetor, int tamanho) 2 { 3 4 lista *bucket0 = NULL; 5 lista *bucket1 = NULL; 6 int i, j; 7 int numero_bits; 8 9 numero_bits = sizeof(int) * 8; 10 11 for(i = 0; i < numero_bits; i++) { // Primeiro loop 12 for(j = 0; j < tamanho; j++) { // Segundo loop 13 if((((vetor[j] >> i) & 0xf) % 2) == 0) 14 insere(&bucket0, vetor[j]); 15 else 16 insere(&bucket1, vetor[j]); 17 } 18 19 j = 0; 20 while(bucket0 != NULL) { 21 vetor[j] = pop(&bucket0);

22 23 24 25 26 27 28 29 30 31 }

j++; } while(bucket1 != NULL) { vetor[j] = pop(&bucket1); j++; } }

Como na explicao sobre o algortmo, estou usando aqui 2 buckets e o vetor com os dados. O bucket0 recebe os nmeros cujo bit atual sendo verificado 0, e o bucket 1 recebe os nmeros cujo bit atual sendo verificado 1. A varivel 'numero_bits' se refere a quantos bits ns iremos verificar para realizar a ordenao. No caso como estamos organizando nmeros inteiros, em teoria (mais tarde discutirei uma maneira melhor de determinar esse nmero) faz sentido percorremos todos os bits do mesmo. sizeof(int) retorna o nmero de bytes de um int, se multiplicamos esse valor por 8 temos o nmero de bits. O primeiro loop se refere a qual bit estamos olhando e o segundo loop passa por cada elemento para determinar se o bit 0 ou 1 e jogar o nmero em seu respectivo bucket. Acredito que a linha que mais chama a ateno aqui a linha 13. Vamos a explicao ento! Imagine que estou trabalhando com um nmero de 4 bits apenas que se encontra em vetor[0]. O que essa linha faz pegar o nmero inteiro e fazer uma lgica AND com o valor 0xf (eu poderia ter usado 0x1). Suponhamos que o numero no nosso vetor 4. Ento a lgica fica assim: Deslocar o valor dentro de vetor[0] para a direita em 0 bits (primeira interao). Em hexa: 0x4 & 0xf Em binario: 0100 & 1111 Resultado: 0100 Deslocar o valor dentro de vetor[0] para a direita em 1 bit (segunda interao). Em hexa: 0x2 & 0xf Em binario: 0010 & 1111 Resultado: 0010 Deslocar o valor dentro de vetor[0] para a direita em 2 bits (terceira interao).

Em hexa: 0x1 & 0xf Em binario: 0001 & 1111 Resultado: 0001 Deslocar o valor dentro de vetor[0] para a direita em 3 bits (quarta interao). Em hexa: 0x0 & 0xf Em binario: 0000 & 1111 Resultado: 0000 O que nos interessa aqui o bit menos significativo, ou seja, o bit mais a direita. Quando realizamos essa lgica AND, o ltimo bit ser 0 ou 1. Se o ltimo bit for 0, temos um nmero par e ento o nmero deve ser colocado no bucket0. Se o ltimo bit for 1, temos um nmero mpar e ento o nmero deve ser colocado no bucket1. O deslocamento que fazemos para a direita apenas para colocar o bit que desejamos verificar na posio menos significativa do nmero para que possamos saber se ele deve ir ao bucket0 ou ao bucket1. A lgica que segue apenas referente a 'remontar' o vetor com todos os valores que esto no bucket0 e depois todos os valores que esto no bucket1. O algortmo um pouco menos simples
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include #include #include #include <stdio.h> <stdlib.h> <time.h> <math.h>

void radix_sort(int *, int, int); void imprime(int *vetor, int tamanho); int main() { int tamanho = 20; int *vetor; int i; int maior = 0; clock_t inicio, fim; float periodo; vetor = (int *)malloc(sizeof(int)*tamanho); srand(time(NULL)); for(i = 0; i < tamanho; i++) { vetor[i] = (rand() % tamanho)+1; if(vetor[i] > maior) maior = vetor[i]; } imprime(vetor, tamanho);

28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93

printf("\n"); inicio = clock(); radix_sort(vetor, tamanho, maior); fim = clock(); periodo = (float)(((float)fim-(float)inicio)/CLOCKS_PER_SEC); printf("Tempo: %.15f\n", periodo); imprime(vetor, tamanho); printf("\n"); return 0; } void radix_sort(int *vetor, int tamanho, int maior) { int i, j, k, l; int numero_bits; int **temp; temp = (int **)malloc(sizeof(int *)*2); temp[0] = (int *)malloc(sizeof(int)*tamanho); temp[1] = (int *)malloc(sizeof(int)*tamanho); numero_bits = (log(maior)/log(2))+1; for(i = 0; i < numero_bits; i++) { k = 0; l = 0; for(j = 0; j < tamanho; j++) { if((((vetor[j] >> i) & 0xf) % 2) == 0) { temp[0][k++] = vetor[j]; } else { temp[1][l++] = vetor[j]; } } temp[0][k] = '0'; temp[1][l] = '0'; j = 0; while(temp[0][j] != '0') { vetor[j] = temp[0][j]; j++; } k=0; while(temp[1][k] != '0') { vetor[j] = temp[1][k]; j++; k++; } } } void imprime(int *vetor, int tamanho) { int i; for(i = 0; i < tamanho; i++) printf("%d ", vetor[i]); }

[radix2.c]

Consideraes:
o o
Essa implementao do algortmo apenas para nmeros maiores que 0 Para compilar com o gcc nao esquea de compilar o cdigo com -lm para a biblioteca math

As chamadas feitas para a funo clock() antes e depois da chamada de radix_sort() so para que possamos visualizar quanto tempo a execuo do algortmo levou. A primeira pequena diferena que vemos nesse cdigo na linha 12, onde agora declaramos nossa varivel vetor como um ponteiro para que possamos alocar o espao necessrio para ela em runtime. Isso se faz necessrio pois quando executarmos o programa para organizar 20 milhes de registros, no seria possvel alocar o espao necessrio na stack. A segunda pequena diferena que vemos nesse cdigo est na linha 22, onde garantimos que o nmero sorteado no mnimo 1. A terceira pequena diferena que agora ns j identificamos qual o maior nmero do nosso vetor. Isso ser til mais a frente. As grandes diferenas comeam dentro da funo radix_sort(). A primeira coisa para se notar que no usamos mais listas. Inserir e retirar items de um array mais rpido, logo isso que usaremos. Nosso bucket agora a varivel temp; temp[0] o bucket0 e temp[1] o bucket1. A segunda coisa a se notar a forma de como calculamos o numero de bits. Essa linha de extrema imoprtncia pois ela reflete diretamente na perfomance do algortmo. Suponha que estamos trabalhando em uma plataforma onde a varivel int tem 32 bits. Suponha tambm que o maior nmero que se encontra em nosso array o nmero 230. Vamos representar ento o nmero 230 em binrio: 00000000000000000000000011100110 So 32 bits, apenas 8 bits significativos e 24 bits no significativos. A partir do momento que organizamos os nmeros usando todos os bits significativos intil que faamos a verificao para os dgitos no significativos pois eles iro sempre ser colocados no bucket0 e sempre na mesma ordem. Para entender o clculo utilizado, precisamos lembrar um pouco da matemtica da escola. 2^x = 16 Quanto vale x?

log(base 2) de 16 = x x=4 Ou seja, para representarmos 16 nmeros precisamos de 4 bits. Sabemos que o a contagem para os nmeros binrios comea em 0, logo com esses 4 bits podemos contar de 0 a 15. Para que o nmero 16 fosse considerado precisamos adicionar mais 1 bit. Logo, para representar o nmero 16 temos: (log(base 2) de 16)+1 Como em C no temos uma forma de calcular log em base 2. ns usamos uma propriedade matemtica que diz: log em base x de y = (log y) / (log x) Para os que no se lembram log sem meno de base log em base 10. Voltando ento ao nosso exemplo do nmero 230. Se calcularmos: log(base 2) de 230 = x x = 7,85 No h possibilidade de termos 0.85 bit. Em C variveis int se arredondam pra baixo. Logo em nosso caso x vale 7. 2^7 = 128 No o suficiente, e matemticamente falando 7,85 seria arredondado de fato para 8. por essa razo que somamos 1. 2^8 = 256 Agora sim temos espao o bastante para o valor 230. Caso no tenha ficado muito clara a explicao, tente ler novamente e se precisar relembre um pouco de logartmos. Tenho certeza que ficar mais claro. O resto da lgica que segue basicamente o mesmo. Como precisamos de uma forma de identificar quando os nmeros de nosso bucket acabaram, eu optei por dizer que o ltimo nmero do bucket 0. por essa razo que o esse algortmo s capaz de organizar nmeros maiores que 0. Todo o resto basicamente o mesmo j apresentado antes. Observaes Se voc testou esses algortmos deve ter percebido que so

bastante rpidos. Eu quis determinar o quo rpida essa segunda implementao apresentada quando comparada ao quicksort e os resultados so impressionantes. Eis ento os algortmos usados: Quicksort
#include #include #include #include <stdio.h> <stdlib.h> <time.h> <string.h>

void quicksort(int left, int right, int *v); void swap(int i, int j, int **v); int main(int argc, char **argv) { int MAX; if(argc < 2) { printf("Use: %s numero\n", argv[0]); return -1; } MAX= atoi(argv[1]); int i; int *vetor; clock_t inicio, fim; float periodo; vetor = (int *)malloc(sizeof(int)*MAX); srand(time(NULL)); for(i = 0; i < MAX; i++) { vetor[i] = rand() % MAX; } inicio = clock(); quicksort(0, MAX-1, vetor); fim = clock(); periodo = (float)(((float)fim-(float)inicio)/CLOCKS_PER_SEC); printf("Tempo: %.15f\n", periodo); return 0; } void quicksort(int left, int right, int *v) { int pivot = v[(left+right)/2]; int i = left; int j = right; if ( left < right ) { do { while (v[i] < pivot) i++; while (v[j] > pivot) j--; if( i <= j ) {

swap(i, j, &v); i++; j--; } } while( i <= j ); quicksort(left, j, v); quicksort(i, right, v); } } void swap(int i, int j, int **v) { int aux; aux = (*v)[j]; (*v)[j] = (*v)[i]; (*v)[i] = aux; }

Radixsort
#include #include #include #include <stdio.h> <stdlib.h> <time.h> <math.h>

void radix_sort(int *, int, int); void imprime(int *vetor, int tamanho); int main(int argc, char **argv) { int tamanho; int *vetor; int i; int maior = 0; clock_t inicio, fim; float periodo; if(argc < 2) { printf("Use: %s numero\n", argv[0]); return -1; } tamanho = atoi(argv[1]); vetor = (int *)malloc(sizeof(int)*tamanho); srand(time(NULL)); for(i = 0; i < tamanho; i++) { vetor[i] = (rand() % tamanho)+1; if(vetor[i] > maior) maior = vetor[i]; } //imprime(vetor, tamanho); //printf("\n"); inicio = clock(); radix_sort(vetor, tamanho, maior); fim = clock(); periodo = (float)(((float)fim-(float)inicio)/CLOCKS_PER_SEC); printf("Tempo: %.15f\n", periodo); //imprime(vetor, tamanho);

//printf("\n"); return 0; } void radix_sort(int *vetor, int tamanho, int maior) { int i, j, k, l; int numero_bits; int **temp; temp = (int **)malloc(sizeof(int *)*2); temp[0] = (int *)malloc(sizeof(int)*tamanho); temp[1] = (int *)malloc(sizeof(int)*tamanho); numero_bits = (log(maior)/log(2))+1; for(i = 0; i < numero_bits; i++) { k = 0; l = 0; for(j = 0; j < tamanho; j++) { if((((vetor[j] >> i) & 0xf) % 2) == 0) { temp[0][k++] = vetor[j]; } else { temp[1][l++] = vetor[j]; } } temp[0][k] = '0'; temp[1][l] = '0'; j = 0; while(temp[0][j] != '0') { vetor[j] = temp[0][j]; j++; } k=0; while(temp[1][k] != '0') { vetor[j] = temp[1][k]; j++; k++; } } } void imprime(int *vetor, int tamanho) { int i; for(i = 0; i < tamanho; i++) printf("%d ", vetor[i]); }

[Radixsort] [Quicksort] Dados da mquina onde o teste foi feito: $ uname -a Linux myhost 2.6.30-ARCH #1 SMP PREEMPT Mon Aug 17

16:06:45 CEST 2009 x86_64 Intel(R) Core(TM)2 Duo CPU E7400 @ 2.80GHz GenuineIntel GNU/Linux 4GB Ram DDR2 800MHz Eis ento uma tabela comparativa do tempo de execuo dos algortmos para quando o maior valor que pode existir dentro do vetor o prprio tamanho do vetor. O tempo dado em segundos
Numero elementos Radix Sort (s) Quick Sort (s)

10

0.000000000000000

0.000000000000000

1000

0.000000000000000

0.000000000000000

100 mil

0.019999999552965

0.009999999776483

1 milho

0.310000002384186

0.219999998807907

10 milhes

3.759999990463257

2.549999952316284

100 milhes

42.380001068115234

28.530000686645508

Ao que parece, quicksort vence em todos os testes. Agora faamos a seguinte alterao nos dois cdigos: No cdigo do quicksort, trocamos
vetor[i] = rand() % MAX;

por
vetor[i] = rand() % 1023;

No cdigo do radixsort, trocamos


vetor[i] = (rand() % tamanho)+1;

por
vetor[i] = (rand() % 1022)+1;

Eis os novos resultados:


Numero elementos Radix Sort (s) Quick Sort (s)

10

0.000000000000000

0.000000000000000

1000

0.000000000000000

0.000000000000000

100 mil

0.009999999776483

0.009999999776483

1 milho

0.159999996423721

0.180000007152557

10 milhes

1.559999942779541

2.059999942779541

100 milhes

15.560000419616699

22.809999465942383

O Radixsort vence todas as comparaes! Logo, dependendo do tipo de dados que se deseja ordenar (caracteres por exemplo), o Radixsort pode ter uma performance superior a do Quicksort. As implementaes do Radixsort aqui apresentadas ainda podem ser otimizadas eu acredito, podendo apresentar uma performance ainda maior! Referncias http://www.itl.nist.gov/div897/sqg/dads/HTML/radixsort.html http://www.jimloy.com/computer/radix.htm