Capítulo 9
Ordenação
[RTF]
[PowerPoint] [PostScript]
9.1 Introdução
à Ordenação
9.2 Quicksort
9.3 HeapSort
9.4 Exercícios
9.1 Introdução
à Ordenação
- 
A noção
de um conjunto de dados ordenados é de considerável importância
na nossa vida quotidiana e por conseguinte também em computação
 
- 
Exemplos:
 
- 
Listas telefônicas,
 
- 
Listas de clientes
de uma empresa,
 
- 
Listas de peças
em um catálogo,
 
- 
etc.
 
- 
Algoritmos eficientes
para ordenar grandes quantidades de dados são de extrema importância.
 
- 
Já vimos:
 
- 
Algoritmos simples
e ineficientes, árvores
 
9.1.1 Conceitos básicos
em Ordenação
- 
Arquivo
 
- 
Um arquivo r de tamanho
n é uma seqüência de n ítens r[0], r[1], r[n-1].
 
- 
Cada ítem em
um arquivo é chamado registro.
 
- 
Um arquivo é
classificado por chave
 
- 
se para cada registro
r[i] existe um uma parte de r[i], k[i] chamada chave de classificação.
 
- 
se i<j implicar
que k[i] precede k[j] de acordo com algum critério qualquer predefinido
 
- 
Exemplo:
 
- 
Em um catálogo
telefônico o arquivo é o próprio catálogo,
 
- 
Um registro é
uma entrada com nome, nº e endereço
 
- 
O nome é a
chave.
 
Localização
de Execução:
- 
Ordenação
interna é aquela realizada na memória principal do computador.
 
- 
Ordenação
externa é aquela, onde os registros podem estar em uma memória
auxiliar (arquivo em disco).
 
Estabilidade:
- 
Uma técnica
de ordenação é chamada estável se, para todos
os registros i e j, dado que k[i] é igual a k[j];
 
- 
se r[i] precede r[j] no arquivo original,
 
- 
então r[i] precederá r[j] no
arquivo classificado.
 
Uma ordenação
pode ocorrer sobre os próprios registros ou sobre uma tabela auxiliar
de ponteiros.
- 
Classificação
direta é uma ordenação onde um arquivo é ordenado
diretamente, sendo os registros movidos de uma posição física
para outra.
 
- 
b) Classificação
por endereços é uma ordenação onde somente
uma tabela de ponteiros é ordenada, os registros em si não
são tocados.
 
9.1.2 Critérios
para Selecionar uma Ordenação
- 
Eficiência no
Tempo
 
- 
Se um arquivo é
pequeno (ex.: 20 registros) técnicas sofisticadas podem ser piores
 
- 
Algoritmos complexos,
módulos grandes
 
- 
Nº de vezes que
a ordenação será executada
 
- 
Se vamos classificar
um conjunto de dados, mesmo grande, só uma vez, pode ser mais interessante
implementar um algoritmo simples.
 
- 
Um programa que executa
ordenações repetidamente deve ser eficiente.
 
- 
Esforço de
programação não deve ser uma desculpa para a utilização
de um algoritmo inadequado. Um sistema ineficiente não vai vender.
 
Eficiência de
Espaço
- 
A técnica que
escolhi é compatível com as características de hardware
do ambiente para o qual a estou destinando?
 
9.1.2.1 Aspectos da
Eficiência de Tempo
- 
O critério
básico para a determinação da eficiência de
tempo de um algoritmo não é a somente sua complexidade assintótica
 
- 
Operações
críticas:
 
- 
Operação
crítica é uma operação básica no processo
de ordenação. Ex.: a comparação de duas chaves
ou o deslocamento de um registro de uma posição de memória
para outra.
 
- 
Um algoritmo que executa
todas as operações críticas na memória será
geralmente mais rápido, do que outro que executa algumas em memória
secundária (disco).
 
9.2
Quicksort
- 
A ordenação
por troca de partição ou quicksort é provavelmente
o algoritmo de ordenação mais utilizado.
 
- 
Divide and Conquer
 
- 
Complexidade média
O(n log n)
 
- 
Vantagens:
 
- 
Simples de implementar
 
- 
Bastante bem compreendido.
 
- 
Extensas análises
matemáticas de seu comportamento já foram feitas
 
- 
Desvantagens:
 
- 
Complexidade pode
ser O(n2) no pior caso, em que o arquivo está ordenado
ou ordenado em ordem inversa.
 
- 
Diferentes versões
melhoradas existem.
 
