Algoritmos e complexidade

Pesquisar, justificar uma redução e comparar crescimento do trabalho para entradas maiores.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

Procuramos 14 em [2, 5, 8, 11, 14, 17, 20]; podemos visitar os valores desde o início ou usar a ordenação para excluir metade dos candidatos. O contrato deve dizer se queremos uma posição qualquer ou a primeira ocorrência, e como representar a ausência.

Pesquisa linear e pesquisa binária

A pesquisa linear visita os elementos por ordem até encontrar o alvo; escolhemos neste exemplo devolver um índice, ou None quando o alvo falta, convenção que evita confundir ausência com o índice zero.

A pesquisa binária exige uma lista ordenada. Mantém um intervalo de candidatos, compara com o meio e conserva só a metade possível.

Comparar duas pesquisas
def linear(valores, alvo):
    for i, valor in enumerate(valores):
        if valor == alvo:
            return i
    return None

def binaria(valores, alvo):
    inicio, fim = 0, len(valores) - 1
    while inicio <= fim:
        meio = (inicio + fim) // 2
        if valores[meio] == alvo:
            return meio
        if alvo < valores[meio]:
            fim = meio - 1
        else:
            inicio = meio + 1
    return None

valores = [2, 5, 8, 11, 14, 17, 20]
print(linear(valores, 14), binaria(valores, 14))
print(binaria(valores, 9), binaria([], 2))
Resultado

Executa o código para ver a saída e os gráficos.

Intervalo de índicesMeio e valorDecisão ao procurar 14
[0, 6]3, valor 11Excluir até 3, porque 14 é maior
[4, 6]5, valor 17Excluir a partir de 5, porque 14 é menor
[4, 4]4, valor 14Encontrado

A condição inicio <= fim inclui o último candidato, e atualizar para meio - 1 ou meio + 1 exclui o meio já comparado para garantir progresso.

Se o alvo faltar, o intervalo torna-se vazio. A ordenação é a razão para descartar cada metade, e numa lista desordenada essa decisão pode perder o alvo.

Com valores repetidos, esta função pode devolver qualquer posição com o alvo. Se o contrato pede a primeira, encontrar uma ocorrência ainda não chega; tens de continuar à esquerda ou usar outra estratégia.

Contar operações

Conta primeiro num caso concreto: com 7 elementos, a pesquisa linear pode fazer 7 comparações; a binária do exemplo encontra 14 em 3 iterações. Se n é o comprimento da lista, a pesquisa linear faz até n comparações de igualdade. A binária reduz o número de candidatos aproximadamente a metade por iteração, fazendo um número de iterações proporcional a log⁡2n\log_2 n.

A notação O(g(n))O(g(n)) é um limite superior do crescimento do trabalho para entradas suficientemente grandes. Por exemplo, 3n+23n+2 é O(n)O(n): a partir de n=1n=1, nunca ultrapassa 5n5n. Também é O(n2)O(n^2), mas esse limite é menos informativo. Para exprimir um limite superior e inferior da mesma ordem, usa-se Θ\Theta; 3n+23n+2 é Θ(n)\Theta(n). Dizer apenas O(n2)O(n^2) não prova que um algoritmo precise de trabalho quadrático.

Crescimento típicoSituação
O(1)O(1)Aceder a uma posição de lista
O(log⁡n)O(\log n)Pesquisa binária numa lista ordenada
O(n)O(n)Percorrer toda a lista uma vez
O(nlog⁡n)O(n\log n)Ordenação por comparação como limite geral de sorted
O(n2)O(n^2)Comparar todos os pares de n elementos

Esta análise é um complemento para comparar estratégias. O tamanho da entrada e a operação contada têm de estar definidos. Os inteiros Python podem ter tamanho arbitrário; tratar qualquer operação aritmética como constante é uma simplificação que exige valores de tamanho limitado.

Ciclos sucessivos e aninhados

Dois percursos sucessivos de n elementos fazem trabalho proporcional a 2n2n, portanto linear, enquanto dois percursos aninhados, ambos de n, fazem n2n^2 visitas. Não é o número de ciclos escritos que decide o custo, é quantas vezes o corpo executa.

n = 4
pares = 0
for i in range(n):
    for j in range(i + 1, n):
        pares += 1
print(pares)

A saída é 6: para i = 0, visitamos j = 1, 2, 3; para i = 1, visitamos j = 2, 3; para i = 2, só j = 3; para i = 3, não há parceiros. As visitas são 3+2+1+0=63+2+1+0=6 e, em geral, somamos (n−1)+(n−2)+⋯+1=n(n−1)/2(n-1)+(n-2)+\cdots+1=n(n-1)/2, logo o crescimento é quadrático embora o ciclo interior não percorra sempre n posições.

Custos das operações mais usadas

Uma fatia de k elementos de lista cria outra lista e custa trabalho proporcional a k. Inserir no início desloca elementos. append tem custo amortizado constante.

Ver custos com hipóteses

Algumas inserções com append exigem aumentar a capacidade. Pertença numa lista pode visitar todos os elementos; num dicionário ou conjunto, espera-se custo médio constante com hipóteses adequadas sobre hashes, mas o pior caso pode ser linear.

