Colocar um vetor numérico em ordem crescente é o primeiro passo na solução de muitos problemas práticos. Um vetor pode ser ordenado de muitas maneiras diferentes: algumas elementares, outras mais sofisticadas e eficientes. Assim, o problema da ordenação é um verdadeiro laboratório de projeto de algoritmos. Trataremos do assunto neste capítulo e nos três capítulos seguintes.
8.1 O problema da ordenação
Um vetor v[0 . . n−1] é crescente se v[0] ≤ v[1] ≤ ··· ≤ v[n−1]. O problema da ordenação de um vetor consiste no seguinte:
Rearranjar (ou seja, permutar) os elementos de um vetor v[0 . . n−1] de tal modo que ele se torne crescente.
Este capítulo discute dois algoritmos simples para o problema. Os três capítulos seguintes examinam algoritmos mais sofisticados e eficientes.
Exercícios
8.1.1 Escreva uma função que verifique se um dado vetor v[0 . . n−1] é crescente. 8.1.2 Leia o verbete Sorting algorithm na Wikipedia [21].
60 •ALGORITMOS em linguagem C ELSEVIER
8.2 Algoritmo de inserção
O algoritmo de ordenação por inserção é muito popular; ele é frequentemente usado para colocar em ordem um baralho de cartas.
/* Esta função rearranja o vetor v[0..n-1] em ordem * crescente. */
void Inserção (int n, int v[]) {
int i, j, x; for ( j = 1; /*A*/ j < n; j++) { x = v[ j]; for (i = j-1; i >= 0 && v[i] > x; i--) v[i+1] = v[i]; v[i+1] = x; } }
Para entender o algoritmo, basta observar que no início de cada repetição do for externo, ou seja, a cada passagem pelo ponto A,
1. o vetor v[0 . . n−1] é uma permutação do vetor srcinal e 2. o vetor v[0 . . j−1] é crescente.
Estas propriedades invariantes são trivialmente verdadeiras no início da primeira iteração, quando j vale 1, e permanecem verdadeiras no início das iterações subsequentes. No início da última iteração, j vale n e portanto o vetor v[0 . . n−1] está na ordem dese jada. (Note que a última iteração é interrompida logo no início, pois a condição j < n é falsa.)
0 crescente j−1 j n−1
444 555 555 666 777 222 999 222 999 222 999
Figura 8.1: Vetor v[0 . . n−1] no início de uma iteração da função Inserção.
Desempenho do algoritmo. O consumo de tempo da função Inserção é proporcional ao número de execuções da comparação “ v[i] > x”. Calculemos esse número. Para cada val or de j, a variável i assume no máximo j valores, a saber, j − 1, j − 2,..., 0. Como j varia de 1 a n, o número de execuções da
ELSEVIER Capítulo 8. Ordenação: algoritmos elementares • 61
comparação “ v[i] > x” é igual a j=1n−1 j no pior caso. A soma vale n (n−1)/2 e este número é essencialmente igual a n2/2 quando n é grande. Pode-se dizer,
portanto, que o consumo de tempo da função é proporcional a n2
no pior caso. Em outras palavras, a função consome no máximo n2 unidades
de tempo.1
Se a ordenação de n números consumir t milissegundos, a ordena- ção de 2n números consumirá 4t milissegundos e a ordenação de 10n números consumirá 100t milissegundos. Portanto, o algoritmo é lento quando n é grande.
Exercícios
8.2.1 No código da função Inserção, troque “ v[i] > x” por “ v[i] >= x”. A nova função continua produzindo uma ordenação crescente de v[0..n-1]?
8.2.2 No código da função Inserção, que ac ontece se tr ocarmos “for ( j = 1” por “for ( j = 0” ? Que acontece se trocarmos “ v[i+1] = x” por “ v[i] = x” ?
8.2.3 Critique a seguinte implementação do algoritmo de ordenação por inserção:
int i, j, x;
for (j = 1; j < n; j++) {
for (i = j-1; i >= 0 && v[i] > v[i+1]; i--) {x = v[i]; v[i] = v[i+1]; v[i+1] = x; } }
8.2.4 Critique a seguinte implementação do algoritmo de ordenação por inserção:
int h, i, j, x;
for (j = 1; j < n; j++) { x = v[j];
for (h = 0; h < j && v[h] <= x; h++) ; for (i = j-1; i >= h; i--) v[i+1] = v[i]; v[h] = x; }
8.2.5 Escreva uma versão do algoritmo de inserção que tenha o seguinte invariante: no início de cada iteração, o vetor v[ j+1 . . n−1] é crescente.
8.2.6 Busca binária. O for interno na função Inserção tem a missão de encontrar o ponto onde v[ j] deve ser inserido em v[0.. j-1], ou seja, encontrar o índice i tal que v[i] ≤ v[ j] < v[i+1]. Considere fazer isso com uma busca binária (veja Seção 7.3). Analise o resultado.
8.2.7 Ordem estritamente crescente. Escreva uma função que rearranje um vetor v[0 . . n−1] de modo que ele fique em ordem estritamente crescente.
8.2.8 Ordem decrescente. Escreva uma função que permute os elementos de um vetor v[0 . . n−1] de modo que eles fiquem em ordem decrescente.
1
A unidade de tempo depende do computador e dos detalhes da implementação da função, mas não do valor de n.
62 •ALGORITMOS em linguagem C ELSEVIER
8.2.9 Versão recursiva. Escreva uma versão recursiva do algoritmo de ordenação por inserção.
8.2.10 Animações. Veja animações do algoritmo de inserção nas páginas de Harri- son [8] e Morin [13] da teia www.
8.2.11 Leia o verbete Insertion sort na Wikipedia [21].
8.3 Algoritmo de seleção
O algoritmo de ordenação por seleção é baseado na ideia de escolher o menor elemento do vetor, depois o segundo menor, 2
e assim por diante.
/* Rearranja o vetor v[0..n-1] em ordem crescente. */
void Seleção (int n, int v[]) {
int i, j, min, x; for (i = 0; /*A*/ i < n-1; i++) { min = i; for ( j = i+1; j < n; j++) if (v[ j] < v[min]) min = j; x = v[i]; v[i] = v[min]; v[min] = x; } }
Para entender como e por que o algoritmo funciona, basta observar que no início de cada repetição do for externo, ou seja, a cada passagem pelo ponto A, valem os seguintes invariantes:
1. v[0 . . n−1] é uma permutação do vetor srcinal, 2. v[0 . . i−1] está em ordem crescente e
3. v[i − 1] ≤ v[ j] para j = i, i+1,...,n −1.
A tradução do invariante 3 para linguagem humana é a seguinte: v[0 . . i−1] contém todos os elementos “pequenos” do vetor original e v[i..n −1] contém todos os elementos “grandes”. Os três invariantes garantem que no início de cada iteração os elementos v[0],. .. ,v [i−1] já estão em suas posições definitivas.
Desempenho do algoritmo. Uma análise semelhante à que fizemos para o algoritmo de inserção mostra que o algoritmo de seleção faz cerca de n2/2
2
ELSEVIER Capítulo 8. Ordenação: algoritmos elementares • 63
0 i−1 i n−1
110 120 120 130 140 666 999 666 999 666 999
pequenos, crescente grandes
Figura 8.2: Vetor v[0 . . n−1] no início de uma iteração da função Seleção.
comparações entre elementos do vetor. Portanto, consome n2unidades de tempo
no pior caso.
Exercícios
8.3.1 Que acontece se trocarmos “ for (i = 0” por “ for (i = 1” no códig o da função
Seleção? Que acontece se trocarmos “ for (i = 0; i < n-1” por “for (i = 0; i < n” ? 8.3.2 Troque “ v[ j] < v[min]” por “ v[ j] <= v[min]” no código de Seleção. A nova função continua produzindo uma ordenação crescente de v[0..n-1]?
8.3.3 Ordem decrescente. Escreva uma função que permu te os elementos de um vetor v[0 . . n−1] de modo que eles fiquem em ordem decrescente.
8.3.4 Versão recursiva. Escreva uma versão recursiva do algoritmo de ordenação por seleção.
8.3.5 Animações. Veja animações do algoritmo de seleção nas páginas de Harrison [8] e Morin [13].
8.3.6 Leia o verbete Selection sort na Wikipedia [21].
8.4 Exercícios: ordenação de strings e listas
8.4.1 Ordenação de strings. Escreva uma função que coloque um vetor de strings em ordem lexicográfica (veja Seção G.3). Faça duas versões: uma baseada no algoritmo de inserção e outra baseada no algoritmo de seleção.
8.4.2 Ordenação de arquivo. Escreva uma função que rearranje as linhas de um arquivo (veja Apêndice H) em ordem lexicográfica (veja Seção G.3). Compare com o utilitário sort presente em todo sistema UNIX e GNU/Linux.
8.4.3 Ordenação de structs. Suponha que cada elemento de um vetor é um registro que consiste em um inteiro e uma string:
struct elem {int i; char *s;};
Escreva uma função que rearranje o vetor de modo que os campos i fiquem em ordem crescente. Escreva outra função que rearranje o vetor de modo que os campos s fiquem em ordem lexicográfica (veja Seção G.3).
64 •ALGORITMOS em linguagem C ELSEVIER
8.4.4 Ordenação de lista encadeada. Escreva uma função que ordene uma lista encadeada. Inspire-se no algoritmo de ordenação por inserç ão. Faça duas vers ões: uma para lista com cabeça e outra para lista sem cabeça. (Sua função precisa devolver alguma coisa?). Repita o exercício com base no algoritmo de ordenação por seleção. 8.4.5 Projeto de programação. Digamos que duas palavras são equivalentes se uma é anagrama da outra, ou seja, se a sequência de letras de uma é permutação da sequência de letras da outra. Por exemplo, “ aberto” e “rebato” são equivalentes.
Uma classe de equivalência de palavras é um conjunto de palavras duas a duas equi- valentes. Escreva um programa que receba um arquivo de palavras (uma palavra por linha) e extraia desse arquivo uma classe de equiva lência de tamanho máximo. Apli- que o seu programa ao arquivo de palavras www.ime.usp.br/~pf/algoritmos/dicios/, que contém todas as palavras do português falado no Brasil (o arquivo foi extraído do Dicionário br.ispell [20]).
8.5 Ordenação estável
Um algoritmo de ordenação é estável se não altera a posição relativa de ele- mentos que têm um mesmo valor. Por exemplo, se o vetor tiver dois elementos de valor 222, um algoritmo de ordenação estável manterá o primeiro 222 antes do segundo.
vetor srcinal: 444 555 666 777 333 222 1 111 222 2 888
vetor ordenado: 111 222 1 2222 333 444 555 666 777 888
Figura 8.3: Ordenação estável. O vetor srcinal tem dois elementos com valor 222 (índices 1 e 2 são usados para distinguir o primeiro do segundo). No vetor
ordenado, o primeiro destes elementos continua à frente do segundo.
Suponha, por exemplo, que os elementos de um vetor são pares da forma (d, m) que representam datas de um certo ano: a primeira componente repre- senta o dia e a segunda representa o mês. (Compare com o Exercício 8.4.3.) Suponha que o vetor está em ordem crescente das componentes d:
(1, 12), (7, 12), (16, 3), (25, 9), (30, 3), (30, 6), (31, 3).
Agora ordene o vetor pelas componentes m. Se usarmos um algoritmo de orde- nação estável, o resultado estará em ordem cronológica:
ELSEVIER Capítulo 8. Ordenação: algoritmos elementares • 65
Se o algoritmo de ordenação não for estável, o resultado pode não ficar em ordem cronológica:
(30, 3), (16, 3), (31, 3), (30, 6), (25, 9), (7, 12), (1, 12).
Exercícios
8.5.1 O algoritmo de ordenação por inserção (Seção 8.2) é estável?
8.5.2 Na código da função Inserção (Seção 8.2), troque a comparação “ v[i] > x” por “v[i] >= x”. A nova função faz uma ordenação estável de v[0..n-1]?