Projeto de Algoritmos: Mergesort

Propaganda
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
n². Vamos examinar agora um algoritmo mais complexo mas mais rápido.
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
111
q-1
333
555
555
777
999
999
q
222
r-1
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+1] = 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++];
if (v[i] > v[j]) 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
for
for
i =
for
w[MAX], i, j, k;
(i = p; i < q; i++) w[i] = v[i];
(j = q; j < r; j++) w[r+q-j-1] = v[j];
p; j = r-1;
(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 é estavel 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
for
i =
for
(i = 0, k = p; k < q; i++, k++) w[i] = v[k];
(j = r-1, k = q; k < r; j--, k++) w[j] = v[k];
0; j = r-p-1;
(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.
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 n² gasto pelos algoritmos da pagina 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 pagina anterior, ele
só é realmente mais rápido na prática quando n é grande.
Mergesort: animações
Veja algumas animações do algoritmo Mergesort:

BF e DF, na Universidade SUNY Brockport

MergeSort demo, preparada por David Neto na Universidade de Toronto em 1996.

algoritmos de ordenação, página de Pat Morin na Universidade de Carlton, Canadá

sorting demos na Universidade de British Columbia
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
111
p
999
222
999
333
p+b
888
444
p+2*b
777
555
n-1
666
555
Download