Projeto de Algoritmos

Mergesort: ordenação por intercalação

Nosso problema:  Rearranjar um vetor v[0 .. n-1] de tal modo que ele fique em ordem crescente, ou seja, de tal modo que tenhamos v[0] ≤ . . . ≤ v[n-1].

Já analisamos alguns algoritmos simples para o problema que consomem tempo proporcional a n2.  Vamos examinar agora um algoritmo mais complexo mas mais rápido.

Veja o verbete Merge sort na Wikipedia.

Intercalação (= merge) de vetores ordenados

Antes de resolver nosso problema principal é preciso resolver o seguinte problema auxiliar:  dados vetores crescentes v[p .. q-1] e v[q .. r-1], rearranjar v[p .. r-1] em ordem crescente.  Basta tratar do caso em que os vetores v[p .. q-1] e v[q .. r-1] não são vazios.
p           q-1 q     r-1
111 333 555 555 777 999 999 222 444 777 888

É fácil resolver o problema em tempo proporcional ao quadrado de r-p:  basta ordenar o vetor v[p..r-1] sem dar atenção ao caráter ordenado das duas "metades". Mas isso é muito lento; precisamos de um algoritmo mais rápido.

O problema é fácil, mas não é trivial. Será preciso usar um vetor auxiliar, digamos  w,  do mesmo tipo e mesmo tamanho que v[p..r-1]

// A função recebe vetores crescentes v[p..q-1] e 
// v[q..r-1] e rearranja v[p..r-1] em ordem crescente.

void 
intercala1 (int p, int q, int r, int v[]) 
{
   int i, j, k, *w;
   w = mallocX ((r-p) * sizeof (int));
   i = p; j = q;
   k = 0;

   while (i < q && j < r) {
      if (v[i] <= v[j])  w[k++] = v[i++];
      else  w[k++] = v[j++];
   }
   while (i < q)  w[k++] = v[i++];
   while (j < r)  w[k++] = v[j++];
   for (i = p; i < r; ++i)  v[i] = w[i-p];
   free (w);
}

Desempenho da intercalação

O tempo que a função consome para fazer o serviço é proporcional ao número de comparações entre elementos do vetor. Esse número é no máximo   r - p - 1 .   O consumo de tempo também é proporcional ao número de movimentações, ou seja, cópias de elementos do vetor de um lugar para outro. Esse número é igual a  2(r-p).   Resumindo, o consumo de tempo da função é proporcional ao número de elementos do vetor, ou seja,

proporcional  a   r - p .

Exercícios

  1. Analise e discuta a seguinte alternativa para a função intercala1 (a alocação e liberação de memória foram omitidas):
       i = p; j = q; k = 0;
       while (i < q && j < r) {
          if (v[i] <= v[j])  w[k++] = v[i++];
          else  w[k++] = v[j++];
       }
       while (i < q)  w[k++] = v[i++];
       while (j < r)  w[k++] = v[j++];
       for (i = p; i < r; ++i)  v[i] = w[i-p]; 
    
  2. Analise e discuta a seguinte alternativa para a função intercala1 (a alocação e liberação de memória foram omitidas):
       i = p; j = q; k = 0;
       while (i < q && j < r) {
          if (v[i] <= v[j])  w[k++] = v[i++];
          else  w[k++] = v[j++];
       }
       while (i < q)  w[k++] = v[i++];
       for (i = p; i < j; ++i)  v[i] = w[i-p]; 
    
  3. Analise e discuta a seguinte alternativa para a função intercala1 (a alocação e liberação de memória foram omitidas):
       i = p; j = q;
       for (k = 0; k < r-p; k++) {
          if (j >= r || (i < q && v[i] <= v[j])) 
             w[k] = v[i++];
          else 
             w[k] = v[j++]; 
       }
       for (i = p; i < r; ++i)  v[i] = w[i-p]; 
    
  4. Critique a seguinte alternativa para a função intercala1 (a alocação e liberação de memória foram omitidas):
       i = p; j = q; k = 0;
       while (k < r-p) {
          while (i < q && v[i] <= v[j]) 
             w[k++] = v[i++];
          while (j < r && v[j] <= v[i]) 
             w[k++] = v[j++];
       }
       for (i = p; i < r; ++i)  v[i] = w[i-p]; 
    
  5. Suponha que MAX é uma constante definida por um #define. Em que condições a seguinte implementação da função intercala1 pode ser usada?
       int w[MAX], i, j, k;
       for (i = p; i < q; i++) w[i] = v[i];
       for (j = q; j < r; j++) w[r+q-j-1] = v[j];
       i = p; j = r-1;
       for (k = p; k < r; k++)
          if (w[i] < w[j])) v[k] = w[i++];
          else v[k] = w[j--];
    
  6. Escreva uma função que receba vetores disjuntos x[0..m-1] e y[0..n-1], ambos em ordem crescente, e produza um vetor z[0..m+n-1] que contenha o resultado da intercalação dos dois vetores dados. (É claro que z estará em ordem crescente). Escreva duas versões da função: uma iterativa e uma recursiva.
  7. Um algoritmo de intercalação é estável se não altera a posição relativa de elementos iguais. A função intercala1 discutida acima é estável?  E se a comparação  "v[i] <= v[j]"  for trocada por "v[i] < v[j]"?

