algoritmos e estruturas de...
TRANSCRIPT
Algoritmos
e Estruturas de Dados
Décima sexta aula:
Quicksort
02-07-2012 Algoritmos e Estruturas de Dados I - 16 © Pedro Guerreiro 2
Nesta aula vamos…
• Estudar o quicksort.
• Considerar algumas variantes:
Quicksort geral, parametrizando a função de comparação.
Quicksort com partição de Hoare.
Quicksort com partição de Lomuto.
Quicksort iterativo, com pilha.
Quicksort com pivô aleatório.
Quicksort com pivô mediana de três.
Quicksort com cutoff.
02-07-2012 Algoritmos e Estruturas de Dados I - 16 © Pedro Guerreiro 3
Quicksort
clássico
• Função principal:
void qs (int *a, int x, int y) { int i = x; int j = y; int p = a [(i+j)/2]; do { while (a[i] < p) i++; while (p < a[j]) j−−; if (i <= j) { int m = a[i]; a[i] = a[j]; a[j] = m; i++; j−−; } } while (i <= j); if (x < j) qs (a, x, j); if (i < y) qs (a, i, y); }
void quicksort (int *a, int n) { if (n > 0) qs (a, 0, n−1); }
O quicksort é uma das obras-primas
da programação. Esta é a versão
original de Hoare, tal como
publicada por Wirth no livro
Algorithms + Data Structures =
Programs, aqui adaptada para C.
Primeira parte, partição • Reorganizar os elementos de um vector de
maneira a que todos os que são menores ou
iguais ao elemento de índice médio venham
antes de todos os restantes, isto é, de todos
os que são maiores ou iguais a esse elemento
de índice médio.
02-07-2012 Algoritmos e Estruturas de Dados I - 16 © Pedro Guerreiro 4
[17, 4, 13, 6, 0, 12, 18, 0, 13, 14, 1, 19]
[1, 4, 0, 6, 0, 12, 18, 13, 13, 14, 17, 19]
[14, 13, 9, 1, 0, 3, 6, 13, 19, 10, 11, 7]
[3, 0, 1, 9, 13, 14, 6, 13, 19, 10, 11, 7]
Segunda parte,
continuar recursivamente
• Isto é, aplicar a partição a cada uma das
“metades” do vector:
02-07-2012 Algoritmos e Estruturas de Dados I - 16 © Pedro Guerreiro 5
[17, 4, 13, 6, 0, 12, 18, 0, 13, 14, 1, 19]
[1, 4, 0, 6, 0, 12, 18, 13, 13, 14, 17, 19]
[1, 4, 0, 6, 0, 12, 18, 13, 13, 14, 17, 19]
[0, 0, 4, 6, 1, 12, 18, 13, 13, 14, 17, 19]
[0, 0, 1, 4, 6, 12, 18, 13, 13, 14, 17, 19]
[0, 0, 1, 4, 6, 12, 13, 13, 18, 14, 17, 19]
Note bem: por via da aplicação recursiva, quando
chegarmos à segunda metade, já a primeira estará
ordenada!
Como fazer a partição
1. Selecionar o elemento de índice médio, chamado
pivô. (Dizemos que é o pivô central.)
2. Percorrer o vector da esquerda para a direita até
encontrar um elemento maior ou igual ao pivô.
3. Percorrer o vector da direita para a esquerda até
encontrar um elemento menor ou igual ao pivô.
4. Trocar os elementos selecionados nos passos 2 e 3,
se o primeiro ainda estiver à esquerda do segundo.
5. Repetir a partir de 2, continuando a partir das
posições seguintes, até os percursos se cruzarem.
02-07-2012 Algoritmos e Estruturas de Dados I - 16 © Pedro Guerreiro 6
02-07-2012 Algoritmos e Estruturas de Dados I - 16 © Pedro Guerreiro 7
Outras ordenações
• Tal como está programado na página
anterior, o quicksort ordena um vector de
números por ordem ascendente.
• Se quisermos outras ordenações, temos
de programar uma nova função quicksort
ad-hoc.
• Ou então parametrizar a função de
comparação.
02-07-2012 Algoritmos e Estruturas de Dados I - 16 © Pedro Guerreiro 8
Quicksort
geral
• Função principal:
void qs_general (int *a, int x, int y, int cmp (int, int)) { int i = x; int j = y; int p = a [(i+j)/2]; do { while (cmp (a[i], p) < 0) i++; while (cmp (p, a[j]) < 0) j−−; if (i <= j) { int m = a[i]; a[i] = a[j]; a[j] = m; i++; j−−; } } while (i <= j); if (x < j) qs_general (a, x, j, cmp); if (i < y) qs_general (a, i, y, cmp); }
void quicksort_general ( int *a, int n, int cmp (int, int)) { if (n > 0) qs_general (a, 0, n−1, cmp); }
É geral, porque permite usar
qualquer função de
comparação, mas não é
genérico. Ser genérico
significa poder ser usado
com vectores com
elementos de qualquer tipo,
o que não é o caso aqui.
02-07-2012 Algoritmos e Estruturas de Dados I - 16 © Pedro Guerreiro 9
Ordenação por partição • O quicksort representa o método de
ordenação por partição:
• Primeiro “parte-se” o vector em dois subvectores: o dos elementos “pequenos” (isto é, menores ou iguais ao pivô) e o dos elementos grandes (maiores ou iguais ao pivô).
• Depois aplica-se o algoritmo recursivamente a ambas as “partes”.
• O algoritmo de partição é interessante por si só, e merece ser autonomizado.
02-07-2012 Algoritmos e Estruturas de Dados I - 16 © Pedro Guerreiro 10
Pair hoare (int *a, int x, int y) { Pair result; int i = x; int j = y; int p = a [(i+j)/2]; do { while (a[i] < p) i++; while (p < a[j]) j−−; if (i <= j) { int m = a[i]; a[i] = a[j]; a[j] = m; i++; j−−; } } while (i <= j); result.first = j; result.second = i; return result; }
Partição de Hoare O resultado é um par de índices:
typedef struct { int first; int second; } Pair;
void qs_hoare (int *a, int x, int y) { Pair r = hoare (a, x, y); if (x < r.first) qs_hoare (a, x, r.first); if (r.second < y) qs_hoare (a, r.second, y); }
void quicksort_hoare (int *a, int n) { if (n > 0) qs_hoare (a, 0, n−1); }
Função principal.
Função
recursiva.
02-07-2012 Algoritmos e Estruturas de Dados I - 16 © Pedro Guerreiro 11
Partição de Lomuto • O livro Introduction to Algorithms usa uma
partição diferente da de Hoare, muito
interessante também, chamada partição de
Lomuto: int lomuto (int *a, int x, int y) { int p = a [y]; int result = x; int i; for (i = x; i < y; i++) if (a[i] < p) numbers_swap (a, result++, i); numbers_swap (a, result, y); return result; }
Veja a explicação na
página seguinte.
02-07-2012 Algoritmos e Estruturas de Dados I - 16 © Pedro Guerreiro 12
Explicação • O pivô (a azul) é o último
elemento (está na posição y). Em cada momento, os elementos nas posições [x..result[ (a amarelo) são menores ou iguais ao pivô e os nas posições [result..i[ (a verde) são maiores do que o pivô. Os outros (nas posições [i..y[, a cor-de-rosa) não sabemos. No final (depois do ciclo) trocamos o elemento na posição result com o pivô, o que garante que o pivô já encontrou a sua posição definitiva, e que o vector está partido.
3 7 2 9 5 8 1 7 4 5
3 7 2 9 5 8 1 7 4 5
3 7 2 9 5 8 1 7 4 5
3 2 7 9 5 8 1 7 4 5
3 2 7 9 5 8 1 7 4 5
3 2 5 9 7 8 1 7 4 5
3 2 5 9 7 8 1 7 4 5
3 2 5 1 7 8 9 7 4 5
3 2 5 1 7 8 9 7 4 5
3 2 5 1 4 8 9 7 7 5
3 2 5 1 4 5 9 7 7 8
02-07-2012 Algoritmos e Estruturas de Dados I - 16 © Pedro Guerreiro 13
Quicksort de Lomuto
• Observe:
void qs_lomuto (int *a, int x, int y) { int r = lomuto (a, x, y); if (x < r−1) qs_lomuto (a, x, r−1); if (r+1 < y) qs_lomuto (a, r+1, y); }
Fica um pouco mais simples, porque o
resultado da partição é um número inteiro,
representando a posição onde fica o pivô
depois da troca final, e não um par de
inteiros, como na partição de Hoare.
02-07-2012 Algoritmos e Estruturas de Dados I - 16 © Pedro Guerreiro 14
Podemos evitar
as chamadas
recursivas,
empilhando “à
mão” os
argumentos
para posterior
processamento:
void quicksort_iterative_lomuto (int *a, int n) { Stack s = stack_init (); stack_push (&s, 0); stack_push (&s, n−1); while (!stack_empty (s)) { int x; int y; int r; y = stack_top (s); stack_pop (&s); x = stack_top (s); stack_pop (&s); r = lomuto (a, x, y); if (r+1 < y) { stack_push (&s, r+1); stack_push (&s, y); } if (x < r−1) { stack_push (&s, x); stack_push (&s, r−1); } } }
Quicksort iterativo
Empilhamos
aos pares, da
direita para
esquerda, para
ao desempilhar,
sair da
esquerda para
a direita.
02-07-2012 Algoritmos e Estruturas de Dados I - 16 © Pedro Guerreiro 15
Pivô aleatório • Uma má escolha do pivô pode comprometer o
desempenho do quicksort.
• Para evitar casos particulares desagradáveis,
podemos “randomizar” o pivô.
• Por exemplo, no quicksort de Lomuto, basta
substituir a partição por esta:
int lomuto_with_random_pivot (int *a, int x, int y)
{
numbers_swap (a, x + rand_to (y−x), y);
return lomuto (a, x, y);
} Troca-se o último com um
elemento escolhido aleatoria-
mente. Este será o pivô.
02-07-2012 Algoritmos e Estruturas de Dados I - 16 © Pedro Guerreiro 16
Pivô mediana de 3
• Escolher para pivô o mínimo ou o máximo do
vector é mau, porque todos os elementos vão
para a mesma partição.
• Evita-se isso usando para pivô a mediana do
conjunto formado pelo primeiro elemento, pelo
elemento central e pelo último elemento.
• Aliás, ordena-se estes três elementos, sobre o
vector e depois escolhe-se o elemento central,
que, depois da ordenação, será a mediana
dos 3.
02-07-2012 Algoritmos e Estruturas de Dados I - 16 © Pedro Guerreiro 17
• Usa-se uma espécie de
bubblesort, à mão.
• Programamos assim, por
extenso, em vez de usar
a função swap (que troca
o valor de duas
variáveis), para evitar
três chamadas de função
suplementares, por cada
chamada recursiva do
quicksort.
void sort_3 (int *x, int *y, int *z)
{
if (*y > *z)
{
int m = *y;
*y = *z;
*z = m;
}
if (*x > *y)
{
int m = *x;
*x = *y;
*y = m;
}
if (*y > *z)
{
int m = *y;
*y = *z;
*z = m;
}
}
Ordenando 3
Comprido
e chato.
02-07-2012 Algoritmos e Estruturas de Dados I - 16 © Pedro Guerreiro 18
Quicksort mediana de 3 void qs_median_of_3 (int *a, int x, int y)
{
sort_3 (a+x, a+(x+y)/2, a+y);
if (y − x > 2)
{
Pair r = hoare (a, x+1, y−1);
if (x < r.first)
qs_median_of_3 (a, x, r.first);
if (r.second < y)
qs_median_of_3 (a, r.second, y);
}
} void quicksort_median_of_3 (int*a, int n)
{
if (n > 0)
qs_median_of_3 (a, 0, n−1);
}
Se houver três ou menos
elementos, já estão
ordenados.
O primeiro elemento e
último já estão nas suas
partições. Logo,
podemos excluí-los do
processo de partição.
02-07-2012 Algoritmos e Estruturas de Dados I - 16 © Pedro Guerreiro 19
Cutoff • A ideia é não fazer as chamadas
recursivas quando os subvectores
tiverem menos do que um certo número
de elementos, o cutoff.
• O vector ficará “quase ordenado”, mas
haverá alguns elementos localmente
fora de ordem.
• A seguir entra o insertionsort, pois é
mesmo desse tipo de vectores que ele
gosta mais.
02-07-2012 Algoritmos e Estruturas de Dados I - 16 © Pedro Guerreiro 20
Quicksort com cutoff • De facto, é com mediana de 3 e cutoff:
void qs_cutoff (int *a, int x, int y)
{
sort_3 (a+x, a+(x+y)/2, a+y);
if (y − x > 2)
{
Pair r = hoare (a, x+1, y−1);
if (r.first − x + 1 >= CUTOFF)
qs_cutoff (a, x, r.first);
if (y − r.second +1 >= CUTOFF)
qs_cutoff (a, r.second, y);
}
} void quicksort_cutoff (int *a, int n)
{
if (n > 0)
qs_cutoff (a, 0, n−1);
insertionsort (a, n);
}
#define CUTOFF 3
02-07-2012 Algoritmos e Estruturas de Dados I - 16 © Pedro Guerreiro 21
Exercícios
• Programe o quicksort geral para
vectores de cadeias.
• Programa a versão iterativa do quicksort
de Hoare.
02-07-2012 Algoritmos e Estruturas de Dados I - 16 © Pedro Guerreiro 22
Controlo
• Que variantes do quicksort estudámos
hoje? Qual a melhor?
• Qual o tamanho máximo da pilha no
quicksort iterativo?
02-07-2012 Algoritmos e Estruturas de Dados I - 16 © Pedro Guerreiro 23
Na próxima aula
• Analisaremos a complexidade do
quicksort.
• Estudaremos os casos em que se dá
mal.
• Trataremos de mais um interessante
algoritmo de ordenação: o mergesort.