Programação II Tópicos Extras Ordenação (sort) Bruno Feijó Dept. de Informática, PUC-Rio
qsort Quick Sort da Biblioteca C
Ponteiros para Funções Em C é possível definir ponteiros para funções que podem ser colocados em vetores, passados para funções e retornados por funções No exemplo a seguir, a função opera(m, x, y, func) recebe um ponteiro de função como um de seus argumentos (func) e retorna m func(x,y). Devemos notar que o nome da função mult é um ponteiro quando escrevemos opera(2,3,5,mult); #include <stdio.h> Se argumentos são ponteiros, usamos const quando queremos garantir que a função func não modificará os valores que são int mult(int x,int y); apontados: int(*func)(const int *, const int *)) int soma(int x, int y); int opera(int m, int x, int y, int(*func)(int, int)); int main(void) int a, b; a = opera(2,3,5,mult); b = opera(2,3,5,soma); printf("%d %d\n",a,b); return 0; Saída: 30 16 int mult(int x,int y) return x*y; int soma(int x, int y) return x+y; int opera(int m,int x,int y,int(*func)(int, int)) return m*func(x,y);
Quick Sort da stdlib do C void qsort(void * v, int n, int tam, int (*cmp)(const void *, const void *)); v: vetor de ponteiros genéricos n: número de elementos do vetor tam: tamanho em bytes de cada elemento (use sizeof para especificar) cmp: ponteiro para função que compara elementos genéricos int nome(const void * a, const void * b); deve retornar <0 se a<b, >0 se a>b e 0 se a == b const é para garantir que a função não modificará os valores dos elementos static int compfloat(const void * a, const void * b) /* converte os ponteiros genericos */ float * aa = (float *)a; float * bb = (float *)b; /* faz a comparacao com (*aa) e (*bb) retornando -1,0, ou 1 */ if (*aa > *bb) // atenção para o * return 1; else if (*aa < *bb) return -1; else return 0; chamada: qsort(v,n,sizeof(float),compfloat); // N é o tamanho de v
Exemplo Quick Sort da stdlib #include <stdlib.h> int main(void)... void QsortPessoa(int n, Pessoa ** v) qsort(v,n,sizeof(pessoa *),comppessoa); Pessoa tab[] = "Diana Maria",22,...; Pessoa * v[] = tab,tab+1,...;... QsortPessoa(n,tabPessoa); // ordenacao com Quick Sort... int comppessoa(const void * a, const void * b) Pessoa ** aa = (Pessoa **)a; Pessoa ** bb = (Pessoa **)b; int cmp = strcmp((*aa)->nome,(*bb)->nome); if (cmp>0 (cmp==0 && ((*aa)->idade>(*bb)->idade))) return 1; else if (cmp<0 (cmp==0 && ((*aa)->idade<(*bb)->idade))) return -1; else return 0;
Bubble Sort
Bubble Sort Ordem Crescente Apenas de interesse didático e de referência A idéia é ir comparando dois vizinhos e trocando o menor pelo maior até que o maior de todos fica no final (como se o maior fosse uma bolha que sobe até o topo) 0 1 0 1 1 2... 1 2...... j j+1... j j+1 passo 1 passo 2 passo 3... Este é o maior de todos Este é o segundo maior de todos
Exemplo Bubble Sort Passo 1 e Passo 2 n 1 comparações (i.e. 7) n = 8 elementos 25 48 37 12 57 86 33 92 25x48 25 48 37 12 57 86 33 92 48x37 troca 25 37 48 12 57 86 33 92 48x12 troca 25 37 12 48 57 86 33 92 48x57 25 37 12 48 57 86 33 92 57x86 25 37 12 48 57 86 33 92 86x33 troca 25 37 12 48 57 33 86 92 86x92 25 37 12 48 57 33 86 92 final do passo 1 o maior elemento, 92, já está na sua posição final n 2 comparações (i.e. 6) 25 37 12 48 57 33 86 92 25x37 25 37 12 48 57 33 86 92 37x12 troca 25 12 37 48 57 33 86 92 37x48 25 12 37 48 57 33 86 92 48x57 25 12 37 48 57 33 86 92 57x33 troca 25 12 37 48 33 57 86 92 57x86 25 12 37 48 33 57 86 92 final do passo 2 o segundo maior elemento, 86, já está na sua posição final
Bubble Sort Passo 3 e Passo 4 n 3 (= 5) 25 12 37 48 33 57 86 92 25x12 troca 12 25 37 48 33 57 86 92 25x37 12 25 37 48 33 57 86 92 37x48 12 25 37 48 33 57 86 92 48x33 troca 12 25 37 33 48 57 86 92 48x57 12 25 37 33 48 57 86 92 final passo 3 Idem para 57. n 4 (= 4) 12 25 37 33 48 57 86 92 12x25 12 25 37 33 48 57 86 92 25x37 12 25 37 33 48 57 86 92 37x33 troca 12 25 33 37 48 57 86 92 37x48 12 25 33 37 48 57 86 92 final do passo 4 Idem para 48. Não irá trocar mais. Veja isto no próximo passo
Bubble Sort Passos 5, 6 e 7 n 5 (= 3) Passo 5 sem troca! Os dois próximos passos são desperdícios! 12 25 33 37 48 57 86 92 12x25 12 25 33 37 48 57 86 92 25x33 12 25 33 37 48 57 86 92 33x37 12 25 33 37 48 57 86 92 final do passo 5 Idem para 37. n 6 (= 2) 12 25 33 37 48 57 86 92 12x25 12 25 33 37 48 57 86 92 25x33 12 25 33 37 48 57 86 92 final do passo 6 Idem para 33. n 7 (= 1) 12 25 33 37 48 57 86 92 12x25 12 25 33 37 48 57 86 92 final do passo 7 Idem para 25 e, conseqüentemente, 12. 12 25 33 37 48 57 86 92 final da ordenação
Código bolhaint Ordem Crescente bolhaint(vetor v de inteiros, n) i = n-1 para cada i, enquanto i>0 hatroca= 0 j = 0 para cada j, enquanto j<i se compint(v j,v j+1 ) é VERDADE troca v j e v j+1 hatroca= 1; // marca troca incrementa j de 1 if hatroca é zero retorna decrementa i de 1 static int compint(int a, int b) return a > b; void bolhaint(int * v, int n) int i,j,hatroca; int temp; for (i=n-1;i>0;i--) hatroca= 0; for (j=0;j<i;j++) if (compint(v[j],v[j+1])) temp = v[j]; v[j] = v[j+1]; troca v[j+1] = temp; hatroca= 1; if (hatroca==0) return; Para outros tipos, só muda o que está em vermelho e itálico
Código bolhastr Ordem Crescente void bolhastr(char ** v, int n) int i,j,hatroca; char * temp; for (i=n-1;i>0;i--) hatroca= 0; for (j=0;j<i;j++) if (compstr(v[j],v[j+1])) temp = v[j]; v[j] = v[j+1]; troca v[j+1] = temp; hatroca= 1; if (hatroca==0) return; int main(void) char * v[] = "daniel","ana",...,;... bolhastr(tab, N);... static int compstr(char * a, char * b) return (strcmp(a,b) > 0); strcmp(x,y) é uma função da biblioteca de strings que retorna o seguinte: < 0 se o primeiro caractere que não casa tem um valor mais baixo em x do que em y 0 se os conteúdos dos dois strings são iguais >0 se o primeiro caractere que não casa tem um valor mais alto em x do que em y Por ex.: strcmp("daniel","ana") retorna > 0 strcmp("ana","ana") retorna < 0 (porque 'A' é menor que 'a' na tabela ASCII
Algoritmo Genérico void bolhagen(void * v, int n, int tam, int(*comp)(const void *, const void *)) int i,j,hatroca; void * p1; void * p2; for (i=n-1;i>0;i--) hatroca= 0; for (j=0;j<i;j++) p1 = acessa(v,j,tam); p2 = acessa(v,j+1,tam); if (comp(p1,p2)) troca(p1,p2,tam); void ordenapessoa(int n, Pessoa ** v) hatroca= 1; bolhagen(v,n,sizeof(pessoa *),comppessoa); if (hatroca==0) return; Assunto Avançado static void * acessa(void * v,int i,int tam) char * t = (char *)v; // char = 1 byte t += tam*i; return (void *)t; static void troca(void * a, void * b, int tam) char temp; char * v1 = (char *)a; // troca byte a byte char * v2 = (char *)b; int i; for (i=0; i<tam; i++) temp = v1[i]; v1[i] = v2[i]; v2[i] = temp; static int comppessoagen(const void * a, const void * b) Pessoa ** aa = (Pessoa **)a; Pessoa ** bb = (Pessoa **)b; int cmp = strcmp((*aa)->nome,(*bb)->nome); return (cmp>0 (cmp==0 && (*aa)->idade > (*bb)->idade));
Selection Sort (Ordenação por Seleção)
Conceito do Selection Sort Ordem Crescente - Supomos que o maior elemento (max) é o que está na posição 0 - A partir da posição 1, procuramos se há alguém maior (max) do que o valor na posição 0 - trocamos este máximo pelo último - Deslocamos o fim do vetor para 1 (uma) posição à esquerda - Repetimos o processo fim fim fim Note que o processo vai dividindo o vetor em uma parte ordenada e outra não ordenada. fim Um processo equivalente é trabalhar com o mínimo e trocar com o primeiro. Neste caso, o vetor ordenado vai se construindo à esquerda. fim
Selection Sort void selectionint(int * v, int n) int fim, imax, i; int temp; for (fim = n-1; fim > 0; fim--) imax= 0; /* indice do maior*/ for (i=1;i<=fim;i++) if ( v[i]> v[imax] ) imax = i; temp = v[fim]; v[fim] = v[imax]; v[imax] = temp; Implemente a versão que trabalha com o mínimo! troca Ou usando uma função para um critério genérico critério if ( compint(v[i], v[imax]) ) static int compint(int a, int b) return a > b;
Complexidade (assunto avançado)
Eficiência e Complexidade Geralmente avaliamos algoritmos em termos das seguintes métricas: O quão rápido o algoritmo executa (eficiência de tempo) O quanto de memória ele requer (eficiência de espaço) Estas métricas dependem de: 1. Qualidade do código; 2. Natureza do processador; 3. Dados de entrada. Supondo que o algoritmos têm a melhor qualidade de código possível e rodam num mesmo excelente processador, vamos analisar a questão dos dados. Ao invés de calcular exatamente quanto tempo e quanto espaço consome um determinado algoritmo, vamos estimar um valor limite superior. E mais: nos slides a seguir, vamos analisar apenas o tempo.
Complexidade de Tempo O tempo gasto por um algoritmo é função do tamanho n da entrada de dados e depende do número de operações que cada passo do algoritmo executa. T(n) indica a dimensão este tempo de uma forma geral (e independente de hardware) No caso do algoritmo Bubble, numa análise aproximada, o passo 1 faz n-1 comparações, o passo 2 faz n-2 comparações,.... De maneira aproximada, temos: T ( n) = ( n 1) + ( n 2) + + 2 + 1 Isto é igual à soma de uma série aritmética de m termos, onde m = n-1: (n-1) + (n-2) +... + 2 + 1 Série Aritmética S m = m(a 1 + a m )/2 a m a 1 m = n-1 S m = n(n-1)/2 1 1 T ( n) = n( n 1) / 2 = n 2 n 2 2 Para grandes valores de n (n ), esta função é limitada pela função bn 2, onde b é uma constante, chamada de upper bound, i.e.: 1 2 1 2 T ( n) = n n bn 2 2 Como regra geral, o termo de mais alta ordem de uma função domina sua taxa de crescimento (i.e., na prática, suprimimos constantes multiplicativas e termos de baixa ordem para descrever o comportamento limite de uma função) Dizemos, então, que o algoritmo Bubble tem uma complexidade de tempo quadrática e usamos a notação do Big O para indicar este limite superior: O(n 2 )
Complexidades Comuns (notação Big O) Notação Big O O(1) O(log n) O(n) O(n log n) O(n 2 ) Tempo constante Tempo logarítmico Tempo linear Tempo loglinear (ou quaselinear) Tempo quadrático Limite n 2 T ( n) = 1 2 n 2 Quase linear 1 2 n n
Diferença entre n e log n tamanho 10 60 600 3 600 86 400 2 592 000 946 080 000 94 608 000 000 O(n) 10 seg 1 min 10 min 1 h 1 dia 1 mês 1 ano 100 anos O(log n) 3 seg 6 9 12 16 21 30 36 seg
Outras Considerações sobre Complexidade Na análise de complexidade, procuramos o Pior Caso, o Melhor Caso e o Caso Médio Bubble Sort tem o Melhor Caso quando a lista já está ordenada. Neste caso, a complexidade de tempo é O(n). Uma única passada resolve. O Caso Médio para Bublle Sort também é O(n 2 ) Entre algoritmos de mesma complexidade, há diferenças em eficiência Por exemplo, Selection Sort é similar ao Bubble Sort e ambos tem O(n 2 ), isto é: ele faz o mesmo número de comparações que o Bubble. Entretanto, Selection Sort tem um menor número de comparações e tende a ser aproximadamente 2 vezes mais eficiente. Eficiência não é a mesma coisa que complexidade! O Bubble Sort com ou sem o flag de interrupção é O(n 2 ), sendo o com flag mais eficiente. Em geral uma única operação (ou comando) tem um tempo de execução de O(1) Um loop (laço) que é repetido n vezes tem O(n) Dois laços aninhados (nested loops) indo de 1 a n tem O(n 2 ) Se um algoritmo tem vários passos de complexidades diferentes, a complexidade do algoritmo como um todo é dado pelo máximo. Por exemplo, um algoritmo com 3 passos de O(n 2 ), O(n 2 ) e O(n), tem complexidade O(n 2 ) Um algoritmo com 3 linhas de commandos, sem loop, tem 3 passos O(1), O(1) e O(1), o que leva a um algoritmo de complexidade O(1).
Complexidade do Quick Sort Pior Caso A primeira etapa do algortimo de sort é colocar o pivô x na posição que divide o vetor v entre os elementos menores que x e os maiores que x : k n-k x Esta etapa (que caminha pelo vetor, compara e troca valores) consome um tempo cn (*). As outras duas etapas são as chamadas recursivas. Temos, então: T ( n) = T ( k) + T ( n k) + cn PIOR CASO: pivô x é o menor, em cada passo (**). Neste caso, k =1 e n-k = n-1 passo 1: passo 2: passo 3:... T ( n) = T (1) + T ( n 1) + cn TT nn = TT 1 + TT 1 + TT nn 2 + cc nn 1 + cccc = TT nn 2 + 2TT 1 + cc nn 1 + nn T ( n) = [ T (1) + T ( n 3) + c( n 2)] + 2T (1) + c( n 1+ n) = T ( n 3) + 3T (1) + c( n 2 + n 1+ n) passo i: T ( n) = T ( n i) + it (1) + c( n i + 1+ + n 1+ n) A soma da série aritmética de i termos n i+1,...,n 1,n é: S i = i( 2n + 1 i) / 2 e considerando que vamos até n i=1, i.e. i = n 1, temos: T ( n) = nt (1) + c( n 1)( n + 2) 2 2 A complexidade é, portanto: T ( n) = nt (1) + c( n 1)( n + 2) dn O( n ) O mesmo vale para pivô x sendo o maior. (*) o maior valor de c seria para um código que percorresse uma vez o vetor, guardando os elementos menores do que x (e o próprio x) em um vetor temporário, percorresse novamente o vetor, guardando os elementos maiores do que x, e finalmente copiasse o vetor temporário para o vetor original. Neste caso, o tempo seria 4n (i.e. c = 4). (**) Neste caso, sempre haverá apenas um único elemento em um subvetor e todos os outros no outro subvetor. Trata-se de uma situação muito desbalanceada. O mesmo ocorre se o pivô é sempre o maior em cada passo.
Complexidade do Quick Sort Melhor Caso MELHOR CASO: pivô x divide vetor em duas partes iguais, em cada passo. Neste caso, k =n/2 e n-k = n/2 passo 1: passo 2: passo 3:... passo i: T ( n) = T ( n / 2) + T ( n / 2) + cn = 2T ( n / 2) + cn 2 2 T ( n) = 2[2T ( n / 4) + cn / 2] + cn = 2 T ( n / 2 ) + 2cn 2 3 2 3 3 T ( n) = 2 [2T ( n / 2 ) + cn / 2 ] + 2cn = 2 T ( n / 2 ) + 3cn i i T ( n) = 2 T ( n / 2 ) + icn considerando que vamos até n/2 i = 1, i.e. n = 2 i, ou seja, i = log n, temos: T ( n) = nt (1) + cnlog n dnlog n O n ( log n) A mediana é o valor separando a metade mais alta de uma amostra de dados da metade mais baixa. Pode ser entendida como o valor do meio. Com 3 valores, o cálculo é trivial. Podemos também provar que no CASO MÉDIO (para todas as possíveis configurações de pivô), quick sort tem uma complexidade de tempo de O(n log n). Ironicamente, vetores já ordenados (ou quase ordenados) e com o pivô sendo o primeiro (ou o último) elemento representam situações tipo pior caso. Quanto mais balanceada a partição, melhor! Melhoras no algoritmo podem ser feitas no sentido de reduzir a chance de ocorrer o pior caso. Uma idéia é usar um gerador de números aleatórios para pegar um pivô e trocá-lo com o item em v[0]. Outra idéia é encontrar a mediana de 3 elementos (o primeiro, o do fim e o do meio) e trocá-la com o item em v[0] (que passará a ser o pivô). Por exemplo, v= 4,2,8,1,3,6 tem first = v[0]= 4, last = v[5] = 6, middle = v[(0+5)/2] = v[2] = 8, e a mediana é o 6. Selection Sort pode ser mais vantajoso para listas pequenas ( 30) ou quase ordenadas. não use quick sort para arrays pequenos! Portanto, uma idéia é fazer o algoritmo quick sort usar selection sort quando o subvetor tiver tamanho 30 (ou outro limite, e.g. 10). Acredita-se que isto economiza cerca de 15% no tempo de processamento.