💻 Algoritmos e Programação — Notas

Complexidade  |  Ordenação  |  Busca  |  Recursão  |  Python

Complexidade de Algoritmos e Notação Big-O

A complexidade mede como o tempo de execução ou uso de memória cresce conforme o tamanho da entrada (n). A notação Big-O descreve o pior caso.

NotaçãoNomeExemplon=1000
O(1)ConstanteAcessar lista[i]1 op
O(log n)LogarítmicaBusca binária~10 ops
O(n)LinearPercorrer lista1.000 ops
O(n log n)LinearítmicaMerge Sort~10.000 ops
O(n²)QuadráticaBubble Sort1.000.000 ops
O(2ⁿ)ExponencialSubconjuntosinviável
⚠ Regra prática
O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ). Em entrevistas, sempre justifique a complexidade do seu algoritmo.
Como calcular Big-O
1. Loop simples de 0 a n → O(n)
2. Loop dentro de loop (ambos até n) → O(n²)
3. Dividir o problema pela metade a cada passo → O(log n)
4. Constantes e termos menores são ignorados: O(3n + 5) = O(n)
# O(n) — percorre uma vez def soma_lista(lst): total = 0 for x in lst: # n iterações total += x # O(1) return total # total: O(n) # O(n²) — loop aninhado def tem_duplicata(lst): for i in range(len(lst)): # n for j in range(i+1, len(lst)): # n if lst[i] == lst[j]: return True return False # O(n²) # O(n) com set — muito melhor! def tem_duplicata_rapido(lst): return len(lst) != len(set(lst)) # O(n)

Espaço vs Tempo

Complexidade de tempo = quantas operações. Complexidade de espaço = quanta memória extra usa. Frequentemente há um trade-off entre os dois.

Exercício 1: Qual a complexidade de buscar um elemento em uma lista não ordenada?
O(n) — pior caso: percorre tudo sem encontrar.
Tem que checar cada elemento um a um. Não tem como pular.
Exercício 2: Qual a complexidade de acessar um dicionário Python por chave?
O(1) médio — dicionário é uma tabela hash internamente.
Hash mapeia a chave diretamente para a posição. Por isso dicionários são preferíveis a listas para busca.
Algoritmos de Ordenação
AlgoritmoMelhorMédioPiorEstável?Quando usar
Merge SortO(n log n)O(n log n)O(n log n)SimDados grandes, garantia de desempenho
Quick SortO(n log n)O(n log n)O(n²)NãoPrática — rápido na maioria dos casos
Insertion SortO(n)O(n²)O(n²)SimListas pequenas ou quase ordenadas
Bubble SortO(n)O(n²)O(n²)SimSó didático — nunca em produção
Selection SortO(n²)O(n²)O(n²)NãoSó didático
# Merge Sort em Python — O(n log n) def merge_sort(lst): if len(lst) <= 1: return lst meio = len(lst) // 2 esq = merge_sort(lst[:meio]) dir = merge_sort(lst[meio:]) return merge(esq, dir) def merge(esq, dir): resultado = [] i = j = 0 while i < len(esq) and j < len(dir): if esq[i] <= dir[j]: resultado.append(esq[i]); i += 1 else: resultado.append(dir[j]); j += 1 return resultado + esq[i:] + dir[j:] # Quick Sort em Python — O(n log n) médio def quick_sort(lst): if len(lst) <= 1: return lst pivo = lst[len(lst) // 2] esq = [x for x in lst if x < pivo] meio = [x for x in lst if x == pivo] dir = [x for x in lst if x > pivo] return quick_sort(esq) + meio + quick_sort(dir) # Python nativo — use sempre que possível! lista = [3,1,4,1,5,9,2,6] lista.sort() # in-place, O(n log n) — Timsort sorted(lista) # retorna nova lista
⚠ Python usa Timsort
O método sort() e a função sorted() do Python usam Timsort — híbrido de Merge Sort e Insertion Sort. É O(n log n) garantido e estável. Sempre prefira ao implementar do zero.
Exercício 3: Por que Quick Sort é O(n²) no pior caso?
→ Quando o pivô escolhido é sempre o menor ou maior elemento (lista já ordenada).
Cada partição fica com n-1 e 0 elementos — gera n recursões de tamanho n-1, n-2... = O(n²). Escolher pivô aleatório ou do meio minimiza esse risco.
Busca e Recursão
Busca Linear vs Busca Binária
# Busca Linear — O(n) — funciona em qualquer lista def busca_linear(lst, alvo): for i, v in enumerate(lst): if v == alvo: return i return -1 # Busca Binária — O(log n) — EXIGE lista ORDENADA def busca_binaria(lst, alvo): esq, dir = 0, len(lst) - 1 while esq <= dir: meio = (esq + dir) // 2 if lst[meio] == alvo: return meio elif lst[meio] < alvo: esq = meio + 1 else: dir = meio - 1 return -1 # Python nativo import bisect lst = [1, 3, 5, 7, 9, 11] idx = bisect.bisect_left(lst, 7) # busca binária nativa
⚠ Busca Binária exige ordenação
Busca binária é O(log n) mas só funciona em listas ordenadas. Se a lista não está ordenada: ordenar (O(n log n)) + buscar (O(log n)) = O(n log n). Para uma busca só, busca linear é melhor. Para muitas buscas, vale ordenar uma vez.
Recursão

Uma função recursiva chama a si mesma com um problema menor. Toda recursão precisa de: (1) caso base — para de chamar; (2) caso recursivo — reduz o problema.