Intercalação com sentinelas

Sedgewick tem uma maneira mais elegante de escrever o algoritmo de intercalação.  O primeiro for copia v[p..q-1] para w[0..q-p-1];  o segundo, copia v[q..r-1] para w[q-p..r-p-1] em ordem invertida. Com isso, a intercalação de w[0..q-p-1] com w[q-p..r-p-1] pode ser feita em um único for.

// A função recebe vetores crescentes v[p..q-1] e 
// v[q..r-1] e rearranja v[p..r-1] em ordem crescente.

void
intercala2 (int p, int q, int r, int v[])
{
   int i, j, k, *w;
   w = mallocX ((r-p) * sizeof (int));

   for (i = 0, k = p; k < q; i++, k++) w[i] = v[k];
   for (j = r-1, k = q; k < r; j--, k++) w[j] = v[k];
   i = 0; j = r-p-1;
   for (k = p; k < r; k++)
      if (w[i] <= w[j]) v[k] = w[i++];
      else v[k] = w[j--];
   free (w);
}

Tal como a versão anterior, esta consome tempo proporcional a  r - p.

Exercícios

  1. [Sedgewick 8.6]  Mostre que a função intercala2 discutida acima não é estável.  Que modificações é preciso introduzir para que ela se torne estável?
  2. Listas Encadeadas.  Uma lec é uma lista encadeada sem cabeça que contém uma seqüência crescente de números inteiros. Escreva uma função que intercale duas lec dadas, produzindo assim uma terceira lec. Sua função não deve alocar novas células na memória, mas reaproveitar as células das duas listas dadas. 

Mergesort

Agora podemos usar qualquer das funções intercala discutidas acima para escrever um algoritmo rápido de ordenação: o algoritmo recebe um vetor v[p..r-1] e rearranja o vetor em ordem crescente. O algoritmo é recursivo. A base da recursão é o caso p ≥ r-1; nesse caso não é preciso fazer nada.

// A função mergesort rearranja o vetor v[p..r-1]
// em ordem crescente.

void
mergesort (int p, int r, int v[])
{
   if (p < r-1) {
      int q = (p + r)/2;
      mergesort (p, q, v);
      mergesort (q, r, v);
      intercala (p, q, r, v);
   }
}

O resultado da divisão por 2 na expressão (p+r)/2 é automaticamente truncado. Por exemplo, (3+6)/2 vale 4.  Para rearranjar v[0..n-1] em ordem crescente basta executar mergesort (0, n, v).
0 1 2 3 4 5 6 7 8 9 10
111 999 222 999 333 888 444 777 555 666 555
 
111 999 222 999 333 888 444 777 555 666 555
 
111 999 222 999 333 888 444 777 555 666 555
 
111 999 222 999 333 888 444 777 555 666 555
 
111 999 222 999 333 888 444 777 555 666 555

Mergesort: desempenho do algoritmo

Quanto tempo o algoritmo consome para ordenar v[0..n-1]?  Como o número de elementos do vetor é reduzido à metade em cada chamada do mergesort, o número total de "rodadas" é log2n. Na primeira rodada, nosso problema original é reduzido a dois outros:  ordenar  v[0 .. n/2-1]  e ordenar  v[n/2 .. n-1]. Na segunda rodada temos quatro problemas: ordenar v[0..n/4-1], v[n/4..n/2-1], v[n/2..3n/4-1] e v[3n/4..n-1]. E assim por diante. O tempo total que intercala gasta em cada "rodada" é n  (por que? pense!). Conclusão: mergesort consome tempo proporcional a

n log2n .

Isso é bem melhor que o tempo n2 gasto pelos algoritmos da página anterior. Por exemplo, se a ordenação de n números exige  t  segundos, a ordenação de  16n  números exigirá apenas  64t  segundos (contra os  256t  segundos do algoritmo anterior.)

Observação final: Como mergesort é mais complexo que os algoritmos da página anterior, ele só é realmente mais rápido na prática quando n é grande.

Mergesort: animações

Veja algumas animações do algoritmo Mergesort:

Exercícios

  1. Considere a função mergesort discutida acima. Que acontece se trocarmos "(p+r)/2" por "(p+r-1)/2"? Que acontece se trocarmos "(p+r)/2" por "(p+r+1)/2"?
  2. A função mergesort é estável?
  3. Discuta a seguinte implementação do algoritmo Mergesort:
    void mergesort1 (int p, int r, int v[]) {
        if (p < r-1) {
           int q = (p + r) / 2;
           mergesort1 (p, q, v);  
           mergesort1 (q, r, v);
           intercala (p, q+1, r, v);
        }
    }
    
  4. Discuta a seguinte implementação do algoritmo Mergesort:
    void mergesort2 (int p, int r, int v[]) {
        if (p < r) {
           int q = (p + r) / 2;
           mergesort2 (p, q, v);  
           mergesort2 (q, r, v);
           intercala (p, q, r, v);
        }
    }
    
  5. Discuta a seguinte implementação do algoritmo Mergesort:
    void mergesort3 (int p, int r, int v[]) {
        if (p < r-1) {
           int q = (p + r - 1) / 2;
           mergesort3 (p, q, v);  
           mergesort3 (q, r, v);
           intercala (p, q, r, v);
        }
    }
    
  6. Discuta a implementação do algoritmo Mergesort que está abaixo. Depois, tente mais uma versão, com (p+r+1)/2 no lugar de (p+r)/2.
    void mergesort4 (int p, int r, int v[]) {
        if (p < r-1) {
           int q = (p + r) / 2;
           mergesort4 (p, q-1, v);  
           mergesort4 (q-1, r, v);
           intercala (p, q-1, r, v);
        }
    }
    
  7. Critique a seguinte versão do Mergesort:
    void mergesort5 (int p, int r, int v[]) {
       if (p < r-1) {
          q = r - 1;
          mergesort5 (p, q, v);
          intercala (p, q, r, v);
    }
    
  8. A função mergesort acima chama as funções malloc e free muitas vezes (as chamadas acontecem dentro de intercala).  Escreva uma versão da função que contenha o código da função de intercalação e chame malloc uma só vez. 
  9. Escreva uma versão recursiva do algoritmo Mergesort que rearrange um vetor dado  v[p..r-1]  em ordem decrescente.   Sua versão deve ser integrada com o código da intercalação, ou seja, deve incluir o código de intercalação. A intercalação deve começar com  v[p..q-1]  e  v[q..r-1]  decrescentes e terminar com  v[p..r-1]  decrescente.
  10. Suponha que sua biblioteca tem uma função M (x,i,j,k) que funciona assim: recebe um vetor x tal que  x[i..j] e x[j+1..k] são decrescentes e rearranja o vetor de modo que x[i..k] fique decrescente.   Use M para escrever uma função recursiva eficiente (tão eficiente quanto M permite, é claro) que coloque um vetor x[p..r] em ordem decrescente. 

Versão iterativa do Mergesort

A versão iterativa do algoritmo Mergesort recebe um vetor v[0..n-1] e rearranja o vetor em ordem crescente. A idéia é muito simples: a cada iteração, intercalamos dois "blocos" com  b  elementos cada: o primeiro bloco com o segundo, o terceiro com o quarto, etc. A variável b assume os valores 1, 2, 4, . . . .

// Esta função rearranja o vetor v[0..n-1] 
// em ordem crescente.

void
mergesort_i (int n, int v[])
{
   int p, r;
   int b = 1;
   while (b < n) {
      p = 0;
      while (p + b < n) {
         r = p + 2*b;
         if (r > n) r = n;
         intercala (p, p+b, r, v);
         p = p + 2*b; 
      }
      b = 2*b;
   }
}

A figura ilustra a iteração b == 2.
0 p p+b p+2*b n-1
111 999 222 999 333 888 444 777 555 666 555

Exercícios

  1. A versão iterativa do Mergesort começa por quebrar o vetor original em segmentos de comprimento 1.  Por que não começar com segmentos crescentes maximais?  Exemplo: os segmentos crescentes maximais do vetor  1 2 3 0 2 4 6 4 5 6 7 8 9  são

    1 2 3  ,    0 2 4 6    e    4 5 6 7 8 9 .

    Explore esta idéia. 

Mais exercícios

  1. Listas Encadeadas.  Escreva uma versão do algoritmo Mergesort que rearranje uma lista encadeada em ordem crescente. Sua função não deve alocar novas células na memória. 

 


URL of this site: www.ime.usp.br/~pf/algoritmos/
1998 | Last modified: Wed Jan 3 06:38:04 BRST 2007
Paulo Feofiloff
IME-USP

Valid HTML 4.0!     Valid CSS!