Algoritmo
Básico
- 
Quicksort trabalha
particionando um arquivo em duas partes e então as ordenando separadamente.
 
- 
Pode ser definido
recursivamente:
 
quicksort (tipoInfo : a[], inteiro : esq, dir)
        inteiro i;
        início
                se dir > esq então
                        i <- particione(a, esq, dir);
                        quicksort(a, esq, i-1);
                        quicksort(a, i+1, dir);
                fim se
        fim
Os parâmetros
dir e esq delimitam os subarquivos dentro do arquivo original, dentro dos
quais a ordenação ocorre.
A chamada inicial
pode ser feita com quicksort(a, 1, N);
O ponto crucial é
o algoritmo de partição.
Particionamento
em Quicksort
- 
O procedimento de
particionamento deve rearranjar o arquivo de maneira que as seguintes condições
valham:
 
- 
o elemento a[i] está
em seu lugar final no arquivo para um i dado,
 
- 
nenhum dos elementos
em a[esq],...,a[i-1] são maiores do que a[i],
 
- 
nenhum dos elementos
em a[i+1],...,a[dir] são menores do que a[i].
 
Exemplo
- 
Ordenação
do vetor inicial 25 57 48 37 12 92 86 33.
 
- 
Se o primeiro elemento
(25) for colocado na sua posição correta, teremos: 12 25
57 48 37 92 86 33.
 
Neste ponto:
- 
todos os elementos
abaixo de 25 serão menores e
 
- 
todos os elementos
acima de 25 serão maiores que 25.
 
- 
Como 25 está
na sua posição final, o problema foi decomposto na ordenação
dos subvetores: (12) e (57 48 37 92 86 33).
 
- 
O subvetor (12) já
está classificado.
 
- 
Agora o vetor pode
ser visualizado: 12 25 (57 48 37 92 86 33).
 
- 
Repetir o processo
para a[2]...a[7] resulta em:
 
12 25 (48 37 33) 57 (92 86)
- 
Se continuarmos particionando
12 25 (48 37 33) 57 (92 86), teremos:
 
- 
12 25 (37 33) 48 57
(92 86)
 
- 
12 25 (33) 37 48 57
(92 86)
 
- 
12 25 33 37 48 57
(92 86)
 
- 
12 25 33 37 48 57
(86) 92
 
- 
12 25 33 37 48 57
86 92
 
Visão
da Recursividade do Quicksort como Árvore:
Método
para o Particionamento
- 
Considere x=a[limInf]
como o elemento cuja posição final é a procurada.
 
- 
Usar sempre o primeiro
é só um artifício p/facilitar a implementação.
 
- 
Dois ponteiros alto
e baixo são incializados como os limites máximo e mínimo
do subvetor que vamos analisar.
 
- 
Em qualquer ponto
da execução, todo elemento acima de alto é maior do
que x e todo elemento abaixo de baixo é menor do que x.
 
- 
Os dois ponteiros
alto e baixo são movidos um em direção ao outro da
seguinte forma:
 
- 
1. Incremente baixo
em uma posição até que a[baixo] > x.
 
- 
2. Decremente alto
em uma posição até que a[alto] <= x.
 
- 
3. Se a alto > baixo
, troque a[baixo] por a[alto].
 
- 
O processo é
repetido até que a condição descrita em 3. falhe (quando
 
alto <= baixo ). Neste ponto a[alto]será
trocado por a[limInf],cuja posição final era procurada,
e alto é retornado em i.
Algoritmo
de Particionamento
inteiro partição(tipoInfo : a[], inteiro : limInf, limSup)
        variáveis
                tipoInfo : x, temp; inteiro : baixo, alto;
        início
                x <- a[limInf]; alto <- limSup; baixo <- limInf + 1;
                enquanto (baixo < alto) faça
                        enquanto (a[baixo] <= x E baixo < limSup) faça
                                incremente baixo;               /* Sobe no arquivo */
                        enquanto (a[alto] > x) faça
                                decremente alto;                /* Desce no arquivo */ 
                        se (baixo < alto) então /* Troca */
                                temp <- a[baixo];
                                a[baixo] <- a[alto];
                                a[alto] <- temp;
                        fim se
                fim enquanto
                a[limInf] <- a[alto];
                a[alto] <- x;
                retorne alto;
        fim
Comentários
sobre o Quicksort
- 
Para evitar o pior
caso e casos ruins onde elementos estão em grupos ordenados pode-se
utilizar uma estratégia probabilística:
 
