Complexidade e invariantes
Notação assintótica para tempo e espaço, e invariantes para provar a correção de ciclos.
Nesta página
Dois programas fazem o mesmo e um deles demora o dobro. Qual escolhes? A resposta séria depende do tamanho da entrada: o dobro numa lista de dez elementos é ruído, numa lista de dez milhões é a diferença entre responder hoje e responder amanhã. A análise de complexidade mede como o custo cresce com a entrada, e os invariantes provam que o programa calcula o que promete. São as duas ferramentas que vais usar em todas as páginas desta cadeira.
O que conta como custo
Fixar primeiro o que se mede: o tamanho da entrada (número de elementos, de vértices, de bits) e o custo que interessa (comparações, acessos à memória, operações elementares). O resto é ruído de máquina e ignora-se. Quando dissermos que um algoritmo é , estamos a dizer que o número de operações elementares cresce no máximo com o quadrado de , a menos de constantes.
A notação Big-O descreve um limite superior: significa que, a partir de certo , fica abaixo de uma constante vezes . Há mais duas que vais encontrar: para limite inferior (o algoritmo custa pelo menos isto) e quando os dois limites coincidem (o custo é exatamente desta ordem). Na prática da cadeira, domina: queres garantir que o programa aguenta o pior caso.
Os tempos que interessam, do melhor para o pior: constante, logarítmico, linear, , quadrático e exponencial. Decora a intuição, não a lista: logaritmo é “cortar o problema a metade em cada passo”, é “cortar e depois juntar”, quadrático é “comparar tudo com tudo”.
Linear contra binária em
Recorda a pesquisa linear e binária: a linear percorre o vetor até encontrar, a binária exige o vetor ordenado e descarta metade a cada comparação. Conta comparações no pior caso com .
Na linear, o pior caso é o elemento estar na última posição ou não existir: comparações. Na binária, cada comparação elimina metade dos candidatos: depois de comparações restam elementos, e o processo termina quando resta um, ou seja . Como e , bastam comparações. Mil contra dez, e a diferença aumenta com : para um milhão, a linear faz um milhão de comparações e a binária faz , porque .
O preço da binária está fora da pesquisa: alguém teve de ordenar o vetor primeiro, e ordenar custa pelo menos . Se pesquisares uma vez num vetor desordenado, ordenar mais pesquisar sai mais caro que a linear. A análise decide consoante o uso, não em abstrato.
Espaço também conta
A complexidade espacial mede a memória extra além da entrada. A pesquisa binária iterativa usa de espaço extra (algumas variáveis); a versão recursiva usa pela pilha de chamadas. O mergesort, que vais seguir na próxima página, precisa de um vetor auxiliar de tamanho : tempo , espaço . O quicksort no próprio vetor usa de pilha em média. Quando a memória é curta, esta coluna da tabela pesa tanto como a do tempo.
Invariantes: provar o ciclo
Complexidade diz quanto custa; correção diz que o resultado está certo. Para ciclos, a técnica é o invariante: uma afirmação sobre as variáveis que é verdadeira antes de cada iteração e que, combinada com a condição de saída, implica o resultado.
Toma a soma dos primeiros inteiros:
int soma = 0;
for (int i = 1; i <= n; i++) {
soma += i;
}
O invariante é: antes da iteração com valor , vale . Verifica por indução sobre , como aprendeste em indução: para , e . Se vale para , a iteração soma e obtém , que é exatamente o invariante para . Quando o ciclo termina, , e o invariante dá , a fórmula fechada. O invariante transforma “o ciclo parece somar tudo” numa prova.
Para levar para a próxima página
A análise de um algoritmo não é a mesma coisa que a classe de um problema: um algoritmo concreto corre em enquanto o problema pode admitir solução melhor. A distinção entre “este programa custa isto” e “este problema exige pelo menos isto” reaparece em complexidade, com as classes P e NP. Já a seguir, aplica estas contas à pesquisa e ordenação.