Ordenação digital

A good algorithm is like a sharp knife:
it does what it is supposed to do
with a minimum amount of applied effort.
Using the wrong algorithm to solve a problem
is like trying to cut a steak with a screwdriver:
you may eventually get a digestible result,
but you will expend considerably more effort
than necessary,
and the result is unlikely to be aesthetically pleasing.

— Th. Cormen, Ch. Leiserson, R. Rivest,
Introduction to Algorithms

Muitos algoritmos de ordenação de vetores (como o Insertionsort, o Selectionsort, o Mergesort, o Quicksort, e o Heapsort) têm por base a comparação entre os elementos do vetor. Este capítulo trata de um algoritmo de ordenação baseado na contagem dos elementos do vetor. Esse algoritmo, por sua vez, é a mola mestra do método radix sort de ordenação de strings curtas.  (O capítulo foi adaptado da seção 5.1 do livro de Sedgewick e Wayne.)

Sumário:

Vetores de inteiros pequenos

Suponha que queremos rearranjar em ordem crescente um vetor v[0 . . n−1] cujos elementos são números inteiros no intervalo

0 . . R−1

sendo R é um número pequeno.  Esse intervalo é o universo do vetor.  Depois de ordenado, o vetor terá uma carreira de elementos iguais a 0, seguida de uma carreira de elementos iguais a 1, etc., e finalmente uma carreira de elementos iguais a R−1.  A figura mostra um vetor sobre o universo 0 . . 4 antes e depois da ordenação:

2 3 3 4 1 3 0 3 1 2 2 1 2 4 3 4 4 2 3 4
0 1 1 1 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4

Exercícios 1

  1. Vetor de bits.  Escreva uma função que rearranje em ordem crescente um vetor v[0 . . n−1] cujos elementos são 0s e 1s.
  2. DNA.  Escreva uma função que rearranje em ordem crescente um vetor cujos elementos são os caracteres A, C, G e T do código genético.  Comece por transformar o conjunto  A C G T  no intervalo numérico 0 . . 3.

Ordenação por contagem

Para ordenar um vetor v[0..n-1] cujo universo é conhecido, podemos recorrer a um processo de contagem que não compara os elementos entre si. Para começar, precisamos do conceito de frequência.

A frequência de um elemento r do universo 0..R-1 é o número de ocorrências de r no vetor v[0..n-1].  Esse número será denotado por  f[r].  No vetor do exemplo acima, que tem universo 0..4, as frequências são dadas pela seguinte tabela f[0..4]:

  0 1 2 3 4
f 1 3 5 6 5

A frequência dos predecessores de um elemento r de 0..R  é a soma f[0] + ... + f[r-1], denotada por  fp[r].  Note que acrescentamos R ao universo e que a soma que define fp[r] não inclui f[r].  É claro que fp[0] é nulo.  No vetor do exemplo acima, a frequência dos predecessores é dada pela seguinte tabela:

  0 1 2 3 4 5
fp 0 1 4 9 15 20

Observe que fp[r] é o índice a partir do qual deve começar, no vetor ordenado, a carreira de elementos iguais a r.  No exemplo acima, a carreira de 1's deve começar na posição 1, a carreira de 2's deve começar na posição 4, a carreira de 3's deve começar na posição 9, etc.

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
0 1 1 1 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4

Portanto, o vetor v[0..n-1] pode ser ordenado em duas etapas:  na primeira, a tabela fp é usada para copiar os elementos de v[0..n-1] para as posições corretas de um vetor auxiliar aux[0..n-1];  na segunda, o vetor aux é copiado de volta para o espaço ocupado por v.  O seguinte código implementa o algoritmo:

// Rearranja v[0..n-1] em ordem crescente
// supondo que os elementos do vetor 
// pertencem ao universo 0..R-1.

void 
countingsort (int *v, int n, int R) 
{
   int r;
   int *fp, *aux;
   fp = malloc ((R+1) * sizeof (int));
   aux = malloc (n * sizeof (int));

   for (r = 0; r <= R; ++r) 
      fp[r] = 0;
   for (int i = 0; i < n; ++i) {
      r = v[i];
      fp[r+1] += 1; 
   }
   // agora fp[r] é a frequência de r-1
   for (r = 1; r <= R; ++r) 
      fp[r] += fp[r-1]; 
   // agora fp[r] é a freq dos predecessores
   // de r; logo, a carreira de elementos
   // iguais a r deve começar no índice fp[r]
   for (int i = 0; i < n; ++i) {
      r = v[i];
      aux[fp[r]] = v[i]; 
      fp[r]++;  // *
   }
   // aux[0..n-1] está em ordem crescente
   for (int i = 0; i < n; ++i) 
      v[i] = aux[i];

   free (fp);
   free (aux);
}

A linha * incrementa fp[r] e assim prepara o terreno para posicionar o próximo elemento do vetor que tenha valor igual a r.

Consumo de tempo.  A função countingsort consome  n + R  unidades de tempo.  Se R é pequeno (no máximo um múltiplo de n), isso é melhor que o consumo de tempo n log n de algoritmos como o Mergesort, o Quicksort, e o Heapsort.

Estabilidade.  O algoritmo que a função countingsort implementa é estável (ou seja, não altera a posição relativa de elementos de mesmo valor).  Essa propriedade é a base da aplicação da função à ordenação digital de vetores de strings a ser examinada na próxima seção.

Exercícios 2

  1. Verifique que a função countingsort está correta.
  2. Teste de correção.  Escreva um programa que teste, experimentalmente, a correção da função countingsort. (Veja exercício análogo para Insertionsort.) O seu programa poderia receber o valor do parâmetro R pela linha de comando.
  3. Critique a seguinte versão simplificada da função countingsort:
    int *f = malloc (R * sizeof (int)); 
    for (int r = 0; r < R; ++r)  f[r] = 0;
    for (int i = 0; i < n; ++i)  f[v[i]] += 1; 
    int i = 0;
    for (int r = 0; r < R; ++r) 
       for (int k = 0; k < f[r]; ++k)
          v[i++] = r;
    free (f);
    
  4. Vetor de structs.  Suponha que o vetor v[0..n-1] representa um conjunto de pontos no plano. Cada ponto é um par (x, y) de inteiros com x no intervalo 0..999. Adapte o código de countingsort para ordenar o vetor com relação à coordenada x.
  5. Vetores de letras.  Adapte a função countingsort para ordenar um vetor cujos elementos pertencem ao conjunto de caracteres A..Z.
  6. Estabilidade.  Mostre que a função countingsort é estável.

Ordenação digital de strings de mesmo comprimento

Suponha dado um vetor v[0..n-1] de strings, todas do mesmo comprimento. (Você pode imaginar que todas as strings são ASCII, mas isso não é essencial.)  Agora considere o problema de

rearranjar o vetor em ordem lexicográfica.

No contexto desse problema, os elementos (bytes) das strings são tradicionalmente chamados dígitos, mesmo que não pertençam no conjunto 0..9.

Exemplo.  Suponha que as strings são placas de licenciamento de automóveis.  (Poderíamos nos restringir aos quarenta e três caracteres que vão de '0''Z'.)  Segue um exemplo com 10 placas de 7 dígitos cada:

FON1723
EAD3312
CDA7891
FAJ4021
DOG1125
BAT7271
GIZ1234
BAT7328
BIG8733
CAT9955

(Esse exemplo é do livro de Sedgewick e Wayne.)  Para colocar esse vetor em ordem lexicográfica, podemos usar ordenação por contagem repetidas vezes:  primeiro, para ordenar pelo último dígito, depois para ordenar pelo penúltimo dígito, e assim por diante:

CDA7891  EAD3312  FAJ4021  DOG1125  CDA7891  EAD3312  BAT7271
FAJ4021  FAJ4021  DOG1125  GIZ1234  EAD3312  FAJ4021  BAT7328
BAT7271  FON1723  GIZ1234  FON1723  DOG1125  BAT7271  BIG8733
EAD3312  DOG1125  BAT7271  EAD3312  BIG8733  BAT7328  CAT9955
FON1723  BAT7328  EAD3312  FAJ4021  FAJ4021  CAT9955  CDA7891
BIG8733  BIG8733  BAT7328  BAT7271  FON1723  CDA7891  DOG1125
GIZ1234  GIZ1234  FON1723  BAT7328  BAT7271  BIG8733  EAD3312
DOG1125  CAT9955  BIG8733  CDA7891  BAT7328  GIZ1234  FAJ4021
CAT9955  BAT7271  CDA7891  BIG8733  CAT9955  DOG1125  FON1723
BAT7328  CDA7891  CAT9955  CAT9955  GIZ1234  FON1723  GIZ1234

Como a ordenação por contagem é estável, o vetor de strings acaba ficando em ordem lexicográfica.  Observe que é essencial aplicar a contagem da direita para a esquerda, ou seja, começando pelo último dígito.

Algoritmo.  O código a seguir implementa o algoritmo sugerido no exemplo.  O parâmetro W representa o comprimento comum de todas as strings (7 no exemplo acima) e o parâmetro R (menor que 256) especifica o tamanho do universo de dígitos:

typedef unsigned char byte;

// Rearranja em ordem lexicográfica um vetor v[0..n-1] 
// de strings. Cada v[i] é uma string v[i][0..W-1]
// cujos elementos pertencem ao conjunto 0..R-1.

void 
ordenacaoDigital (byte **v, int n, int W, int R) 
{
   int *fp;
   byte **aux;
   fp = malloc ((R+1) * sizeof (int));
   aux = malloc (n * sizeof (byte *));

   for (int d = W-1; d >= 0; --d) {
      int r;
      for (r = 0; r <= R; ++r) 
         fp[r] = 0;
      for (int i = 0; i < n; ++i) {
         r = v[i][d];
         fp[r+1] += 1; 
      }
      for (r = 1; r <= R; ++r) 
         fp[r] += fp[r-1]; 
      for (int i = 0; i < n; ++i) {
         r = v[i][d];
         aux[fp[r]] = v[i]; 
         fp[r]++; 
      }
      for (int i = 0; i < n; ++i) 
         v[i] = aux[i];
   }

   free (fp);
   free (aux);
}

O código ignora o byte nulo, \0, que marca o fim de cada string e portanto pode ser aplicado a qualquer vetor de vetores de bytes (desde que o valor de cada byte esteja no intervalo 0..R-1).

Esse algoritmo de ordenação é digital porque ordena as strings dígito-a-dígito.  O algoritmo também é conhecido como

Consumo de tempo.  A função ordenacaoDigital consome  W(n+R)  unidades de tempo.  Se R é pequeno (da ordem de n na pior hipótese), o consumo de tempo é proporcional a  Wn,  ou seja, ao número total de dígitos na entrada.

Animações.  Há diversas animações do algoritmo Radixsort no YouTube. Eis uma amostra:

Exercícios 3

  1. Na função ordenacaoDigital, diga o que acontece se trocarmos a linha  for (d = W-1; d >= 0; --d)  por  for (d = 0; d < W; ++d).
  2. [Sedgewick-Wayne 5.1.2]  Aplique o algoritmo de ordenação digital ao vetor de strings
    no is th ti fo al go pe to co to th ai of th pa
    
  3. [Sedgewick-Wayne 5.1.9]  Escreva uma versão de ordenacaoDigital para vetores de strings ASCII de comprimento variável.
  4. [Sedgewick-Wayne 5.1.20]  Escreva uma função que receba inteiros n e W e produza um vetor de n strings aleatórias com W caracteres ASCII cada. (Essa função gera dados de teste para ordenacaoDigital.)
  5. Teste de correção.  Escreva um programa para testar, experimentalmente, a correção da sua implementação do algoritmo de ordenacao digital. (Veja exercício análogo para Insertionsort.)
  6. Teste de desempenho.  Escreva um programa para cronometrar sua implementação do algoritmo de ordenacao digital. (Veja exercício análogo para Mergesort.)  Faça experimentos com W igual a 1, 2, 4, e 8.
  7. [Sedgewick 10.36]  Implemente o algoritmo de ordenação digital para listas encadeadas.