- 
Selecione, ao invés
do primeiro elemento para ser a, um elemento aleatório. Critérios:
 
- 
Seleção
totalmente randômica: selecione qualquer elemento do subvetor
usando um gerador de números aleatórios.
 
- 
Desvantagem: tempo
de processamento extra para o gerador.
 
- 
Seleção
pseudorandômica: selecione um elemento do subvetor com base em
um cálculo qualquer baseado em valores que você tem à
mão (alto, baixo, chave do primeiro)
 
- 
Seleção
média: pegue ao invés do elemento inicial, sempre o do
meio.
 
- 
Todos estes métodos
melhoram a performance média.
 
9.3
HeapSort
- 
O HeapSort, também
chamado de Classificação por Monte ou Ordenação
com Árvore-Heap é um método de ordenação
que utiliza uma árvore-heap como estrutura intermediária
para efetuar a ordenação.
 
- 
Vantagens:
 
- 
O pior caso é
O(n log n). Os outros são um pouco, porém muito pouco, melhores.
 
- 
HeapSort é
mais estável que Quicksort.
 
- 
Desvantagens:
 
- 
Construir a árvore-heap
pode consumir muita memória.
 
- 
Em suma:
 
- 
Para dados bem bagunçados
o Quicksort é mais vantajoso por ser mais econômico.
 
- 
Para dados imprevisíveis,
pode ser mais vantajoso por ser previsível em termos de tempo de
execução.
 
9.3.1
Preâmbulo: Árvore Heap
- 
Def.: Uma árvore-heap,
também chamada de monte é uma árvore binária
por níveis, onde o nodo pai de uma subárvore sempre é
maior que todos os seus descendentes.
 
- 
Exemplo:
 
9.3.2
Método em HeapSort
É um método
em três estágios:
- 
Primeiramente criamos
uma visão sobre o arquivo como árvore binária por
níveis.
 
- 
Iterativamente:
 
- 
Reestruturamos o
arquivo para para que se transforme em um monte.
 
- 
Na terceira etapa
geramos a seqüência de saída retirando a raiz do monte
e reestruturando o monte para que retorne a satisfazer a condição-heap.
 
Visão do arquivo
x1,...xn por Níveis:
Árvore-Heap a partir
do Arquivo
(26
5 77 1 61 11 59 15 48 19)
| 
Arquivo Original por Níveis
 | 
Árvore Heap Resultante
 | 
  | 
  | 
Algoritmo
HeapSort
heapsort( tipoInfo : a[], inteiro : n)
        variáveis
                inteiro         :       i;
                tipoInfo        :       temp;
        início
                para I de (n div 2) até 1 passo -1 faça
                        ajuste(a, I, n);        /* Converta a em um heap */
                fim para
                para I de (n-1) até 1 passo -1 faça
                        temp <- a[I+1];
                        a[I+1] <- a[1]; /* Supõe posição inicial = 1 */
                        a[1] <- temp;
                        ajuste(a,1,I);  /* Restabeleça o heap */
                fim para
        fim
Algoritmo
do Ajuste
ajuste( tipoInfo : a[], inteiro : i, n)
        variáveis
                inteiro         :       j;
                tipoInfo        :       aAux;
        início
                aAux <- a[i]            /* Supõe posição inicial = 1 */
                j <- 2*i;
                enquanto j <= n faça
                        se j < n E a[j] < a[j+1] então
                                incremente j;
                        fim se
                        se aAux >= a[j] então
                                retorne
                        fim se
                        a[j div 2] <- a[j]; 
                        a[j] <- aAux;   /* falta no algoritmo dado em
                                           Horowitz&Sahni */
                        j <- 2*j;
                fim enquanto
                a[j div 2] <- aAux;
        fim
9.4
Exercícios
- 
Execute o algoritmo
de particionamento no papel para o exemplo de arquivo dado no exemplo de
QuickSort.
 
- 
Execute o algoritmo
do HeapSort sobre o mesmo exemplo. Mostre o arquivo a cada iteração
do ajuste.
 
- 
Escolha e implemente
em C++ um dos dois algoritmos dados.
 
- 
A implementação
deve ser capaz de ler um arquivo contendo uma série de números
reais, um por linha.
 
- 
A implementação
deverá ser capaz de escrever em um arquivo o tempo necessário
para ordenar os dados, no mesmo formato pedido par
 
- 
a a árvore.
 
- 
Diferença:
serão fornecidos diversos arquivos. O tempo deverá ser escrito
sempre no fim de um arquivo só.