Para uma única procura, ordenar primeiro costuma acrescentar trabalho desnecessário. Para muitas procuras sobre a mesma lista, a preparação pode compensar.

Uma medição em segundos compara uma implementação e um ambiente; a análise de crescimento explica o que muda quando a entrada aumenta. Nenhuma destas medidas substitui a prova de que a resposta está correta.

Para aprofundar invariantes de ciclos e crescimento do trabalho, tens este complemento opcional de Rodrigo Dias:

Abrir no YouTube

Exercícios

Construir um contraexemplo à pesquisa binária

Aplica a função binaria da lição à lista [4, 1, 8, 2] para procurar 4. Regista os intervalos visitados e o resultado. Usa esse percurso para explicar por que não basta os valores serem distintos. Se ordenares a lista, a posição devolvida ainda identifica a posição na entrada original?

Pista
Um valor maior que o meio pode estar à sua esquerda nesta lista.
Solução

No intervalo [0, 3], o meio é 1 e contém 1. Como 4 é maior, a função conserva [2, 3] e perde o índice 0, onde estava o alvo. O meio seguinte é 2 e contém 8; a função passa para [2, 1], um intervalo vazio, e devolve None.

A exclusão só é válida com a lista ordenada. A pesquisa linear encontra 4 no índice 0. Ordenando, obtemos [1, 2, 4, 8] e encontramos 4 no índice 2, que é uma posição da lista nova e não da entrada original.

Confere a tua resposta:

  • Segui os meios 1 e 2, com valores 1 e 8.
  • Obtive None apesar de 4 existir.
  • Justifiquei a pré-condição de ordenação.
Voltar à explicação
Seguir uma pesquisa sem resultado

Segue a função binaria da lição ao procurar 9 em [2, 5, 8, 11, 14, 17, 20]. Regista inicio, fim, meio e o valor do meio em cada iteração. Indica o resultado, o número de iterações e o número de comparações entre o alvo e um valor da lista. Conta cada == e cada < executado, sem contar a condição do while.

Pista
Numa iteração sem igualdade, a função também executa o teste <.
Solução
IntervaloMeioValorIntervalo seguinte
[0, 6]311[0, 2]
[0, 2]15[2, 2]
[2, 2]28[3, 2]

O último intervalo é vazio porque inicio > fim, por isso a função devolve None. Houve três iterações e seis comparações entre valores: em cada iteração, a igualdade falhou e foi preciso avaliar <. Definir a operação contada evita confundir iterações com comparações.

Confere a tua resposta:

  • Segui [0, 6], [0, 2] e [2, 2].
  • Terminei em [3, 2] e None.
  • Distingui 3 iterações de 6 comparações entre valores.
Voltar à explicação
Contar pares distintos

Para n = 5, quantas visitas faz o ciclo sobre todos os pares i < j da lição?

Pista
Cada primeiro índice tem menos parceiros à direita.
Mais uma pista
As contagens são 4, 3, 2, 1 e 0.
Solução
4 + 3 + 2 + 1 = 10, o mesmo que 5 × 4 / 2. O crescimento é quadrático, apesar de não visitar os 25 pares possíveis.

Erros frequentes

Contar também pares iguais ou as duas ordens do mesmo par.
Voltar à explicação
Devolver a primeira posição

Define primeira_posicao(valores, alvo): recebe uma lista, que pode estar desordenada, e devolve o índice da primeira ocorrência do alvo, ou None se faltar. Conserva a entrada. Justifica para ti por que podes parar quando encontras o primeiro igual.

Python
def primeira_posicao(valores, alvo):
    return None
Testes e resultado

Verifica o código para comparar o resultado com os testes.

Testes
  1. Desordenada e com repetição
    Código do teste
    print(primeira_posicao([8, 3, 8, 1], 8))
    Esperado:
    0
  2. Repetição depois do início
    Código do teste
    print(primeira_posicao([2, 7, 7, 9], 7))
    Esperado:
    1
  3. Última posição
    Código do teste
    print(primeira_posicao([3, 1, 4], 4))
    Esperado:
    2
  4. Alvo ausente
    Código do teste
    print(primeira_posicao([2, 7], 5))
    Esperado:
    None
  5. Lista vazia
    Código do teste
    print(primeira_posicao([], 1))
    Esperado:
    None
  6. Entrada preservada
    Código do teste
    xs = [8, 3, 8, 1]
    print(primeira_posicao(xs, 3))
    print(xs)
    Esperado:
    1
    [8, 3, 8, 1]

Pista

Sem ordenação, não podes descartar metade por comparar com o meio.

Mais uma pista

enumerate fornece o índice e o valor. Devolve o índice no primeiro igual; só devolve None depois do percurso.

Solução

O percurso começa no índice zero e avança por ordem. Quando encontra o alvo, todas as posições anteriores já foram excluídas. No pior caso faz n comparações para uma lista de n elementos.

def primeira_posicao(valores, alvo):
    for indice, valor in enumerate(valores):
        if valor == alvo:
            return indice
    return None

Erros frequentes

Ordenar a entrada e devolver o índice novo, usar zero para ausência ou continuar a procurar e devolver a última ocorrência.

Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.