Ordenação:
BubbleSort, SelectSort e InsertSort
O Critério de Ordenação
● Ordena-se de acordo com uma chave:
typedef int TChave; typedef struct{
TChave chave;
/* outros componentes */ }Item;
Características
● Estabilidade: relativo à manutenção da ordem original de itens
de chaves iguais
– Um método de ordenação é estável se a ordem relativa dos itens com chaves iguais não se altera durante a ordenação.
● Ordenação interna: arquivo a ser ordenado cabe todo na
Critério de Avaliação
● Sendo n o número de registros no arquivo, as medidas de
complexidade relevantes são:
– Número de comparações C(n) entre chaves. – Número de movimentações M(n) de itens
Outras Considerações
● O uso econômico da memória disponível é um requisito
primordial na ordenação interna.
● Métodos de ordenação in situ são os preferidos.
● Métodos que utilizam listas encadeadas não são muito
utilizados.
● Métodos que fazem cópias dos itens a serem ordenados
Métodos simples
● Bolha (BubbleSort) ● Seleção (SelectSort) ● Inserção (InsertSort)
Método Bolha
● Os elementos vão “borbulhando” a cada iteração do método
até a posição correta para ordenação da lista
● O método poderia parar quando nenhum elemento
borbulhasse/trocasse de posição
● Como os elementos são trocados (borbulhados)
Método Bolha
void Bolha( Item* v, int n ){
int i, j; Item aux;
for( i = 0; i < n-1; i++ ){ for( j = 1; j < n-i; j++ ){
if( v[j].chave < v[j-1].chave ){ aux = v[j]; v[j] = v[j-1]; v[j-1] = aux; } } } }
Análise de Complexidade
● Comparações - C(n) ● Movimentações – M(n) M(n) = 3C(n) ) ( 2 ) 1 ( 2 ) 1 )( 2 0 ( ) 1 ( 1 ) 1 ( ) ( 2 2 2 0 2 0 2 0 2 0 n O n n n n n n n i n i n n C n i n i n i n i
Ordenação por Bolha
● Vantagens:
– Algoritmo simples – Algoritmo estável
● Desvantagens:
– O fato de o arquivo já estar ordenado não ajuda reduzir o número de comparações (o custo continua quadrático), porém o número de movimentação cai a zero.
Método Bolha
void Bolha (Item* v, int n ){
int i, j, troca; Item aux;
for( i = 0; i < n-1; i++ ){ troca = 0;
for( j = 1; j < n-i; j++ ){
if( v[j].chave < v[j-1].chave ){ aux = v[j]; v[j] = v[j-1]; v[j-1] = aux; troca = 1; } if(troca == 0) break;
Método Seleção
● Seleção do n-ésimo menor (ou maior) elemento da lista
● Troca do n-ésimo menor (ou maior) elemento com a n-ésima
posição da lista
Método Seleção
void Selecao( Item* v, int n ){
int i, j, min; Item aux;
for(i = 0; i < n - 1; i++){ min = i;
for(j = i+1; j < n; j++)
if( v[j].chave < v[min].chave) min = j; aux = v[min]; v[min] = v[i]; v[i] = aux; } }
Análise de Complexidade
● Comparações – C(n) ● Movimentações – M(n) M(n) = 3(n-1) ) ( 2 ) 1 ( 2 ) 1 )( 2 0 ( ) 1 ( 1 ) 1 ( ) ( 2 2 2 0 2 0 2 0 2 0 n O n n n n n n n i n i n n C n i n i n i n i
Ordenação por Seleção
● Vantagens:
– Custo linear no tamanho da entrada para o número de movimentos de registros.
– É o algoritmo a ser utilizado para arquivos com registros muito grandes.
– É muito interessante para arquivos pequenos.
● Desvantagens:
– O fato de o arquivo já estar ordenado não ajuda em nada, pois o custo continua quadrático.
Método Seleção – Melhoria!
void Selecao(Item* v, int n){
int i, j, min; Item aux;
for( i = 0; i < n-1; i++ ){ min = i;
for( j = i+1; j < n; j++ )
if( v[j].chave < v[min].chave) min = j; if( i != min ){ aux = v[min]; v[min] = v[i]; v[i] = aux; } } }
Método Inserção
● Algoritmo utilizado pelo jogador de cartas
– As cartas são ordenadas da esquerda para direita uma por uma. – O jogador escolhe a segunda carta e verifica se ela deve ficar
antes ou na posição que está.
– Depois a terceira carta é classificada, deslocando-a até sua correta posição
– O jogador realiza esse procedimento até ordenar todas as cartas
● Alto custo em remover uma carta de uma posição e colocá-la
Método Inserção
void Insercao( Item* v, int n ){
int i, j; Item aux;
for( i = 1; i < n; i++ ){ aux = v[i];
j = i–1;
while( (j >= 0) && (aux.chave < v[j].chave) ){ v[j+1] = v[j]; j--; } v[j+1] = aux; } }
Análise de Complexidade
● Comparações – C(n)
– No anel mais interno, na i-ésima iteração, o valor de Ci é:
● melhor caso: C i (n) = 1 ● pior caso: C i (n) = i ● caso medio: C i(n) = 1/i (1 + 2 + ... + i) = (i+1)/2
– Assumindo que todas as permutações de n são igualmente prováveis no caso médio, temos:
● melhor caso: C(n) = (1 + 1 + ... + 1) = n - 1
● pior caso: C(n) = (1 + 2 + ... + n-1) = n2/2 - n/2
Análise de Complexidade
● Movimentações – C(n)
– No anel mais interno, na i-ésima iteração, o valor de Ci é:
● melhor caso: C
i (n) = 0
● pior caso: C
i (n) = i
● caso medio: C
i(n) = 1/i (0 + 1 + 2 + ... + i-1) = (i-1)/2
– Assumindo que todas as permutações de n são igualmente prováveis no caso médio, temos:
● melhor caso: C(n) = (2 + 2 + ... + 2) = 2n - 2
● pior caso: C(n) = (2+1 + 2+2 + ... + 2+n-1) = (n2+3n-4)/2 ● caso médio: C(n) = ½ (2 + 3 + ... + n ) = (n2 + n – 2)/2
Ordenação por Inserção
● O número mínimo de comparações e movimentos ocorre
quando os itens estão originalmente em ordem.
● O número máximo ocorre quando os itens estão originalmente
na ordem reversa.
● É o método a ser utilizado quando o arquivo está “quase”
ordenado.
● É um bom método quando se deseja adicionar uns poucos
itens a um arquivo ordenado, pois o custo é linear.
Ordenação Interna
● Métodos simples:
– Adequados para pequenos arquivos. – Requerem O(n²) comparações.
– Produzem programas pequenos.
● Métodos eficientes:
– Adequados para arquivos maiores. – Requerem O(n log n) comparações. – Usam menos comparações.
– As comparações são mais complexas nos detalhes.
Quicksort
● Proposto por Hoare em 1960 e publicado em 1962.
● É o algoritmo de ordenação interna mais rápido que se
conhece para uma ampla variedade de situações.
● Provavelmente é o mais utilizado.
● A ideia básica é dividir o problema de ordenar um conjunto com
n itens em dois problemas menores.
● Os problemas menores são ordenados independentemente.
Quicksort
● A parte mais delicada do método é o processo de partição.
● O vetor A [esq..dir] é rearranjado por meio da escolha arbitrária
de um pivô x.
● O vetor A é particionado em duas partes:
– Parte esquerda: chaves ≤ x. – Parte direita: chaves ≥ x.
Quicksort:
Partição
● Algoritmo para o particionamento:
1) Escolha arbitrariamente um pivô x.
2) Percorra o vetor a partir da esquerda até que A[i] ≥ x. 3) Percorra o vetor a partir da direita até que A[j] ≤ x. 4) Troque A[i] com A[j].
Quicksort:
Após a Partição
● Ao final do algoritmo de partição, o vetor A[Esq..Dir] está
particionado de tal forma que:
– Os itens em A[esq], A[esq + 1], ..., A[j] são menores ou iguais a x; – Os itens em A[i], A[i + 1], ..., A[dir] são maiores ou iguais a x.
Quicksort:
Exemplo
● O pivô x é escolhido como sendo:
– O elemento central: A[(i + j) / 2].
● Exemplo:
Quicksort:
Exemplo
3
6
4
5
1
7
2
3
2
4
1
5
7
6
1
2
4
3
5
7
6
.
.
.
1
2
3
4
5
7
6
Caso especial: pivô já na posição correta
Quicksort:
Exemplo
1
2
3
4
5
7
6
1
2
3
4
5
6
7
Final3
2
4
1
5
6
7
1
2
4
3
5
6
7
Quicksort:
Partição
void particao(Item* A, int esq, int dir,
int *i, int *j){ Item x, aux;
*i = esq; *j = dir;
x = A[(*i + *j)/2]; /* obtem o pivo x */ do{
while(x.chave > A[*i].chave) (*i)++; while(x.chave < A[*j].chave) (*j)--; if(*i <= *j){
aux = A[*i]; A[*i] = A[*j]; A[*j] = aux; (*i)++; (*j)--;
}
Quicksort
● O anel interno da função Particao é extremamente simples. ● Razão pela qual o algoritmo Quicksort é tão rápido.
Quicksort:
Função
void quickSort(Item *A, int n){
Ordena(A, 0, n-1); }
void ordena(Item *A, int esq, int dir){
int i,j;
particao(A, esq, dir, &i, &j);
if(esq < j ) ordena( A, esq, j ); if(i < dir) ordena( A, i, dir ); }
Quicksort:
Características
● Qual o pior caso para o Quicksort? ● Qual o melhor caso?
Quicksort:
Análise
● Seja C(n) a função que conta o número de comparações. ● Pior caso: C(n) = O(n²)
● O pior caso ocorre quando, sistematicamente, o pivô é
escolhido como sendo um dos extremos de um arquivo já ordenado.
● Isto faz com que o procedimento Ordena seja chamado
recursivamente n vezes, eliminando apenas um item em cada chamada.
Quicksort:
Análise
● Melhor caso:
– C(n) = 2C(n/2) + n = n log n
– Esta situação ocorre quando cada partição divide o arquivo em duas partes iguais.
● Caso médio de acordo com Sedgewick e Flajolet (1996, p. 17):
– C(n) ≈ 1,386n log n – 0,846n,
– Isso significa que em média o tempo de execução do Quicksort é O(n log n).
Quicksort
● Vantagens:
– É extremamente eficiente para ordenar arquivos de dados.
– Necessita de apenas uma pequena pilha como memória auxiliar. – Requer cerca de n log n comparações em média para ordenar n
itens.
● Desvantagens:
– Tem um pior caso O(n²) comparações.
– Sua implementação é muito delicada e difícil:
Problema de Ordenação
● Dado um arranjo com n números naturais, ordenar estes
números em ordem crescente.
Entrada: 95 48 70 86 21 37
Abordagem Dividir-para-Conquistar
● Método em Computação que consiste em
– Dividir a entrada em conjuntos menores
– Resolver cada instância menor de maneira recursiva
– Reunir as soluções parciais para compor a solução do problema original.
Abordagem Balanceamento
● Métodos de ordenação de divisão por conquista:
– SelectSort
– QuickSort (pior caso?)
● Divisão do problema de forma balanceada:
Exemplo de MergeSort
Entrada: 47 26 33 05 99 38 64 15 Divide: 47 26 33 05 99 38 64 15
● Resolve Recursivamente:
– Primeira metade 47 26 33 05
(Divide, Resolve recursivamente, Intercala Obtendo 05 26 33 47 )
● Segunda metade 99 38 64 15
(Divide, Resolve recursivamente, Intercala Obtendo 15 38 64 99 )
Exemplo de MergeSort
Entrada: 47 26 33 05 99 38 64 15 Resolve Recursivamente:
(Retorna 05 26 33 47 ) (Retorna 15 38 64 99 )
Intercala as soluções parciais:
Algoritmo MergeSort
void mergesort(Item* A,int ini,int fim){
int meio;
if(fim == ini) return;
meio = (ini+fim)/2;
mergesort( A, ini, meio ); mergesort( A, meio+1, fim );
intercala( A, ini, meio, fim ); return;
} }
Implementação de Intercala
intercalaAB(Item* C, Item* A, int n, Item* B, int m){ int i, j, k;
for(i = 0, j = 0, k = 0; k < n+m; k++){
if( i == n ) { C[k] = B[j++]; continue; } if( j == m ) { C[k] = A[i++]; continue; } if( A[i].chave < B[j].chave ) C[k] = A[i++]; else C[k] = B[j++]; }
Exemplo de MergeSort
2 6 8 5 10 9 12 1 15 7 3 13 4 11 16 14 2 6 5 8 10 9 12 1 15 7 3 13 4 11 16 14 2 5 6 8 10 9 12 1 15 7 3 13 4 11 16 14 2 5 6 8 9 10 12 1 15 7 3 13 4 11 16 14 2 5 6 8 9 10 1 12 15 7 3 13 4 11 16 14 2 5 6 8 1 9 10 12 15 7 3 13 4 11 16 14 1 2 5 6 8 9 10 12 15 7 3 13 4 11 16 14 1 2 5 6 8 9 10 12 7 15 3 13 4 11 16 14 1 2 5 6 8 9 10 12 7 15 3 13 4 11 16 14 1 2 5 6 8 9 10 12 3 7 13 15 4 11 16 14 1 2 5 6 8 9 10 12 3 7 13 15 4 11 16 14 1 2 5 6 8 9 10 12 3 7 13 15 4 11 14 16 1 2 5 6 8 9 10 12 3 7 13 15 4 11 14 16 1 2 5 6 8 9 10 12 3 4 7 11 13 14 15 16Implementação de Intercala
intercala(Item* A, int ini, int meio, int fim){ int i, j, k;
Item* C = (int*) malloc((fim-ini+1)*sizeof(Item)); for(i = ini, j = meio+1, k = 0; k <= fim-ini; k++){ if( i == meio+1 ) { C[k] = A[j++]; continue; }
if( j == fim+1 ) { C[k] = A[i++]; continue; } if( A[i].chave < A[j].chave ) C[k] = A[i++]; else C[k] = A[j++]; }
for(i = ini, k = 0; i <= fim; i++, k++){ A[i] = C[k];
Complexidade do MergeSort
MergeSort
● Vantagens
– Como HeapSort, MergeSort é O(n log n)
– Indicado para aplicações que exigem restrição de tempo (executa sempre em um determinado tempo para um dado n)
– Passível de ser transformado em estável
● Implementação de intercala
– Fácil Implementação
● Desvantagens
Filas de Prioridades
● É uma estrutura de dados onde a chave de cada item reflete
sua habilidade relativa de abandonar o conjunto de itens rapidamente.
● Aplicações:
– SOs usam filas de prioridades, nas quais as chaves representam o tempo em que eventos devem ocorrer.
– Métodos numéricos iterativos são baseados na seleção repetida de um item com maior (menor) valor.
– Sistemas de gerência de memória usam a técnica de substituir a página menos utilizada na memória principal por uma nova
Filas de Prioridades (TAD):
Operações
1) Constrói uma fila de prioridades a partir de um conjunto com n itens.
2) Informa qual é o maior item do conjunto. 3) Retira o item com maior chave.
4) Insere um novo item.
5) Aumenta o valor da chave do item i para um novo valor que é maior que o valor atual da chave.
6) Substitui o maior item por um novo item. 7) Altera a prioridade de um item.
Filas de Prioridades:
Representação
● Lista linear não ordenada:
– Constrói é O(n) – Insere é O(1) – Retira é O(n) – Altera é O(n)
● Lista linear ordenada:
– Constrói é O(n log n) ( ou O(n²) ) – Insere é O(n)
Filas de Prioridades:
Representação
● A melhor representação é através de uma estruturas de dados
chamada heap:
– Neste caso, Constrói é O(n).
– Insere, Retira, Substitui e Altera são O(log n).
● Observação:
– Para implementar a operação Agrupar de forma eficiente e ainda preservar um custo logarítmico para as operações Insere, Retira, Substitui e Altera é necessário utilizar estruturas de dados mais sofisticadas, tais como árvores binomiais (Vuillemin, 1978).
Filas de Prioridades
● As operações das filas de prioridades podem ser utilizadas
para implementar algoritmos de ordenação.
● Basta utilizar repetidamente a operação Insere para construir a
fila de prioridades.
● Em seguida, utilizar repetidamente a operação Retira para
Algoritmos de Ordenação
Usando Listas de Prioridades
● O uso de listas lineares não ordenadas corresponde ao método
da Seleção.
● O uso de listas lineares ordenadas corresponde ao método da
Inserção.
Heaps
É uma seqüência de itens com chaves c[1], c[2], ... , c[n], tal que:
c[i] >= c[2*i],
c[i] >= c[2*i + 1], para todo i = 1, 2, ..., n/2
Heaps
● A definição pode ser facilmente visualizada em uma árvore
Heaps
● Árvore binária completa:
– Os nós são numerados de 1 a n. – O primeiro nó é chamado raiz.
– O nó [k/2] é o pai do nó k, para 1 < k <= n.
– Os nós 2k e 2k + 1 são os filhos à esquerda e à direita do nó k, para 1 <= k <= [k/2].
Heaps
● As chaves na árvore satisfazem a condição do heap.
● A chave em cada nó é maior do que as chaves em seus filhos. ● A chave no nó raiz é a maior chave do conjunto.
● Uma árvore binária completa pode ser representada por um
Heaps
● A representação é extremamente compacta.
● Permite caminhar pelos nós da árvore facilmente. ● Os filhos de um nó i estão nas posições 2i e 2i + 1. ● O pai de um nó i está na posição i/2.
● Na representação do heap em um arranjo, a maior chave está
sempre na posição 1 do vetor.
● Os algoritmos para implementar as operações sobre o heap
Heaps
● O algoritmo não necessita de nenhuma memória auxiliar. ● Dado um vetor A[1], A[2], ..., A[n].
● Os itens A[n/2 + 1], A[n/2 + 2], ..., A[n] formam um heap:
– Neste intervalo não existem dois índices i e j tais que j = 2i ou j = 2i + 1.
Heaps
Heaps
● Os itens de A[4] a A[7] formam um heap.
● O heap é estendido para a esquerda (Esq = 3), englobando o
item A[3], pai dos itens A[6] e A[7].
● A condição de heap é violada:
– O heap é refeito trocando os itens D e S.
● O item R é incluindo no heap (Esq = 2), o que não viola a
condição de heap.
● O item O é incluindo no heap (Esq = 1). ● A Condição de heap violada:
Heaps
● O Programa que implementa a operação que informa o item
com maior chave:
Item max(Item *A){ return (A[1]); }
Heaps
● Programa para refazer a condição do heap: void refaz(Item* A, int esq, int dir){
int i = esq, j = i*2; Item aux = A[i];
while( j <= dir ){ if( j < dir )
if( A[j].chave < A[j+1].chave ) j++;
if( aux.chave >= A[j].chave ) break; A[i] = A[j];
i = j; j = i*2; }
A[i] = aux; }
Heaps
● Programa para construir o heap: void constroi(Item *A, int *n){
int esq = *n / 2; while(esq > 0){ refaz(A, esq, *n); esq--; } }
Heaps
● Programa que implementa a operação de retirar o item com
maior chave:
Item retiraMax(Item *A, int *n){ Item max;
if (*n < 1) printf(“Erro: heap vazio\n”); else{ max = A[1]; A[1] = A[*n]; (*n)--; refaz(A, 1, *n); } return max; }
Heaps
● Programa que implementa a operação de aumentar o valor da
chave do item i:
void aumentaChave(Item* A, int i, TChave novo){
Item aux;
if(novo < A[i].chave){
printf(“Erro: Chave Nova menor que a chave atual”); return;
}
A[i].chave = novo;
while( i > 1 && A[i/2].chave < A[i].chave ){ aux = A[i/2]; A[i/2] = A[i]; A[i] = aux;
Heaps
● Exemplo da operação de aumentar o valor da chave do item na
posição i:
● O tempo de execução do procedimento aumentaChave em um
Heaps
● Programa que implementa a operação de inserir um novo item
no heap:
void insere(Item* A, int* n, Item* x){
(*n)++;
A[*n] = *x;
A[*n].chave = INT_MIN;
aumentaChave(A, *n, x->chave); }
Heapsort:
Algoritmo
1) Construir o heap.
2) Troque o item na posição 1 do vetor (raiz do heap) com o item da posição n.
3) Use o procedimento refaz para reconstituir o heap para os itens A[1], A[2], ..., A[n-1].
4) Repita os passos 2 e 3 com os n-1 itens restantes, depois com os n-2, até que reste apenas um item.
Heapsort
Heapsort
● O caminho seguido pelo procedimento refaz para reconstituir a
condição do heap está em negrito.
● Por exemplo, após a troca dos itens S e D na segunda linha da
Figura, o item D volta para a posição 5, após passar pelas posições 1 e 2.
Heapsort
● Programa que mostra a implementação do Heapsort: void Heapsort(Item *A, int *n){
int esq, dir; Item x;
constroi(A, n); /* constroi o heap */ esq = 1; dir = n;
/* ordena o vetor */ while(dir > 1){
x = A[1]; A[1] = A[dir]; A[dir] = x; dir--; refaz(A, esq, dir);
Heapsort:
Análise
● refaz - gasta cerca de log n operações, no pior caso. ● constroi – executa O(n) * refaz
● Loop interno – executa (n) * refaz
● Logo, Heapsort gasta um tempo de execução proporcional a
Heapsort:
Análise
● Vantagens:
– O comportamento do Heapsort é sempre O(n log n), qualquer que seja a entrada.
● Desvantagens:
– O anel interno do algoritmo é bastante complexo se comparado com o do Quicksort.
– O Heapsort não é estável.
● Recomendado: