Este é um resumo das seções 7.1 a 7.5 do livro do Sedgewick. O assunto é o mesmo do capitulo anterior:
Problema: rearranjar um vetor a[p..r] de modo que ele fique em ordem crescente,
ou seja, de tal modo que tenhamos a[p] ≤ a[p+1] ≤ ... ≤ a[r]. Usualmente, o vetor tem mais de um elemento (ou seja, p < r), mas devemos estar preparados para o caso de um só elemento (ou seja, p == r) e mesmo para o caso de nenhum elemento (ou seja, p > r). Vamos supor que os elementos do vetor são de um tipo Item para o qual os operadores <, <=, >=, > e == estão definidos.
Este capítulo estuda o algoritmo Quicksort, que em geral é bem mais eficiente que os algoritmos elementares.
bipartidose existe i em p..r tal que a[p..i-1] ≤ a[i] ≤ a[i+1..r]. Escreva um algoritmo que decida se um dado vetor a[p..r] é bipartido. Em caso afirmativo, o seu algoritmo deve devolver o índice i que caracteriza a bipartição.
O coração do Quicksort é o algoritmo da partição. Esse algoritmo rearranja os elementos de qualquer vetor a[p..r] e devolve um índice i em p..r tal que
a[p..i-1] ≤ a[i] ≤ a[i+1..r].
A expressão
a[p..i-1] ≤ a[i]
significa a[h] ≤ a[i]
para todo h em p..i-1
.
A expressão a[i] ≤ a[i+1..r]
deve ser entendida de modo semelhante.
p | i | r | |||||||
≤ v | ≤ v | ≤ v | ≤ v | = v | ≥ v | ≥ v | ≥ v | ≥ v | ≥ v |
Vamos nos restringir ao caso em que p < r, embora o problema também faça sentido com p == r. Gostaríamos que i ficasse a meio caminho entre p e r, mas devemos estar preparados para aceitar os casos extremos i == p e i == r.
É fácil inventar um algoritmo que faça o serviço. Mas não é tão fácil inventar um que seja rápido. Veja o programa 7.2 de Sedgewick:
#define less(A, B) (A < B) #define exch(A, B) {Item t = A; A = B; B = t;} // Recebe vetor a[p..r] com p < r. // Rearranja os elementos do vetor e // devolve i em p..r tal que a[p..i-1] <= // a[i] <= a[i+1..r]. // int partition (Item a[], int p, int r) { int i = p-1, j = r; Item v = a[r]; for (;;) { while (less(a[++i], v)) ; while (less(v, a[--j])) if (/*X*/ j == p) break; if (i >= j) break; exch(a[i], a[j]); } exch(a[i], a[r]); return i; }
A cada passagem pelo ponto X, ou seja, imediatamente antes de comparar j com p, temos a seguinte configuração:
p | i | j | r | ||||||
≤ v | ≤ v | ≤ v | ≥ v | ? | ? | ≤ v | ≥ v | ≥ v | = v |
(Os índices i e j estão ambos no intervalo p..r. Fica subentendido que se j == p então, ao contrário de todos os demais casos, pode não ser verdade que a[j] ≤ v.) Em particular, na última passagem pelo ponto X temos três possibilidades:
O número de comparações que o algoritmo executa (só estou contando as comparações entre v e elementos do vetor) é
igual ao número de elementos do vetor,
ou seja, igual a r-p+1. Logo, o algoritmo consome uma quantidade de tempo proporcional ao número de elementos do vetor.
exch(a[i],a[r]); return i;por
exch(a[j],a[r]); return j;?
#define less(A,B) (A < B)por
#define less(A,B) (A <= B)?
#define lesseq(A, B) (A <= B)
int particione (Item a[], int p, int r) {
Item b[1000], v = a[r];
int i = p, j = r, k;
for (k = p; k < r; ++k)
if (lesseq(a[k], v)) b[i++] = a[k];
else b[j--] = a[k];
/* agora i == j */
b[i] = v;
for (k = p; k <= r; ++k) a[k] = b[k];
return i;
}
typedef enum {FALSE, TRUE} bool; int particione (Item a[], int p, int r) { Item v = a[r]; int i = p, j = r-1; while (TRUE) { while (less(a[i], v)) ++i; while (j >= p && v <= a[j]) --j; if (i >= j) break; exch(a[i], a[j]); ++i; --j; } exch(a[j], a[r]); return j; }
int particione (Item a[], int p, int r) { Item v = a[r]; int i = p, j = r-1; while (i <= j) { if (less(a[i], v)) ++i; else { exch(a[i], a[j]); --j; } } exch(a[j], a[r]); return j; }
struct nó { int chave; struct nó *prox; } ;Escreva uma função que transforme a lista em duas: a primeira contendo os nós cuja chave é par e a segunda aqueles cuja chave é impar.
A função abaixo é igual à do programa 7.1 de Sedgewick. Ela implementa o algoritmo Quicksort usando a função partition definida acima.
// A função rearranja o vetor a[p..r], com
// p <= r+1, de modo que ele fique em ordem
// crescente.
//
void quicksort (Item a[], int p, int r) {
int i;
if (p < r) {
i = partition (a, p, r);
quicksort (a, p, i-1);
quicksort (a, i+1, r);
}
}
Note que o algoritmo deve funcionar corretamente até mesmo quando o vetor a ser ordenado está vazio, ou seja, quando p == r+1.
Quantas comparações quicksort faz entre elementos do vetor? É claro que isso depende do número N = r-p+1 de elementos do vetor. O pior caso acontece quanto partition produz, sistematicamente, uma partição desequilibrada: N-2 elementos no subvetor a[p..i-1] e 0 elementos no vetor a[i+1..r]. Nesse caso, a função partition faz N comparações para cuidar da primeira linha da figura abaixo, depois faz N-1 comparações para cuidar da segunda linha da figura, depois faz N-2 comparações para cuidar da terceira linha, etc., e finalmente faz 2 comparações para cuidar da penúltima linha da figura. Total aproximado:
N2/2 comparações.
x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x |
x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x |
x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x |
x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x |
x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x |
x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x |
x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x |
etc. | ||||||||||||||||||
x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x |
x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x |
x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x |
O melhor caso acontece quanto partition produz, sistematicamente, uma partição equilibrada: aproximadamente N/2 elementos no subvetor a[p..i-1] e aproximadamente N/2 no vetor a[i+1..r]. Nesse caso, a função partition faz N comparações para cuidar da primeira linha da figura abaixo. Na segunda linha da figura, partition faz pouco menos que N/2 para cuidar do lado esquerdo e pouco menos que N/2 comparações para cuidar do lado direito. Portanto, partition faz pouco menos que N comparações no total. Agora considere a terceira linha da figura. Aqui, partition faz quatro vezes N/4 comparações, ou seja, um total de pouco menos que N comparações. Na quarta linha da figura, partition também faz menos que N comparações. E assim por diante.
x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x |
x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x |
x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x |
x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x |
x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x |
x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x |
Quantas linhas tem a figura? A primeira linha tem um único bloco de N elementos. A segunda linha tem dois blocos de pouco menos de N/2 elementos cada. Na terceira linha, cada bloco tem menos que N/4 elementos. Na quarta linha, cada bloco tem menos que N/8 elementos. Na quinta linha, cada bloco tem menos que N/16 elementos. E assim por diante. Logo, o número de linhas da figura não passa de lg(N). Como o processamento de cada linha consome tempo proporcional a N, o número de comparações é
N lg(N).
Este é número de comparações no melhor caso. Este é também o número médio de comparações, embora isso não seja óbvio.
void quicksort (Item a[], int p, int r) { while (p < r) { int i; i = partition(a, p, r); quicksort(a, p, i-1); p = i+1; } }
Reescreva a função mais uma vez adotando a política de processar imediatamente o lado menor da partição (e portanto deixar na pilha da recursão o lado maior). Esta política garante que o tamanho da pilha da recursão não passa de lg(r-p+1), pois cada subvetor na pilha será menor que a metade do subvetor que está imediatamente abaixo na pilha.
O programa 7.3 do Sedgewick é uma versão iterativa do Quicksort. Ela usa uma pilha de ints para armazenar os índices que delimitam segmentos do vetor que estão à espera de ordenação. A implementação da pilha foi omitida, mas a ideia é que
// A função rearranja o vetor a[p..r], com
// p <= r+1, de modo que ele fique em ordem
// crescente.
//
void quicksort_i (Item a[], int p, int r) {
int i;
stackinit();
push(r); push(p);
while (!stackempty()) {
p = pop();
r = pop();
if (r <= p) continue;
i = partition(a, p, r);
if (i-p > r-i) {
push(i-1); push(p);
push(r); push(i+1);
}
else {
push(r); push(i+1);
push(i-1); push(p);
}
}
}
A função adota a política de colocar na pilha primeiro o lado maior do vetor e depois o lado menor. Com isso, o lado menor será processado antes que o lado maior. Consequência dessa política: o tamanho da pilha nunca passa de lg(N), onde N = r-p+1.