Italo Info


Merge Sort - Ordenação por intercalação

O mergesort é um algoritmo de ordenação por intercalação que utiliza a técnica de divisão e conquista, um método recursivo de ordenação onde se busca dividir o problema principal em subproblemas semelhantes de modo que esses subproblemas são resolvidos recursivamente para, então, ser resolvido o problema principal. Veja a seguir uma ilustração de como funciona a divisão, conquista e intercalação no mergesort:

Abaixo, a arvore que ilustra a divisão do arranjo principal em subarranjos recursivamente de cima para baixo na árvore:

Mergesort - divisão
Mergesort - divisão

Abaixo, a arvore que ilustra a conquista e intercalação ordenada dos arranjos menores até cada arranjo, um nível acima na arvore, estar completamente ordenado.

Analise a árvore abaixo de baixo para cima!

Mergesort - intercalação
Mergesort - intercalação

A intercalação

Para o mergesort funcionar bem, é necessária uma solução eficiente de intercalação (Ou merge). Veja abaixo um exemplo de intercalação e, mais uma vez, analise de baixo para cima:

Mergesort - intercalação
Intercalação

Perceba que os dois vetores de 3 elementos ordenados são intercalados de modo a formar o vetor principal com os 6 elementos ordenados. Para melhor compreensão do processo de intercalação, vamos à um exemplo:

Imagine os 2 seguintes arranjos:

    A=[3, 6, 9]
    B=[1, 3, 7]

O objetivo é intercalá-los para compor o vetor principal C.

Primeiramente, podemos entender que o vetor C resultante terá o número de elementos de A somado ao número de elementos de B. Então vamos lá!

O vetor C deve corresponder ao arranjo C abaixo:

    A=[3, 6, 9]
    B=[1, 3, 7]
    C=[1, 3, 3, 6, 7, 9]

A lógica é a seguinte: Compara-se A[0] com B[0]. Como B[0] é menor, o C[0]=B[0]. Em seguida, o número de B a ser comparado passa a ser o próximo, dado que o primeiro já foi colocado em C. Então, compara-se A[0] com B[1]. Como são iguais, se pode pegar o primeiro: A[0] para ser colocado em C. Logo, C[1]=A[0] e o próximo elemento de A a ser comparado é o A[1]. Feito isso, compara-se A[1] com B[1]. B[1] é menor e, portanto, este é que é colocado em C. Logo, C[2]=B[1]. Então, compara-se A[1] com B[2]. A[1] é menor. Logo, C[3]=A[1]. Então, compara-se A[2] com B[2] e, então, C[4]=B[2]. Como agora todos os números do vetor B já foram inseridos em C, se pode inserir o restante dos números em A. No caso, resta apenas A[2]. Logo, C[5]=A[2].

Veja outro jeito de entender a intercalação em C:

    C[0]=B[0]
    C[1]=A[0]
    C[2]=B[1]
    C[3]=A[1]
    C[4]=B[2]
    C[5]=A[2]

E se o arranjo fosse como abaixo:

    A=[1, 8, 9]
    B=[8, 9, 10]

Então a intercalação acontece conforme abaixo:

    C[0]=A[0]
    C[1]=A[1]
    C[2]=B[0]
    C[3]=A[2]
    C[4]=B[1]
    C[5]=B[2]  

Logo, o vetor intercalado C é:

    C=[1, 8, 8, 9, 9, 10]

O pseudocódigo do merge

 merge( C, p, q, r )     
   n1=q-p+1              
   n2=r-q                
   A=[]                  
   B=[]                  
   for k = 0 to n1-1 do  
     A[k] = C[p+k]       
   for k = 0 to n2-1 do  
     B[k] = C[q+1+k]     
   A[n1]=∞         
   B[n2]=∞         
   i=0                   
   j=0                   
   for k = p to r do     
     if ( A[i] <= B[j] ) 
       C[k] = A[i]       
       i++               
     else                
       C[k]=B[j]         
       j++               

O algoritmo recebe o vetor C de modo que C[p..q] corresponde ao vetor A na explicação anterior e C[q+1..r] corresponde ao vetor B. Ambos A e B ordenados. Então, pelo algoritmo representado acima, primeiro, calcula-se a quantidade de elementos de C que correspondem ao vetor A e a quantidade de elementos de C que correspondem ao vetor B. Feito isso, entre as linhas 6 e 9, os valores C[p..q] são colocados em A e os valores C[q+1, r] são colocados em B.

Nas linhas 10 e 11, perceba que é acrescentado, tanto em A, quanto em B, mais um elemento com valor ∞. Assim, o tamanho do vetor A, passa a ser n1+1 e o tamanho do vetor B, passa a ser n2+1.

A partir da linha 14 está o conteúdo do for que realiza a intercalação. Ou seja, dado que n1+n2 é igual a r-p+1, se pode garantir que, a cada iteração, o valor de C[k] recebe um dos elementos de A[0..n1-1] ou um dos elementos de B[0..n2-1].

Outro detalhe importante é a utilidade de se colocar valores infinitos ao final dos vetores A e B. Isto é, se por exemplo, todos os valores de A, com exceção de ∞ forem inseridos em C, o restante dos elementos de B, com exceção de ∞ são menores que ∞. Logo, o restante dos elementos de B, B[0,n2-1], são inseridos. Isso porque, ao comparar os elementos de B restantes com o elemento ∞ de A, os elementos de B são menores e, assim, são todos colocados ao final do vetor C, com exceção do valor ∞

Como o algoritmo não tem sequer laços aninhados, e o terceiro for tem maior custo computacional de tempo que os outros. Logo, é facil perceber que a ordem de crescimento corresponde a θ(n)=n, onde n = r-p+1.


Pseudocódigo do mergesort

  mergesort( A, p, r )         
    if ( p < r )               
      q = p + (( r-p )/2);     
      mergesort( A, p, q );    
      mergesort( A, q+1, r );  
      merge( A, p, q , r );    

Agora suponha A o arranjo de números a serem ordenados e o número de elementos em A armazenado na variável 'n'. Então o mergesort deve ser chamado como a seguir:

  mergesort( A, 0, n-1 )  

Em pascal, por exemplo, caso o vetor A tenha seus índices definidos entre [1..n] então a chamada ficaria como a seguir:

  mergesort( A, 1, n )  

Explicação do algoritmo

Dado o algoritmo de ordenação representado em pseudocódigo, se pode facilmente perceber que a função mergesort faz internamente duas chamadas recursivas a ela mesmo.

Não é complicado entender a ideia básica do mergesort. As duas chamadas recursivas são porque o problema maior é resolvido dividindo-se ele em dois problemas menores e esses dois problemas menores podem ainda serem divididos cada um em mais dois problemas menores, conforme uma arvore que pode ilustrar essas chamadas recursivas já listada anteriormente.

Então, primeiro é calculado o valor mediano chamado de 'q' que está no meio entre 'p' e 'r'. Aqui fica evidenciada a idéia de difidir o arranjo A em dois de tamanho iguais (ou o primeiro com, no máximo, um elemento a mais que o segundo). Logo, se o vetor A tiver 6 elementos: primeiro, ordena-se os três primeiros elementos (primeira chamada recursiva), e depois, os três últimos (segunda chamada recursiva) para, então, intercalar (função merge) os dois subarranjos e formar o vetor A ordenado.

O número 'q' corresponde ao índice que divide esse arranjo nas suas duas partes menores, e o IF, é a condição de parada. Uma condição de parada é necessária a toda solução recursiva para evitar loop infinito e a parada do mergesort é quando o p e r são iguais. Isto é, indices do mesmo elemento do Arranjo A.


O cálculo da complexidade do algoritmo

Para analizar a complexidade da função mergesort, podemos escrever uma recorrência em função de 'n' como a seguir:

Mergesort - recorrencia
Recorrência

Conforme a imagem acima, se pode considerar que: se 'n'=1, isto é houver apenas um elemento para ser ordenado, a verificação da condição do if que pode ter uma ordem de crescimento θ(1) e é executada uma vez, e considerada falsa e o bloco de código do IF não é executado.

Se 'n'>1, então o vetor é dividido em 2 de tamanho aproximado à n/2 cada. Logo, são duas cmamadas recursivas à T(n/2) somada a operação de merge que tem ordem de crescimento igual a θ(n).

Se considerarmos que θ(1) corresponde a um valor constante qualquer e θ(n) é igual a 'n' vezes esse valor qualquer, então, caso esse valor qualquer seja representado por uma constante 'c', a recorrência pode ser expressa como a seguir:

Mergesort - recorrencia
Recorrência

Para facilitar a compreensão, vamos pegar um exemplo de vetor a ser ordenado cuja árvore que representa as chamadas recursivas do mergesort tem profundidade 4. Isto é, 'n' (tamanho do vetor) igual a 8. Então, analise a figura abaixo:

Mergesort - ordem de crescimento
Ordem de crescimento

Pela figura acima, se pode notar que o vetor de 8 elementos é dividido em 2 de tamanho 4 = 8/2, logo após, cada um dos vetores de tamanho 4, são divididos 2 vetores de tamanho 2 = 4/2 e cada vetor de tamanho 2 é dividido em 2 vetores de tamanho 1 = 2/2. A constante c corresponde ao tempo de execução de uma instrução qualquer com ordem de crescimento θ(1) e ela multiplica o novo número de elementos de cada vetor.

Somando todos os valores da árvore, se consegue obter o valor 4cn. Logo, 4 corresponde a altura da árvore e se utilizarmos esse valor em função da altura, ele pode ser substituído por 'log 8 + 1' (logarítmo na base 2). O 8 corresponde ao número de elementos, logo, o tempo em função de n fica como a seguir:

Mergesort - Tempo de execução
Tempo de execução

Como c é constante e 'cn*log(n)' tem ordem maior que 'cn', então a ordem de crescimento do algoritmo pode ser expressa como abaixo:

Mergesort - ordem de crescimento
Ordem de crescimento

Conclusão

Esse foi o artigo escrito sobre o mergesort, um algoritmo de intercalação mais rápido se comparado com algoritmos de ordenação de ordem θ(n²) e que utiliza a técnica de divisão e conquista para ordenar um Arranjo de elementos. No entanto, há uma limitação de memória, dado que, é necessário mais dois vetores auxiliares de tamanho (n/2 + 2), onde n é o tamanho do arranjo de elementos principal.

Como recurso auxiliar do aprendizado do algoritmo mergesort, eu preparei um programinha escrito em linguagem C que implementa o algoritmo e mostra o resultado do vetor ordenado. Faça o download logo abaixo:

Link: mergesort.c

O CodeBlocks foi utilizado como IDE para desenvolvimento do programa ordenação acima.

E, é isso pessoal, até a próxima!

Inspirado em

Cormen. H. Thomas et. al, 2002 - Algoritmos teoria e prática - 2 ed. Rio de Janeiro - RJ, Brasil, 2002