Início / Complexidade de Tempo & Espaço / Análise de Loops

⏱️ Análise de Loops

Antes de falar em notações mais formais, vamos aprender a medir o esforço de um algoritmo de forma simples.

A ideia aqui é contar quantas vezes cada parte do código é executada e representar isso como uma expressão em função de n.

Pense nisso como transformar o algoritmo em uma "fórmula de esforço".

A regra de ouro na análise de loops é: quantas vezes o bloco de código será executado em relação ao tamanho da entrada n?

A. O loop simples

Este é o padrão mais comum. O algoritmo percorre a entrada uma única vez.

def sum_elements(items: list) -> int:
    total = 0            # Executa 1 vez
    for num in items:    # Controla n iterações
        total += num     # Executa n vezes
    return total         # Executa 1 vez

Análise

Podemos representar o esforço total como:

1 (inicialização) + n (repetições do loop) + 1 (retorno)

Ou seja:

1 + n + 1

Simplificando:

n + 2

O ponto aqui não é decorar a expressão final, mas entender como o número de execuções depende de n.

Para visualizar esse crescimento, veja o gráfico da função f(n) = n + 2:

Gráfico da função f(n) = n + 2 mostrando crescimento linear

B. Loops sequenciais

O que acontece se tivermos dois loops, um depois do outro?

def process_data(items: list) -> None:
    for x in items:
        print(x)

    for y in items:
        print(y)

Análise

O primeiro loop executa n vezes. O segundo também executa n vezes.

Esforço total:

n + n = 2n

Neste momento, ainda estamos interessados na contagem exata.

Para visualizar esse comportamento, veja o gráfico comparando f(n) = n e f(n) = 2n:

Gráfico comparando f(n) = n e f(n) = 2n

C. Loops aninhados

Aqui a escala começa a pesar. Um loop dentro de outro significa que, para cada iteração do primeiro, o segundo roda inteiro.

def compare_all(items: list) -> None:
    for i in items:          # Roda n vezes
        for j in items:      # Roda n vezes para cada i
            print(i, j)

Análise

Para cada uma das n iterações do primeiro loop, o segundo executa n vezes.

Esforço total:

n × n = n²

Se n = 10, temos 100 execuções do bloco interno. Se n = 1.000, saltamos para 1.000.000 execuções.

É por isso que loops aninhados em listas grandes rapidamente podem se tornar caros.

Para visualizar esse crescimento, veja o gráfico da função f(n) = n²:

Gráfico da função f(n) = n^2

Um resumo prático

Duas regras aparecem com bastante frequência:

  • Se os blocos acontecem um depois do outro, somamos os esforços.
  • Se um bloco acontece dentro do outro, multiplicamos os esforços.

Em outras palavras:

  • Dois loops separados sobre a mesma entrada costumam gerar algo como n + n.
  • Um loop dentro de outro sobre a mesma entrada costuma gerar algo como n × n.

Essa forma de contar execuções é a base para analisar algoritmos com mais segurança. O mais importante é desenvolver o reflexo de olhar para o código e perguntar:

  • Quantas vezes este trecho roda?
  • O que depende de n?
  • Estou somando repetições ou multiplicando repetições?

Nas próximas seções, veremos que, para comparar algoritmos, não precisamos de números exatos como n+2 ou 2n – apenas da ordem de crescimento. Isso nos leva à Notação Big‑O.