Ordenação: Mergesort

Esta página mostra como calcular o consumo de tempo do algoritmo de ordenação-por-intercalação conhecido como Mergesort. O algoritmo é recursivo, e portanto é preciso resolver uma recorrência para estimar seu consumo de tempo.

Esta página é inspirada na segunda parte do capítulo 2 do CLRS.

Ordenação por intercalação

Considere o clássico problema de ordenar um vetor A[1 .. n]. Nesta página, convém trocar o primeiro índice do vetor por uma variável e apresentar o problema assim:

Problema da ordenação:  Rearranjar um vetor A[p .. r] de modo que ele fique em ordem crescente.

O algoritmo Mergesort usa a estratégia da divisão e conquista para resolver o problema. O algoritmo recursivo resultante supõe que pr e adota o caso p = r como base da recursão:

Mergesort (A, p, r)
1 . se p < r
2 .ooo q := ⌊(p+r)/2⌋
3 .ooo Mergesort (A, p, q)
4 .ooo Mergesort (A, q+1, r)
5 .ooo Intercala (A, p, q, r)

Depois da linha 2, temos p ≤ q < r. Com isso, tanto A[p .. q] quanto A[q+1 .. r] são estritamente menores que A[p .. r]. Isso garante que a execução do algoritmo termina, mais cedo ou mais tarde.

Exemplo A:  A primeira figura mostra o estado do vetor no início da linha 5. A segunda mostra o estado do vetor no fim da linha 5.
p       q q+1       r
110 130 150 170 190 100 120 140 160 180
p       q q+1       r
100 110 120 130 140 150 160 170 180 190

O algoritmo Intercala recebe vetores crescentes A[p .. q] e A[q+1 .. r] e rearranja o vetor A[p .. q.. r] de modo que ele fique crescente. (Veja exercício no Apêndice). Para fazer isso, poderíamos ignorar o caráter ordenado dos dois subvetores e usar o algoritmo de inserção. Mas é possível fazer algo mais eficiente se tivermos permissão para usar um espaço de rascunho B[p .. r]:

Intercala (A, p, q, r)
16 . para i := p até q
17 .ooo B[i] := A[i]
18 . para j := q+1 até r
19 .ooo B[r+q+1−j] := A[ j]
10 . i := p
11 . j := r
12 . para k := p até r
13 .ooo se B[i] ≤ B[ j]
14 .oooooo A[k] := B[i]
15 .oooooo i := i+1
16 .ooo senão A[k] := B[ j]
17 .ooo senão j := j−1

Quanto tempo Intercala consome para processar um vetor com n = r − p + 1 elementos? Suponha, para simplificar, que cada execução de cada linha do código consome 1 unidade de tempo. Então a execução do algoritmo consumirá

6n + 5

unidades de tempo. (Se adotarmos um critério diferente de 1-unidade-de-tempo-por-linha, as contantes 6 e 5 podem mudar, mas o termo n certamente permanecerá.) Portanto, o consumo de tempo está em Θ(n) e o algoritmo é linear.

Exercícios 1

  1. Diga o que o algoritmo Mergesort faz.
  2. É verdade que ⌊(p + r)/2⌋ = p + ⌊(r − p)/2⌋ ?
  3. O algoritmo Mergesort usa o método de divisão e conquista. No que consiste a fase de divisão do Mergesort? No que consiste a fase de conquista? No que consiste a fase de combinação? Dê respostas breves e informais.
  4. ★ Confira a afirmação de que Intercala consome 6n + 5 unidades de tempo.
  5. Quanto tempo Intercala consome no melhor caso? E no pior caso?
  6. Incorpore o código de Intercala ao código de Mergesort, ou seja, reescreva Mergesort substituindo a invocação de Intercala por seu código. Faça os ajustes necessários.
  7. ★ Seja n o número de elementos do vetor A[p .. r]. Mostre que na linha 3 o vetor A[p .. q] tem n/2⌉ elementos. Mostre que na linha 4 o vetor A[q+1 .. r] tem n/2⌋ elementos.
  8. ★ Reescreva algoritmo Mergesort depois de trocar a linha 2 por q := ⌈(p+r)/2⌉.
  9. Número de comparações na intercalação. Seja n o número rp+1 e C(n) o número total de execuções da comparação B[i] ≤ B[ j] na linha 13 do subalgoritmo Intercala. Mostre que C(n) = n. Observe que C(n) é proporcional ao consumo de tempo do subalgoritmo.

Desempenho do Mergesort

Seja n o número rp+1T(n)  o tempo que Mergesort consome para processar o vetor A[p .. r]. (O algoritmo não tem pior caso: todos os casos consomem essencialmente o mesmo tempo.) Suponha que cada execução das linhas 1 a 2 consome 1 unidade de tempo. A linha 5 consome 6n + 5 unidades de tempo, como vimos acima.

Mergesort (A, p, r)    
1 . se p < r   1
2 .ooo q := ⌊(p+r)/2⌋   1
3 .ooo Mergesort (A, p, q)   T(⌈n/2⌉)
4 .ooo Mergesort (A, q+1, r)   T(⌊n/2⌋)
5 .ooo Intercala (A, p, q, r)   6n + 5

Portanto, o consumo de T(n) do algoritmo satisfaz a recorrência

  T(n) = T(⌈n/2⌉) + T(⌊n/2⌋) + 6n + 7 (*)

para todo n > 1. Se adotarmos o valor inicial T(1) = 1, teremos a seguinte tabela para valores pequenos do argumento:

n) 1 2 3 4 5
T(n) 1 21 47 73  105

Como é possível extrair daí uma fórmula fechada para T(n)?