# Fatorial — O(n) def fatorial(n): if n <= 1: # caso base return 1 return n * fatorial(n - 1) # caso recursivo # Fibonacci — O(2ⁿ) ingênuo, O(n) com memoização def fib(n, memo={}): if n <= 1: return n if n not in memo: memo[n] = fib(n-1, memo) + fib(n-2, memo) return memo[n] # Fibonacci — O(n) com programação dinâmica def fib_dp(n): if n <= 1: return n a, b = 0, 1 for _ in range(2, n+1): a, b = b, a + b return b # Soma de lista recursiva def soma(lst): if not lst: return 0 # caso base return lst[0] + soma(lst[1:]) # caso recursivo
⚠ Stack Overflow
Cada chamada recursiva ocupa espaço na pilha. Recursão muito profunda causa RecursionError em Python (limite padrão: 1000 chamadas). Para problemas grandes, prefira versão iterativa ou use memoização.
Programação Dinâmica

Técnica que resolve subproblemas menores e armazena os resultados para não recalcular. Transforma O(2ⁿ) em O(n).

Exercício 4: Qual a diferença entre recursão com memoização e programação dinâmica bottom-up?
Memoização (top-down): começa do problema grande, armazena resultados conforme desce. Bottom-up: começa dos casos base e vai subindo.
Ambas têm O(n) de tempo. Bottom-up evita overhead de chamadas recursivas e risco de stack overflow — preferível para n grande.
Python Essencial para Backend e AI
Estruturas de Dados em Python
# Lista — O(1) append, O(n) busca lst = [1, 2, 3] lst.append(4) # O(1) lst.insert(0, 0) # O(n) — desloca tudo lst.pop() # O(1) — remove o último lst.pop(0) # O(n) — remove o primeiro # Dicionário — O(1) médio para tudo d = {'a': 1, 'b': 2} d['c'] = 3 # inserir d.get('x', 0) # buscar com default d.items() # pares (chave, valor) # Set — O(1) médio, sem duplicatas s = {1, 2, 3} s.add(4) s.discard(1) inter = s & {2, 4} # interseção # Deque — O(1) nas duas extremidades (use como fila/pilha) from collections import deque fila = deque() fila.append(1) # enqueue — O(1) fila.appendleft(0) # O(1) — lista seria O(n) fila.popleft() # dequeue — O(1)
Python Avançado — Ferramentas que Importam
# List comprehension — Pytônico e eficiente quadrados = [x**2 for x in range(10)] pares = [x for x in range(20) if x % 2 == 0] # Dict comprehension freq = {c: s.count(c) for c in set(s)} # Generator — lazy, não carrega tudo na memória gen = (x**2 for x in range(10**6)) # não ocupa memória next(gen) # calcula um por vez # Funções de ordem superior nums = [3, 1, 4, 1, 5] list(map(lambda x: x*2, nums)) # aplica função list(filter(lambda x: x>2, nums)) # filtra from functools import reduce reduce(lambda a,b: a+b, nums) # reduz # Sorted com chave customizada pessoas = [('Ana',25), ('Bia',22), ('Carlos',30)] sorted(pessoas, key=lambda p: p[1]) # ordena por idade # Counter — contar frequências from collections import Counter c = Counter('abracadabra') c.most_common(3) # [('a',5),('b',2),('r',2)] # Defaultdict — evita KeyError from collections import defaultdict grafo = defaultdict(list) grafo['A'].append('B') # sem verificar se 'A' existe
Algoritmos Clássicos em Python
# Dois ponteiros — O(n), para listas ordenadas def dois_ponteiros(lst, alvo): esq, dir = 0, len(lst) - 1 while esq < dir: soma = lst[esq] + lst[dir] if soma == alvo: return (esq, dir) elif soma < alvo: esq += 1 else: dir -= 1 return None # Sliding window — O(n), janela deslizante def max_soma_subarray(lst, k): soma = sum(lst[:k]) maximo = soma for i in range(k, len(lst)): soma += lst[i] - lst[i-k] maximo = max(maximo, soma) return maximo # BFS — busca em largura em grafo from collections import deque def bfs(grafo, inicio): visitados = set() fila = deque([inicio]) while fila: no = fila.popleft() if no not in visitados: visitados.add(no) fila.extend(grafo[no]) return visitados
Exercício 5: Por que usar deque em vez de lista para fila (BFS)?
→ deque.popleft() é O(1). list.pop(0) é O(n) — desloca todos os elementos.
Em BFS com n nós, usar lista seria O(n²) total. Com deque, O(n). Para filas, sempre use deque ou queue.Queue.
📝 Cola — Algoritmos e Programação
Big-O de Referência
OperaçãoComplexidade
Acessar lista[i] / dict[k]O(1)
Busca bináriaO(log n)
Percorrer lista / busca linearO(n)
Merge Sort / Quick Sort / sorted()O(n log n)
Loop aninhado / Bubble SortO(n²)
Fibonacci ingênuo / subconjuntosO(2ⁿ)
Estrutura Certa para o Problema
Precisa de...UsePor quê
Busca por chavedictO(1) médio
Unicidade / pertencimentosetO(1) médio
Fila (FIFO)dequeO(1) nas duas pontas
Pilha (LIFO)list.append/popO(1) no fim
Menor/maior sempre disponívelheapqO(log n) insert
Contar frequênciasCounterO(n) total
⭐ Regras de Ouro
Regra
Constantes e termos menores somem: O(3n² + 5n + 2) = O(n²)
Regra
Loop simples = O(n). Loop dentro de loop = O(n²)
Regra
Dividir pela metade a cada passo = O(log n)
Regra
dict e set são O(1) para busca — prefira a listas quando possível
Regra
Recursão sem memoização em Fibonacci = O(2ⁿ). Com memo = O(n)
Regra
Use deque para fila — list.pop(0) é O(n), deque.popleft() é O(1)