Algumas Noções de Complexidade Luís Lopes DCC-FCUP Estruturas de Dados Algoritmo I um método para resolver um problema I especifica, passo a passo, as operações a realizar I as funções que manipulam as estruturas de dados que definimos são também algoritmos I e.g., inserir elemento no fim de uma lista, encontrar o índice de um elemento na lista, remover um elemento no início da lista I outros algoritmos operam sobre estruturas de dados completas I e.g., ordenar um array, multiplicar matrizes, inverter uma lista Eficiência de um Algoritmo I normalmente mede-se com base em 2 critérios em função do tamanho do input n I taxa de crescimento do tempo de execução (complexidade temporal) I taxa de crescimento do espaço usado na memória (complexidade espacial) I vamos centrar a nossa atenção na primeira métrica Análise da Eficiência I descrição matemática do algoritmo em vez de implementação I caracteriza o tempo de execução como uma função do tamanho do input I tem em conta todos os possíveis inputs I permite avaliar eficiência de forma independente do ambiente de hardware/software Exemplo Estimativa do tempo de execução em função de n i n t findMax ( i n t a [] , i n t n ) { i n t max = a [0]; // 2 operações int i = 1; // 1 operação w h i l e ( i <= n - 1) { // n operações // n-1 vezes ciclo i f ( a [ i ] > max ) // 2 operações max = a [ i ]; // 2 operações i = i + 1; // 2 operações } r e t u r n max ; // 1 operação } Pressupostos I contabilizar número de instruções primitivas I memória de acesso aleatório e ilimitada e.g., max = a[0] é composta por 2 operações primitivas I I 1 leitura de a[0] + 1 atribuição a max Exemplo (cont.) I caso mais favorável (a[0] é o maior elemento): T (n) = 2 + 1 + n + 4(n − 1) + 1 = 5n operações primitivas I pior caso: T (n) = 2 + 1 + n + 6(n − 1) + 1 = 7n − 2 I caso médio depende da distribuição do input I o pior caso dá-nos um limite superior do tempo de execução I neste caso T (n) ∝ n Taxa de Crescimento de T (n) I o tempo de execução, T (n), pode ser afectado pela alteração do ambiente hardware/software I tal não acontece se considerarmos a taxa de crescimento de T (n): a variação de T (n) quando se aumenta o valor de n. I para a função findMax(), T (n) é limitado por 2 funções lineares em n I diz-se que o crescimento de T (n) é linear Taxas de crescimento logarítmica linear n-logarítmica quadrática cúbica exponencial log n n n log n n2 n3 an (a > 1) Crescimento de n log2 n 2 1 8 3 16 4 ... ... 1024 10 algumas funções: √ n n n log2 n 1.4 2 2 2.8 8 24 4.0 16 64 ... ... ... 32 1024 10240 n2 4 64 256 ... > 106 n3 8 512 4096 ... > 109 2n 4 256 65536 ... 308 > 10 Taxas de crescimento Se assumirmos que cada operação pode ser executada em 1µs (micro-sec), qual será o maior problema (função de n) para um programa que execute em 1 seg., 1 min., 1 hora? T (n) 400n 20ndlog ne 2n2 n4 2n Tamanho máximo problema (n) 1 seg. 1 min. 1 hora 2500 150,000 9,000,000 4096 166,666 7,826,087 707 5,477 42,426 31 88 244 19 25 31 No caso de crescimento exponencial, apenas conseguimos tratar problemas para dimensões muito pequenas de n. Ordem de Complexidade Assimptótica: O() Sejam f (n) e g(n) funções de N0 7−→ R. f (n) é O(g(n)) (ou seja, f (n) é da ordem de g(n)) se ∃c > 0, ∃n0 ≥ 1 : f (n) ≤ cg(n), ∀n ≥ n0 Interpretação gráfica: I O(g(n)) corresponde ao limite superior da taxa de crescimento de f (n). I dizer que f (n) é O(g(n)), significa dizer que f (n) não cresce mais do que g(n). Exemplo I 2n + 10 é O(n) porque: 2n+10 ≤ cn ⇔ (c−2)n ≥ 10 ⇔ n≥ 10 (c − 2) para c = 3 e n0 = 10 verifica-se sempre que 2n + 10 ≤ cn I outras funções: 20n3 + 10n log n + 5 ak nk + ak−1 nk−1 + . . . + a0 3 log n + log log n 2100 5 n — — — — — O(n3 ) O(nk ) O(log n) O(1) O( n1 ) Algumas Propriedades Úteis I Se h(n) ∼ O(f (n)), então αh(n) (com α > 0) ∼ O(f (n)) I Se h1 (n) ∼ O(f (n)) e h2 (n) ∼ O(g(n)), então h1 (n) + h2 (n) ∼ O(f (n) + g(n)) I Se h1 (n) ∼ O(f (n)) e h2 (n) ∼ O(g(n)), então h1 (n)h2 (n) ∼ O(f (n)g(n)) I Se h(n) ∼ O(f (n)) e f (n) ∼ O(g(n)), então h(n) ∼ O(g(n)) Exemplo: Método da Bolha (Bubble-Sort) v o i d bubbleSort ( i n t a [] , i n t n ) { i n t i , j , tmp ; f o r ( i n t i = 0; i < n ; i ++) // n iterações f o r ( i n t j = 0; j < n -1 - i ; j ++) // n-i iterações i f ( a [ j ] > a [ j +1]) { tmp = a [ j +1]; a [ j +1] = a [ j ]; a[j] = tmp ; } } Efectivamente, o número de iterações total é a soma das iterações que o ciclo j faz para cada valor de i, i.e. T (n) ∼ Pn−1 i=1 (n − i) = (n − 1) + (n − 2) + . . . + 2 + 1 = 2 = n(n−1) = n2 − n2 2 ⇒ T (n) é O(n2 ) Exemplo: Factorial i n t factorial ( i n t n ) { i f ( n == 0 || n == 1) r e t u r n 1; else r e t u r n n * factorial (n -1); } ∴ T (n) = // O(1) // O(1) // O(1) + T(n-1) α + T (n − 1), se n > 1 β, se n ≤ 1 assumindo n ≥ 2: T (n − 1) = α + T (n − 2) ⇒ T (n) = 2α + T (n − 2) em geral: T (n) = iα + T (n − i), n > i se i = n − 1, então T (n) = α(n − 1) + T (1) = α(n − 1) + β donde podemos concluir que T (n) é O(n) Exemplo: Pesquisa Binária i n t binarySearch ( i n t [] values , i n t val , i n t low , i n t high ) { i f ( high < low ) r e t u r n -1; else { i n t half = low + ( high - low ) / 2; i f ( val == values [ half ] ) ) r e t u r n half ; e l s e i f ( val < values [ half ] ) ) r e t u r n binarySearch ( values , val , low , half - 1); else r e t u r n binarySearch ( values , val , half + 1 , high ); } } I em cada passo do ciclo, o intervalo reduz-se a metade. I para um n dado, o algoritmo pára quando atingir um intervalo de tamanho ≤ 1 Exemplo: Pesquisa Binária I qual dos valores n/2, n/4, n/8, . . . , n/2k . . . será o primeiro a ser ≤ 1? I temos de resolver n/2k ≤ 1, o que dá um número de passos k ≥ log2 n I tomando o mais pequeno inteiro k que satisfaz k ≥ log2 n (k = dlog2 ne) sabemos que ao fim de um máximo de k iterações encontramos o valor no array ou podemos concluir que o valor que procuramos não existe I deduzimos assim que T (n) = dlog2 ne ou seja T (n) é O(log n)