Solução da recorrência.  Se ficarmos só com os valores de n que são potências inteiras de 2, a recorrência se reduz a  T(n) = 2 T(n/2) + 6n + 7. Um cálculo semelhante ao que fizemos no exemplo D da página Recorrências mostra que  T(n) = 6 nlg n + 8n − 7  para n = 2, 4, 8, 16, etc. Esta fórmula só vale para potências inteiras de 2, mas deixa no ar a suspeita de que, para qualquer n, o tempo do Mergesort é Ο(n lg n). Poderíamos recorrer ao Teorema Mestre para confirmar a suspeita, mas prefiro verificar isso diretamente. Vou mostrar que

T(n)  ≤  16 n lg n

para n = 2, 3, 4, 5, 6, …  (A desigualdade não vale quando n = 1. Para determinar o fator 16, comecei fazendo os cálculos com cn lg n e acabei concluindo que 16 era um bom valor para c.)

Prova:  Quando n = 2, a desigualdade vale pois T(n) = T(2) = 21 < 32 = 16⋅2⋅1 = 16⋅2⋅lg 2 = 16 n lg n. Quando n = 3, a desigualdade vale pois T(n) = T(3) = 47 < 48 = 16⋅3 < 16⋅3⋅lg 3 = 16 n lg n. (Não é possível cuidar do caso n = 3 no passo da indução porque T(3) depende de T(1) e a hipótese de indução não pode ser aplicada a T(1).)

Suponha agora que n > 3 e que a desigualdade que queremos provar vale para argumentos de T menores que n. Então

T(n) = T(⌈n/2⌉) + T(⌊n/2⌋) + 6n + 7
  16 ⌈n/2⌉ lg ⌈n/2⌉ + 16 ⌊n/2⌋ lg ⌊n/2⌋ + 6n + 7
  16 (n/2⌉ + ⌊n/2⌋) lg ⌈n/2⌉ + 6n + 7
  16 n lg (2n/3) + 6n + 7
  = 16 n(lg n + lg (2/3)) + 6n + 7
  < 16 n(lg n − 0.5) + 6n + 7
  = 16 n lg n − 8n + 6n + 7
  = 16 n lg n − 2n + 7
  16 n lg n ,  CQD.

Um dos passos da prova depende de saber que lg (2/3) < −0.5894. O último passo segue da desigualdade −2n + 7 ≤ 0, válida para n > 3. Isso termina a prova de que T(n) ≤ 16 n lg n para todo natural n ≥ 2.

É um ótimo exercício mostrar que existe um número positivo c tal que T(n) ≥ cn lg n para todo n suficientemente grande. Isso leva à conclusão de que

T(n) = Θ(n lg n) .

Portanto, o algoritmo Mergesort é linearítmico.

Se deixarmos de lado a hipótese artificial de que cada execução de cada linha simples do algoritmo, consome 1 unidade de tempo, a recorrência (*) pode ser escrita como  T(n) = T(⌈n/2⌉) + T(⌊n/2⌋) + Θ(n). A solução dessa recorrência, está em Θ(n lg n).

Exercícios 2

  1. Prove que n/2⌋ + ⌈n/2⌉ = n para todo número natural n.
  2. Mostre que o consumo de tempo do Mergesort está em Ω(n lg n).
  3. Seja F a função definida pela recorrência  F(n) = 2 F(n/2) + 6n + 7  (para n = 2, 4, 8, 16, etc.) e pelo valor inicial F(1) = 1. Prove, por indução em n, que F(n) = 6 n lg n + 8n − 7 para n = 2, 4, 8, 16, etc.
  4. Seja T a função definida pela recorrência (*) com T(1) = 1. Mostre que T(n) ≥ 2 n lg n para todo número natural n ≥ 2. Deduza daí que T(n) = Ω(n lg n).  (É um bom exercício tentar mostrar também que T(n) ≥ 3 n lg nn para todo n ≥ 2.)
  5. Considere a recorrência  T(n) = T(⌈n/2⌉) + T(⌊n/2⌋) + 7 n + 2  para todo n natural maior que 1. (Essa recorrência é apenas um pouco diferente da recorrência (*).) Seja T uma solução da recorrência com valor inicial T(1) = 1. Prove que T(n) ≤ 30 n lg n para todo natural n ≥ 4.
  6. Número de comparações. Considere uma execução do algoritmo Mergesort com argumentos (A, p, r). Seja n o número rp+1 e F(n) o número total de execuções da comparação B[i] ≤ B[ j] na linha 13 do subalgoritmo Intercala em todas as execuções do subalgoritmo. (Veja o execício Número de comparações na intercalação). Note que o consumo de tempo de Mergesort é proporcional a F(n). Exiba a recorrência que F(n) satisfaz.
  7. ★ Resolva a recorrência do exercício anterior para mostrar que  F(n) = n lg n quando n é potência inteira de 2. Mostre, diretamente a partir da recorrência, que  F(n) ≤ 2 n lg n para todo natural n ≥ 2.  (Tente mostrar que  F(n) ≤ n lg n + n para todo n ≥ 2.) 
  8. Versão iterativa.  Escreva e analise uma versão não recursiva do algoritmo Mergesort.  (Podemos dizer que essa versão não recursiva é ascendente, ou bottom-up, enquanto a versão recursiva é descendente, ou top-down.)
  9. ★ Sabe-se que um certo algoritmo para o problema da ordenação consome Θ(n lg n) unidades de tempo para processar um vetor com n elementos. Quanto tempo o algoritmo consome para processar um vetor com 1024 elementos?
  10. É dado um número inteiro s e um vetor A[1 .. n] de números inteiros. Quero saber se existem dois elementos do vetor cuja soma é exatamente s. Dê um algoritmo que resolva o problema em tempo Ο(n lg n).