Pyth
Pyth
Índice de Conteúdo
Problemas simples
Loops
Arrays
Complexidade de tempo
Ordenação
Sets e Maps
Pilha
Lista Encadeada
Fila de Prioridade
Strings
Dois Ponteiros
Recursão
Backtracking
Matemática
Busca Binária
Grafos
Busca em Profundidade
Busca em Largura
Árvores Binárias
Programação Dinâmica
Trie
Problemas simples
Problemas
Guia Simples
de Algoritmos
Antes de mergulhar em algoritmos mais complexos, vamos começar com alguns problemas simples. Estes
problemas são mais fáceis que os problemas típicos de entrevista, mas são ótimos para praticar suas
habilidades de programação básicas e se familiarizar com o processo de resolução de problemas.
# Exemplo de uso:
print(soma(2, 3)) # Imprime: 5
# Exemplo de uso:
print(maior(2, 3)) # Imprime: 3
# Exemplo de uso:
print(eh_par(4)) # Imprime: True
print(eh_par(3)) # Imprime: False
Próximos passos
Depois de se sentir confortável com estes problemas simples, você pode avançar para tópicos mais
complexos como Arrays e Loops. Lembre-se de que mesmo problemas aparentemente simples
podem ter nuances interessantes e podem ser usados para praticar boas práticas de programação.
Loops
Loops
Em muitos problemas, você não precisa conhecer algoritmos e técnicas especiais. O que você precisa é
apenas usar loops e variáveis simples. Nesta seção, vamos cobrir várias maneiras comuns de como você
pode resolver problemas com loops e praticar com vários exemplos.
def encontrar_maior(arr):
if not arr: # Se o array estiver vazio
return None
return maior
# Exemplo de uso:
numeros = [5, 7, 8, 9, -1, 3]
print(encontrar_maior(numeros)) # Imprime: 9
# Exemplo de uso:
numeros = [5, 7, 8, 9, -1, 3]
maior, segundo = encontrar_dois_maiores(numeros)
print(f"Maior: {maior}, Segundo maior: {segundo}") # Imprime: Maior: 9, Segundo ma
Ponteiros convergentes
Às vezes, é muito conveniente ter duas variáveis se movendo uma em direção à outra em um loop. Por
exemplo, aqui está como você pode verificar se uma string é um palíndromo:
def eh_palindromo(texto):
if not texto: # String vazia é considerada palíndromo
return True
return True
# Exemplos de uso:
print(eh_palindromo("ana")) # Imprime: True
print(eh_palindromo("radar")) # Imprime: True
Guia de Algoritmos
print(eh_palindromo("python")) # Imprime: False
Dígitos de um número
Outra técnica interessante é dividir um número inteiro em seus dígitos. Aqui está, por exemplo, como você
pode encontrar a soma dos dígitos de um número não negativo:
def soma_digitos(numero):
if numero < 0:
return None # Ou podemos trabalhar com o valor absoluto: abs(numero)
soma = 0
while numero > 0:
digito = numero % 10 # Obtém o último dígito
soma += digito
numero //= 10 # Remove o último dígito
return soma
# Exemplos de uso:
print(soma_digitos(123)) # Imprime: 6 (1 + 2 + 3)
print(soma_digitos(9045)) # Imprime: 18 (9 + 0 + 4 + 5)
Dica importante
Ao resolver problemas com loops, sempre considere os casos extremos:
Arrays/strings vazios
Arrays/strings com um único elemento
Números negativos (quando trabalhando com números)
Valores muito grandes que podem causar overflow
Guia de Algoritmos
Próximos passos
Depois de praticar com loops simples, você estará pronto para combinar esse conhecimento com
estruturas de dados mais complexas. O próximo tópico será Arrays, onde veremos como usar loops
de maneira mais sofisticada para resolver problemas mais desafiadores.
Arrays
Arrays
Arrays são uma das estruturas de dados mais fundamentais e frequentemente utilizadas em entrevistas
de programação. Nesta seção, vamos explorar várias técnicas comuns para trabalhar com arrays e
resolver problemas relacionados.
Soma de Prefixos
Uma técnica muito útil ao trabalhar com arrays é calcular e armazenar as somas dos prefixos. Isso pode
ajudar a resolver vários tipos de problemas de maneira eficiente.
def criar_soma_prefixos(arr):
if not arr:
return []
return soma_prefixos
# Exemplo de uso:
arr = [1, 2, 3, 4, 5]
somas = criar_soma_prefixos(arr)
print(somas) # Imprime: [1, 3, 6, 10, 15]
Arrays Bidimensionais
Arrays bidimensionais (ou matrizes) são muito comuns em problemas de entrevista. Aqui está um exemplo
de como percorrer uma matriz em espiral:
def percorrer_espiral(matriz):
if not matriz:
return []
resultado = []
inicio_linha, fim_linha = 0, len(matriz)
inicio_col, fim_col = 0, len(matriz[0])
return resultado
# Exemplo de uso:
matriz = [
[1, 2, 3, 4],
[12, 13, 14, 5],
[11, 16, 15, 6],
[10, 9, 8, 7]
Guia
] de Algoritmos
print(percorrer_espiral(matriz))
# Imprime: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
Próximos passos
Depois de dominar as técnicas básicas com arrays, você estará pronto para explorar conceitos mais
avançados como complexidade de tempo e espaço. Estes conceitos são cruciais para otimizar suas
soluções e se destacar nas entrevistas.
Complexidade
Guia de Algoritmosde tempo
Complexidade de Tempo
A complexidade de tempo é um conceito fundamental em ciência da computação que nos ajuda a
entender quanto tempo um algoritmo leva para executar em relação ao tamanho da entrada. Em
entrevistas, é crucial não apenas resolver o problema, mas também entender e otimizar a complexidade
de tempo da sua solução.
Notação Big O
A notação Big O é usada para descrever o limite superior do crescimento de um algoritmo. Aqui estão as
complexidades mais comuns, da mais eficiente para a menos eficiente:
def primeiro_elemento(arr):
if not arr:
return None
return arr[0] # O(1) - sempre acessa apenas o primeiro elemento
def soma_elementos(arr):
soma = 0
for num in arr: # O(n) - percorre cada elemento uma vez
soma += num
return soma
meio = len(arr) // 2
esquerda = merge_sort(arr[:meio])
direita = merge_sort(arr[meio:])
def bubble_sort(arr):
n = len(arr)
for i in range(n): # O(n²) - dois loops aninhados
for j in range(n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
return arr
Analisando Complexidade
Para analisar a complexidade de tempo de um algoritmo, considere:
Exemplo Prático
def encontrar_par_soma(arr, alvo):
# Abordagem O(n²)
def forca_bruta():
for i in range(len(arr)):
for j in range(i + 1, len(arr)):
if arr[i] + arr[j] == alvo:
return [i, j]
return []
# Abordagem O(n)
def usando_hash():
visto = {}
for i, num in enumerate(arr):
complemento = alvo - num
if complemento in visto:
return [visto[complemento], i]
visto[num] = i
Guia de Algoritmos
return []
Próximos passos
Após entender bem a complexidade de tempo, é importante também estudar a complexidade de
espaço e como equilibrar esses dois aspectos. O próximo tópico será Ordenação, onde aplicaremos
muito desse conhecimento de complexidade.
Ordenação
Ordenação
Algoritmos de ordenação são fundamentais na ciência da computação e frequentemente aparecem em
entrevistas. Além disso, muitos problemas podem ser simplificados ordenando os dados primeiro. Vamos
explorar os principais algoritmos de ordenação e suas características.
Bubble Sort
O algoritmo mais simples de ordenação, mas não o mais eficiente. Útil para entender conceitos básicos
de ordenação.
def bubble_sort(arr):
n = len(arr)
for i in range(n):
# Flag para otimização
Guia de Algoritmos
trocou = False
for j in range(0, n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
trocou = True
# Se não houve trocas, o array já está ordenado
if not trocou:
break
return arr
Merge Sort
Um algoritmo eficiente que usa a estratégia de dividir para conquistar. Garantido de ter complexidade O(n
log n) em todos os casos.
def merge_sort(arr):
if len(arr) <= 1:
return arr
meio = len(arr) // 2
esquerda = merge_sort(arr[:meio])
direita = merge_sort(arr[meio:])
[Link](esquerda[i:])
[Link](direita[j:])
return resultado
def quick_sort(arr):
if len(arr) <= 1:
return arr
pivo = arr[len(arr) // 2]
esquerda = [x for x in arr if x < pivo]
meio = [x for x in arr if x == pivo]
direita = [x for x in arr if x > pivo]
Merge Sort O(n log n) O(n log n) O(n log n) O(n) Sim
Sets e Maps
Sets e Maps
Sets e Maps são estruturas de dados fundamentais que permitem armazenar e acessar dados de forma
eficiente. Em Python, temos sets (conjuntos) e dicionários (maps), que são implementados usando
tabelas hash, oferecendo operações O(1) em média.
Sets (Conjuntos)
Um set é uma coleção não ordenada de elementos únicos. Perfeito para verificar pertencimento e eliminar
duplicatas.
# Criando um set
numeros = {1, 2, 3, 4, 5}
numeros_duplicados = {1, 2, 2, 3, 3, 4, 5} # Automaticamente remove duplicatas
print(numeros_duplicados) # {1, 2, 3, 4, 5}
# Operações de conjunto
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
print(a | b) # União: {1, 2, 3, 4, 5, 6}
print(a & b) # Interseção: {3, 4}
print(a - b) # Diferença: {1, 2}
print(a ^ b) # Diferença simétrica: {1, 2, 5, 6}
Maps (Dicionários)
Um map (ou dicionário em Python) é uma coleção de pares chave-valor, onde cada chave é única.
Guia de Algoritmos
Excelente para associar dados relacionados.
# Criando um dicionário
notas = {
'João': 8.5,
'Maria': 9.0,
'Pedro': 7.5
}
# Métodos úteis
print([Link]()) # Obtém todas as chaves
print([Link]()) # Obtém todos os valores
print([Link]()) # Obtém todos os pares chave-valor
nums = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
print(contar_frequencia(nums)) # {1: 1, 2: 2, 3: 3, 4: 4}
# Agrupar anagramas
def agrupar_anagramas(palavras):
grupos = {}
for palavra in palavras:
chave = ''.join(sorted(palavra))
[Link](chave, []).append(palavra)
return list([Link]())
Próximos passos
Depois de dominar sets e maps, você estará pronto para explorar estruturas de dados mais
complexas como pilhas e filas. Estas estruturas são fundamentais para resolver problemas que
envolvem processamento em ordem específica.
Pilha
Pilha (Stack)
Uma pilha é uma estrutura de dados que segue o princípio LIFO (Last In, First Out - Último a Entrar,
Primeiro a Sair). Pense nela como uma pilha de pratos: você só pode adicionar ou remover pratos do topo
da pilha.
Implementação Básica
Em Python, podemos implementar uma pilha usando uma lista, onde o final da lista representa o topo da
pilha.
class Pilha:
def __init__(self):
[Link] = []
def esta_vazia(self):
return len([Link]) == 0
def desempilhar(self):
if not self.esta_vazia():
return [Link]() # O(1)
raise IndexError("Pilha vazia")
Guia dedefAlgoritmos
topo(self):
if not self.esta_vazia():
return [Link][-1] # O(1)
raise IndexError("Pilha vazia")
def tamanho(self):
return len([Link])
# Exemplo de uso
pilha = Pilha()
[Link](1)
[Link](2)
[Link](3)
print([Link]()) # Imprime: 3
print([Link]()) # Imprime: 3
print([Link]()) # Imprime: 2
Aplicações Comuns
Pilhas são usadas em vários cenários importantes:
return len(pilha) == 0
# Exemplos de uso
print(parenteses_balanceados("(){}[]")) # True
print(parenteses_balanceados("([{}])")) # True
print(parenteses_balanceados("({[}])")) # False
Guia de Algoritmos
print(parenteses_balanceados("((()")) # False
if len(pilha) != 1:
raise ValueError("Expressão inválida")
return pilha[0]
# Exemplos de uso
# "3 4 +" equivale a "3 + 4"
print(avaliar_pos_fixa("3 4 +")) # 7.0
# "5 3 4 * +" equivale a "5 + (3 * 4)"
print(avaliar_pos_fixa("5 3 4 * +")) # 17.0
# "10 5 2 * -" equivale a "10 - (5 * 2)"
print(avaliar_pos_fixa("10 5 2 * -")) # 0.0
Próximos passos
Depois de dominar pilhas, você pode avançar para listas encadeadas, que são estruturas de dados
fundamentais que frequentemente aparecem em conjunto com pilhas em problemas de entrevista.
Lista Encadeada
Implementação Básica
Vamos implementar uma lista encadeada simples em Python:
class No:
def __init__(self, valor):
[Link] = valor
[Link] = None
class ListaEncadeada:
def __init__(self):
[Link] = None
def esta_vazia(self):
return [Link] is None
atual = [Link]
while [Link]:
atual = [Link]
[Link] = novo_no
def remover_inicio(self):
if self.esta_vazia():
raise ValueError("Lista vazia")
valor = [Link]
[Link] = [Link]
return valor
def imprimir(self):
atual = [Link]
elementos = []
while atual:
[Link](str([Link]))
atual = [Link]
print(" -> ".join(elementos))
# Exemplo de uso
lista = ListaEncadeada()
lista.inserir_inicio(3)
lista.inserir_inicio(2)
lista.inserir_inicio(1)
lista.inserir_fim(4)
[Link]() # 1 -> 2 -> 3 -> 4
class NoDuplo:
def __init__(self, valor):
[Link] = valor
[Link] = None
[Link] = None
class ListaDuplamenteEncadeada:
def __init__(self):
[Link] = None
Guia de Algoritmos
[Link] = None
def esta_vazia(self):
return [Link] is None
Técnicas Comuns
1. Técnica do Corredor Rápido e Lento (Floyd's Cycle Finding)
def detectar_ciclo(cabeca):
if not cabeca or not [Link]:
return False
lento = cabeca
rapido = [Link]
return False
while atual:
proximo = [Link]
[Link] = anterior
anterior = atual
atual = proximo
Próximos passos
Após dominar listas encadeadas, você pode avançar para estruturas mais complexas como árvores e
Guiagrafos.
de Algoritmos
A compreensão de listas encadeadas é fundamental para entender como os nós se conectam
nessas estruturas mais avançadas.
Fila de Prioridade
Heap Binário
Um heap binário é uma árvore binária completa onde cada nó pai tem um valor menor (min-heap) ou
maior (max-heap) que seus filhos.
max_heap = []
for valor in valores:
[Link](max_heap, Item(valor))
class Tarefa:
def __init__(self, prioridade, descricao):
[Link] = prioridade
[Link] = descricao
return heap[0]
return -heap[0]
# Exemplo de uso
arr = [7, 10, 4, 3, 20, 15]
k = 3
print(k_menor_elemento(arr, k)) # 7 (3º menor elemento)
print(k_maior_elemento(arr, k)) # 10 (3º maior elemento)
Próximos passos
Após dominar filas de prioridade, você estará pronto para explorar estruturas de dados mais
avançadas como árvores binárias de busca e grafos. Filas de prioridade são frequentemente usadas
como componentes em algoritmos mais complexos, como o algoritmo de Dijkstra.
Strings
Strings
Strings são uma das estruturas de dados mais comuns em programação e frequentemente aparecem em
entrevistas. Em Python, strings são imutáveis, o que significa que cada operação que modifica uma string
cria uma nova string.
Operações Básicas
# Criação e concatenação
texto = "Hello"
texto += " World" # Cria uma nova string
# Fatiamento (slicing)
texto = "Python"
print(texto[0:2]) # "Py"
print(texto[2:]) # "thon"
print(texto[::-1]) # "nohtyP" (reverso)
# Métodos úteis
texto = " Python é incrível! "
print([Link]()) # Remove espaços em branco
print([Link]()) # Converte para minúsculas
print([Link]()) # Converte para maiúsculas
print([Link]()) # Divide em palavras
print(",".join(["a", "b"])) # Une strings com separador
# Verificações comuns
texto = "Python3.9"
print([Link]()) # Contém apenas letras?
print([Link]()) # Contém letras ou números?
Guia de Algoritmos
print([Link]()) # Contém apenas números?
print([Link]("Py")) # Começa com "Py"?
print([Link](".9")) # Termina com ".9"?
Técnicas Comuns
1. Verificar Palíndromo
def eh_palindromo(texto):
# Remove espaços e converte para minúsculas
texto = [Link]().replace(" ", "")
return texto == texto[::-1]
# Exemplos
print(eh_palindromo("ana")) # True
print(eh_palindromo("A man a plan a canal Panama")) # True
print(eh_palindromo("python")) # False
2. Contar Caracteres
from collections import Counter
def contar_caracteres(texto):
# Usando Counter
return Counter(texto)
def contar_manual(texto):
# Implementação manual
contagem = {}
for char in texto:
contagem[char] = [Link](char, 0) + 1
return contagem
texto = "programação"
print(contar_caracteres(texto)) # Counter({'a': 2, 'r': 2, 'o': 2, ...})
print(contar_manual(texto)) # {'p': 1, 'r': 2, 'o': 2, ...}
3. Verificar Anagrama
def sao_anagramas(s1, s2):
# Método 1: Usando ordenação
return sorted(s1) == sorted(s2)
# Exemplos
print(sao_anagramas("listen", "silent")) # True
Guia de Algoritmos
print(sao_anagramas("triangle", "integral")) # True
print(sao_anagramas("python", "java")) # False
4. Encontrar Substring
def encontrar_todas_ocorrencias(texto, padrao):
ocorrencias = []
pos = [Link](padrao)
return ocorrencias
texto = "banana"
padrao = "ana"
print(encontrar_todas_ocorrencias(texto, padrao)) # [1, 3]
Técnicas Avançadas
1. Janela Deslizante (Sliding Window)
Guia deinicio
Algoritmos
def maior_substring_sem_repeticao(s):
= 0
max_tamanho = 0
caracteres = {}
return max_tamanho
# Exemplo
print(maior_substring_sem_repeticao("abcabcbb")) # 3 ("abc")
2. Manacher (Palíndromos)
def expandir_centro(s, esquerda, direita):
while esquerda >= 0 and direita < len(s) and s[esquerda] == s[direita]:
esquerda -= 1
direita += 1
return s[esquerda + 1:direita]
def maior_palindromo(s):
if not s:
return ""
maior = s[0]
for i in range(len(s)):
# Palíndromo ímpar
palindromo1 = expandir_centro(s, i, i)
if len(palindromo1) > len(maior):
maior = palindromo1
# Palíndromo par
palindromo2 = expandir_centro(s, i, i + 1)
if len(palindromo2) > len(maior):
maior = palindromo2
return maior
# Exemplo
print(maior_palindromo("babad")) # "bab" ou "aba"
Próximos passos
Depois de dominar as operações básicas com strings, você pode explorar algoritmos mais
Guiaavançados
de Algoritmos
como KMP (Knuth-Morris-Pratt) para busca de padrões e Árvores de Sufixos para
problemas mais complexos de strings.
Dois Ponteiros
Padrões Comuns
1. Ponteiros nas Extremidades
Um ponteiro no início e outro no fim, movendo-se em direção ao centro.
return ponteiro_escrita
# Exemplo
nums = [1, 1, 2, 2, 3, 4, 4]
tamanho = remover_duplicatas(nums)
print(nums[:tamanho]) # [1, 2, 3, 4]
3. Janela Deslizante
Dois ponteiros que definem uma "janela" que pode crescer ou encolher.
# Exemplo
nums = [2, 3, 1, 2, 4, 3]
alvo = 7
print(menor_subarray_soma(nums, alvo)) # 2 ([4, 3])
def encontrar_duplicata(nums):
# Algoritmo da Lebre e da Tartaruga (Floyd's Cycle Finding)
lento = nums[0]
Guia derapido
Algoritmos
= nums[0]
# Encontrar interseção
while True:
lento = nums[lento]
rapido = nums[nums[rapido]]
if lento == rapido:
break
return lento
# Exemplo
nums = [1, 3, 4, 2, 2] # Array com números de 1 a n, com uma duplicata
print(encontrar_duplicata(nums)) # 2
15 - 3Sum LeetCode
Variações da Técnica
Guia de Algoritmos
# Três ponteiros (para soma
def soma_tres_numeros(nums,
de três números)
alvo):
[Link]() # Importante ordenar primeiro
n = len(nums)
esquerda, direita = i + 1, n - 1
while esquerda < direita:
soma = nums[i] + nums[esquerda] + nums[direita]
if soma == alvo:
return [nums[i], nums[esquerda], nums[direita]]
elif soma < alvo:
esquerda += 1
else:
direita -= 1
return []
# Exemplo
nums = [-1, 0, 1, 2, -1, -4]
alvo = 0
print(soma_tres_numeros(nums, alvo)) # [-1, 0, 1]
Próximos passos
Depois de dominar a técnica dos dois ponteiros, você pode explorar variações mais complexas como
múltiplos ponteiros e combiná-la com outras técnicas como programação dinâmica e backtracking
para resolver problemas mais desafiadores.
Recursão
Recursão
Recursão é uma técnica de programação onde uma função resolve um problema chamando a si mesma
com uma entrada menor. É fundamental entender recursão para resolver problemas complexos e é
frequentemente usada em estruturas de dados como árvores e grafos.
Conceitos Fundamentais
Guia de Algoritmos
1. Caso Base
O caso base é a condição que para a recursão. Sem ele, a função continuaria chamando a si mesma
indefinidamente.
def fatorial(n):
# Caso base
if n == 0 or n == 1:
return 1
# Caso recursivo
return n * fatorial(n - 1)
print(fatorial(5)) # 120
2. Fibonacci
Um exemplo clássico de recursão é a sequência de Fibonacci.
def fibonacci(n):
# Casos base
if n <= 1:
return n
# Caso recursivo
return fibonacci(n - 1) + fibonacci(n - 2)
# Exemplo
print([fibonacci(i) for i in range(7)]) # [0, 1, 1, 2, 3, 5, 8]
print(fibonacci_memo(7)) # 13
3. Soma de Array
Um exemplo simples de como a recursão pode ser usada para processar arrays.
def soma_array(arr):
# Caso base
if not arr:
return 0
Guia de Algoritmos
# Caso recursivo
return arr[0] + soma_array(arr[1:])
# Exemplo
print(soma_array([1, 2, 3, 4, 5])) # 15
print(soma_array_otimizada([1, 2, 3, 4, 5])) # 15
4. Torre de Hanoi
Um problema clássico que demonstra a elegância da recursão.
# Exemplo
print("Resolvendo Torre de Hanoi com 3 discos:")
torre_hanoi(3, 'A', 'C', 'B')
Técnicas Avançadas
1. Recursão com Cauda
def fatorial_cauda(n, acumulador=1):
if n <= 1:
return acumulador
return fatorial_cauda(n - 1, n * acumulador)
print(fatorial_cauda(5)) # 120
2. Divisão e Conquista
def merge_sort(arr):
# Caso base
if len(arr) <= 1:
return arr
# Dividir
meio = len(arr) // 2
esquerda = merge_sort(arr[:meio])
direita = merge_sort(arr[meio:])
# Conquistar e Combinar
return merge(esquerda, direita)
[Link](esquerda[i:])
[Link](direita[j:])
return resultado
# Exemplo
arr = [64, 34, 25, 12, 22, 11, 90]
Guia de Algoritmos
print(merge_sort(arr)) # [11, 12, 22, 25, 34, 64, 90]
Próximos passos
Depois de dominar os conceitos básicos de recursão, você pode explorar tópicos mais avançados
como programação dinâmica, backtracking e algoritmos de divisão e conquista, que frequentemente
utilizam recursão como base.
Backtracking
Backtracking
Backtracking é uma técnica algorítmica que considera a exploração de todas as possíveis soluções de
forma sistemática. Quando percebe que um caminho não levará a uma solução válida, "volta atrás"
(backtrack) e tenta outro caminho.
Problemas Clássicos
1. N-Rainhas
Posicionar N rainhas em um tabuleiro NxN sem que se ataquem.
def n_rainhas(n):
def pode_colocar(tabuleiro, linha, coluna):
# Verifica a coluna
for i in range(linha):
if tabuleiro[i][coluna] == 1:
return False
return True
Guia dedefAlgoritmos
resolver(tabuleiro, linha):
if linha >= n:
return True
return False
# Exemplo
solucao = n_rainhas(4)
for linha in solucao:
print(linha) # Mostra uma solução possível para 4 rainhas
2. Subconjuntos
Gerar todos os subconjuntos possíveis de um conjunto.
def subconjuntos(nums):
resultado = []
backtrack(0, [])
return resultado
# Exemplo
nums = [1, 2, 3]
print(subconjuntos(nums)) # [[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]]
3. Permutações
Gerar todas as permutações possíveis de um conjunto.
Guia de Algoritmos
def permutacoes(nums):
resultado = []
def backtrack(inicio):
if inicio == len(nums):
[Link](nums[:])
return
backtrack(0)
return resultado
# Exemplo
nums = [1, 2, 3]
print(permutacoes(nums)) # [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 2, 1]
78 - Subsets LeetCode
46 - Permutations LeetCode
combinacao_atual.append(candidatos[i])
backtrack(i, alvo - candidatos[i], combinacao_atual)
combinacao_atual.pop()
# Exemplo
candidatos = [2, 3, 6, 7]
alvo = 7
print(combinacao_soma(candidatos, alvo)) # [[2, 2, 3], [7]]
Próximos passos
Após dominar backtracking, você pode explorar sua aplicação em problemas mais complexos de
grafos, como coloração de grafos e problemas de satisfatibilidade booleana (SAT). Também é
importante estudar como combinar backtracking com programação dinâmica para problemas que
exigem ambas as técnicas.
Matemática
Matemática
Guia de Algoritmos
Problemas matemáticos são comuns em entrevistas de programação. Eles geralmente envolvem
conceitos como números primos, divisibilidade, teoria dos números e manipulação de bits.
Conceitos Fundamentais
1. Números Primos
Verificação e geração de números primos são problemas comuns.
def eh_primo(n):
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True
def crivo_eratostenes(n):
# Gera todos os primos até n
primos = [True] * (n + 1)
primos[0] = primos[1] = False
# Exemplos
print(eh_primo(17)) # True
print(crivo_eratostenes(20)) # [2, 3, 5, 7, 11, 13, 17, 19]
2. MDC e MMC
Máximo Divisor Comum e Mínimo Múltiplo Comum são conceitos importantes.
3. Manipulação de Bits
Operações com bits são úteis para otimização e problemas específicos.
def conta_bits(n):
# Conta bits 1 em um número
return bin(n).count('1')
def eh_potencia_de_dois(n):
# Verifica se é potência de 2
return n > 0 and (n & (n - 1)) == 0
def inverte_bits(n):
# Inverte os bits de um número
return ~n
# Exemplos
print(conta_bits(7)) # 3 (111 em binário)
print(eh_potencia_de_dois(8)) # True
print(bin(set_bit(10, 2))) # 0b1110 (14 em decimal)
Técnicas Avançadas
1. Exponenciação Rápida
def pow_mod(base, expoente, modulo):
# Calcula (base ^ expoente) % modulo eficientemente
if expoente == 0:
return 1
resultado = 1
base = base % modulo
return resultado
# Exemplo
print(pow_mod(2, 10, 1000)) # 24
def fibonacci_matriz(n):
if n <= 0:
return 0
# Matriz base [[1,1],[1,0]]
Guia de Algoritmos
A = [[1, 1], [1, 0]]
resultado = [[1, 0], [0, 1]] # Matriz identidade
return resultado[0][1]
# Exemplo
print(fibonacci_matriz(10)) # 55
Próximos passos
Após dominar os conceitos básicos de matemática, você pode explorar tópicos mais avançados
como teoria dos números, combinatória e probabilidade. Também é importante estudar como esses
conceitos se aplicam em problemas de otimização e criptografia.
Busca Binária
Busca Binária
Busca binária é um algoritmo eficiente para encontrar um elemento em uma coleção ordenada, reduzindo
o espaço de busca pela metade a cada iteração. Sua complexidade é O(log n), tornando-o muito mais
rápido que a busca linear O(n).
Implementação Básica
def busca_binaria(arr, alvo):
esquerda, direita = 0, len(arr) - 1
if arr[meio] == alvo:
return meio # Elemento encontrado, retorna o índice
elif arr[meio] < alvo:
Guia de Algoritmos
esquerda = meio + 1 # Busca na metade direita
else:
direita = meio - 1 # Busca na metade esquerda
# Exemplo
arr = [1, 3, 5, 7, 9, 11, 13, 15]
print(busca_binaria(arr, 7)) # 3
print(busca_binaria(arr, 10)) # -1
Variações Comuns
1. Primeiro Elemento Maior ou Igual
Encontra o primeiro elemento maior ou igual ao alvo (lower bound).
# Exemplo
arr = [1, 3, 3, 5, 5, 5, 7]
print(lower_bound(arr, 5)) # 3 (primeiro 5)
return esquerda - 1
# Exemplo
Guia
arrde
= Algoritmos
[1, 3, 3, 5, 5, 5, 7]
print(upper_bound(arr, 5)) # 5 (último 5)
if arr[meio] == alvo:
return meio
return -1
# Exemplo
arr = [4, 5, 6, 7, 0, 1, 2]
print(busca_array_rotacionado(arr, 0)) # 4
69 - Sqrt(x) LeetCode
Aplicações Avançadas
def raiz_quadrada(x):
if x < 2:
return x
esquerda, direita = 1, x // 2
if quadrado == x:
return meio
elif quadrado < x:
esquerda = meio + 1
else:
direita = meio - 1
return direita # Retorna o maior inteiro cuja raiz quadrada é menor ou igual a
# Exemplo
print(raiz_quadrada(8)) # 2 (maior inteiro cuja raiz quadrada é <= 8)
def busca_pico(arr):
# Encontra um elemento que é maior que seus vizinhos
esquerda, direita = 0, len(arr) - 1
return esquerda
# Exemplo
arr = [1, 2, 3, 1]
Guia de Algoritmos
print(busca_pico(arr)) # 2 (3 é o pico)
Próximos passos
Depois de dominar a busca binária básica, explore suas aplicações em problemas mais complexos,
como busca em matrizes 2D ordenadas, minimização de valores máximos e maximização de valores
mínimos. A busca binária também é útil em problemas de otimização onde você pode verificar a
viabilidade de uma solução.
Grafos
Grafos
Grafos são estruturas de dados que representam relações entre objetos. Consistem em vértices (ou nós)
conectados por arestas. São amplamente utilizados para modelar redes sociais, mapas, dependências
entre tarefas e muito mais.
Representações de Grafos
1. Lista de Adjacências
Representa o grafo usando um dicionário onde cada vértice mapeia para seus vizinhos.
2. Matriz de Adjacências
Representa o grafo usando uma matriz onde matriz[i][j] indica se existe aresta entre i e j.
def cria_matriz_adjacencias(n):
return [[0] * n for _ in range(n)]
# Exemplo
n = 4 # número de vértices
matriz = cria_matriz_adjacencias(n)
adiciona_aresta(matriz, 0, 1)
adiciona_aresta(matriz, 1, 2)
adiciona_aresta(matriz, 2, 3)
Algoritmos Fundamentais
1. Busca em Profundidade (DFS)
Explora o grafo indo o mais fundo possível em cada ramo antes de retroceder.
[Link](inicio)
print(inicio, end=' ') # Processa o vértice
while pilha:
vertice = [Link]()
Guia de Algoritmos
if vertice not in visitados:
[Link](vertice)
print(vertice, end=' ')
[Link](v for v in grafo[vertice] if v not in visitados)
# Exemplo
grafo = {'A': ['B', 'C'], 'B': ['A', 'D'], 'C': ['A'], 'D': ['B']}
dfs(grafo, 'A') # A B D C
while fila:
vertice = [Link]()
print(vertice, end=' ')
while fila:
vertice = [Link]()
for vizinho in grafo[vertice]:
if vizinho not in distancias:
distancias[vizinho] = distancias[vertice] + 1
[Link](vizinho)
return distancias
# Exemplo
grafo = {'A': ['B', 'C'], 'B': ['A', 'D'], 'C': ['A'], 'D': ['B']}
print(bfs_com_distancia(grafo, 'A')) # {'A': 0, 'B': 1, 'C': 1, 'D': 2}
3. Detecção de Ciclo
Verifica se existe um ciclo no grafo.
Guia devisitados
Algoritmos
def tem_ciclo(grafo):
= set()
pilha_rec = set()
def dfs_ciclo(vertice):
[Link](vertice)
pilha_rec.add(vertice)
pilha_rec.remove(vertice)
return False
# Exemplo
grafo_com_ciclo = {'A': ['B'], 'B': ['C'], 'C': ['A']}
print(tem_ciclo(grafo_com_ciclo)) # True
Aplicações Avançadas
def encontra_componentes(grafo):
visitados = set()
componentes = []
return componentes
def eh_bipartido(grafo):
cores = {}
# Exemplos
grafo_componentes = {
'A': ['B'], 'B': ['A'],
'C': ['D'], 'D': ['C'],
'E': []
}
print(encontra_componentes(grafo_componentes)) # [['A', 'B'], ['C', 'D'], ['E']]
Guia de Algoritmos
grafo_bipartido = {'A': ['B', 'C'], 'B': ['A'], 'C': ['A']}
print(eh_bipartido(grafo_bipartido)) # True
Próximos passos
Após dominar os conceitos básicos de grafos, explore algoritmos mais avançados como Dijkstra,
Bellman-Ford, Floyd-Warshall para caminhos mínimos, e algoritmos de árvore geradora mínima como
Kruskal e Prim. Também é importante estudar fluxo em redes e algoritmos de emparelhamento.
Busca em Profundidade
Implementações Básicas
1. DFS Recursivo
A forma mais intuitiva de implementar DFS usando recursão.
# Exemplo
grafo = {
'A': ['B', 'C'],
'B': ['A', 'D', 'E'],
Guia de'C':
Algoritmos
['A', 'F'],
'D': ['B'],
'E': ['B', 'F'],
'F': ['C', 'E']
}
dfs_recursivo(grafo, 'A') # A B D E F C
2. DFS Iterativo
Implementação usando pilha, útil para evitar estouro de pilha em grafos muito profundos.
while pilha:
vertice = [Link]()
if vertice not in visitados:
[Link](vertice)
print(vertice, end=' ')
# Adiciona vizinhos não visitados à pilha
[Link](v for v in grafo[vertice] if v not in visitados)
# Exemplo
dfs_iterativo(grafo, 'A') # A C F E B D
Aplicações Comuns
1. Detecção de Ciclos
def detecta_ciclo(grafo):
visitados = set()
pilha_recursao = set()
def dfs_ciclo(vertice):
[Link](vertice)
pilha_recursao.add(vertice)
pilha_recursao.remove(vertice)
return False
for vertice in grafo:
Guia de Algoritmos
if vertice not in visitados:
if dfs_ciclo(vertice):
return True
return False
# Exemplo
grafo_ciclico = {
'A': ['B'],
'B': ['C'],
'C': ['A']
}
print(detecta_ciclo(grafo_ciclico)) # True
2. Ordenação Topológica
Útil para ordenar tarefas com dependências.
def ordenacao_topologica(grafo):
visitados = set()
pilha = []
def dfs_topologico(vertice):
[Link](vertice)
[Link](vertice)
return componentes
# Exemplo
grafo_direcionado = {
'A': ['B'],
'B': ['C', 'D'],
'C': ['A'],
'D': ['E'],
'E': ['F'],
'F': ['D']
}
print(kosaraju_dfs(grafo_direcionado))
# [['A', 'C', 'B'], ['D', 'F', 'E']]
Próximos passos
Após dominar DFS, explore suas aplicações em problemas mais complexos como encontrar pontes
Guiaemde Algoritmos
grafos, articulações, e componentes fortemente conectados. Também é importante estudar como
DFS se relaciona com outros algoritmos de grafos como Tarjan e Kosaraju.
Busca em Largura
Implementação Básica
from collections import deque
while fila:
vertice = [Link]()
print(vertice, end=' ') # Processa o vértice
# Exemplo
grafo = {
'A': ['B', 'C'],
'B': ['A', 'D', 'E'],
'C': ['A', 'F'],
'D': ['B'],
'E': ['B', 'F'],
'F': ['C', 'E']
}
bfs(grafo, 'A') # A B C D E F
Aplicações Comuns
1. Menor Caminho em Grafo Não Ponderado
Guia de Algoritmos
def menor_caminho(grafo, inicio, fim):
if inicio == fim:
return [inicio]
visitados = {inicio}
fila = deque([(inicio, [inicio])])
while fila:
vertice, caminho = [Link]()
# Exemplo
grafo = {
'A': ['B', 'C'],
'B': ['A', 'D'],
'C': ['A', 'D'],
'D': ['B', 'C', 'E'],
'E': ['D']
}
print(menor_caminho(grafo, 'A', 'E')) # ['A', 'B', 'D', 'E']
2. Matriz de Distâncias
Calcula a distância mínima de um ponto inicial para todos os outros pontos.
while fila:
vertice = [Link]()
for vizinho in grafo[vertice]:
if vizinho not in distancias:
distancias[vizinho] = distancias[vertice] + 1
[Link](vizinho)
return distancias
# Exemplo
grafo = {
'A': ['B', 'C'],
'B': ['A', 'D', 'E'],
Guia de'C':
Algoritmos
['A', 'F'],
'D': ['B'],
'E': ['B', 'F'],
'F': ['C', 'E']
}
print(matriz_distancias(grafo, 'A'))
# {'A': 0, 'B': 1, 'C': 1, 'D': 2, 'E': 2, 'F': 2}
n, m = len(matriz), len(matriz[0])
visitados = set([(inicio_i, inicio_j)])
fila = deque([(inicio_i, inicio_j, 0)]) # (i, j, distância)
direcoes = [(0, 1), (1, 0), (0, -1), (-1, 0)] # direita, baixo, esquerda, cima
while fila:
i, j, dist = [Link]()
print(f'({i},{j}) distância: {dist}')
Guia de Algoritmos
for di, dj in direcoes:
novo_i, novo_j = i + di, j + dj
if (0 <= novo_i < n and 0 <= novo_j < m and
(novo_i, novo_j) not in visitados and
matriz[novo_i][novo_j] == 1): # assume 1 como célula válida
[Link]((novo_i, novo_j))
[Link]((novo_i, novo_j, dist + 1))
# Exemplo
matriz = [
[1, 1, 1, 1],
[0, 1, 0, 1],
[1, 1, 1, 1]
]
bfs_matriz(matriz, 0, 0)
2. BFS Bidirecional
Busca a partir do início e do fim simultaneamente, útil para encontrar o menor caminho.
# Exemplo
grafo = {
'A': ['B', 'C'],
'B': ['A', 'D', 'E'],
'C': ['A', 'F'],
'D': ['B'],
'E': ['B', 'F'],
'F': ['C', 'E']
}
print(bfs_bidirecional(grafo, 'A', 'F')) # ['A', 'C', 'F']
Próximos passos
Após dominar BFS, explore suas aplicações em problemas mais complexos como busca em múltiplas
fontes, problemas de roteamento em redes e otimização de caminhos. Também é importante estudar
como BFS se relaciona com algoritmos de caminho mínimo como Dijkstra e com problemas de fluxo
em redes.
Árvores Binárias
Árvores Binárias
Árvores binárias são estruturas de dados hierárquicas onde cada nó tem no máximo dois filhos,
geralmente chamados de filho esquerdo e filho direito. São amplamente utilizadas em problemas que
envolvem hierarquia, busca e ordenação.
Implementação Básica
class No:
def __init__(self, valor=0, esquerda=None, direita=None):
[Link] = valor
[Link] = esquerda
[Link] = direita
# Exemplo de criação de uma árvore
Guia
# de Algoritmos
1
# / \
# 2 3
# / \
# 4 5
raiz = No(1)
[Link] = No(2)
[Link] = No(3)
[Link] = No(4)
[Link] = No(5)
Travessias
1. Percurso em Ordem (Inorder)
Visita o filho esquerdo, a raiz e depois o filho direito.
def inorder(raiz):
if not raiz:
return
inorder([Link])
print([Link], end=' ')
inorder([Link])
# Versão iterativa
def inorder_iterativo(raiz):
pilha = []
atual = raiz
atual = [Link]()
print([Link], end=' ')
atual = [Link]
def preorder(raiz):
if not raiz:
return
Guia de Algoritmos
print([Link], end=' ')
preorder([Link])
preorder([Link])
# Versão iterativa
def preorder_iterativo(raiz):
if not raiz:
return
pilha = [raiz]
while pilha:
no = [Link]()
print([Link], end=' ')
if [Link]:
[Link]([Link])
if [Link]:
[Link]([Link])
def postorder(raiz):
if not raiz:
return
postorder([Link])
postorder([Link])
print([Link], end=' ')
# Versão iterativa
def postorder_iterativo(raiz):
if not raiz:
return
pilha1 = [raiz]
pilha2 = []
while pilha1:
no = [Link]()
[Link](no)
if [Link]:
[Link]([Link])
if [Link]:
[Link]([Link])
while pilha2:
Guia de Algoritmos
no = [Link]()
print([Link], end=' ')
Operações Comuns
1. Altura da Árvore
def altura(raiz):
if not raiz:
return 0
return 1 + max(altura([Link]), altura([Link]))
def altura_iterativa(raiz):
if not raiz:
return 0
altura = 0
fila = deque([raiz])
while fila:
nivel_tamanho = len(fila)
for _ in range(nivel_tamanho):
no = [Link]()
if [Link]:
[Link]([Link])
if [Link]:
[Link]([Link])
altura += 1
return altura
def inorder(no):
nonlocal anterior
if not no:
return True
if not inorder([Link]):
return False
return inorder([Link])
return inorder(raiz)
Técnicas Avançadas
# Serialização e Deserialização
def serializar(raiz):
if not raiz:
Guia de Algoritmos
return "null"
return f"{[Link]},{serializar([Link])},{serializar([Link])}"
def deserializar(dados):
def dfs():
val = next(valores)
if val == "null":
return None
no = No(int(val))
[Link] = dfs()
[Link] = dfs()
return no
valores = iter([Link](','))
return dfs()
raiz_valor = preorder[0]
raiz = No(raiz_valor)
indice = [Link](raiz_valor)
[Link] = constroi_arvore(inorder[:indice],
preorder[1:indice+1])
[Link] = constroi_arvore(inorder[indice+1:],
preorder[indice+1:])
return raiz
esquerda = lca([Link], p, q)
direita = lca([Link], p, q)
Próximos passos
Após dominar árvores binárias básicas, explore estruturas mais avançadas como árvores AVL,
árvores Rubro-Negras e B-Trees. Também é importante estudar aplicações práticas como índices de
banco de dados e estruturas de dados persistentes.
Guia de Algoritmos
Programação Dinâmica
Conceitos Fundamentais
1. Fibonacci com DP
Exemplo clássico que mostra a diferença entre abordagem recursiva e DP.
# Recursivo (exponencial)
def fib_recursivo(n):
if n <= 1:
return n
return fib_recursivo(n-1) + fib_recursivo(n-2)
# DP Top-Down (memoização)
def fib_memo(n, memo=None):
if memo is None:
memo = {}
if n in memo:
return memo[n]
if n <= 1:
return n
# DP Bottom-Up (tabulação)
def fib_tabela(n):
if n <= 1:
return n
dp = [0] * (n + 1)
dp[1] = 1
a, b = 0, 1
for _ in range(2, n + 1):
a, b = b, a + b
return b
return dp[n][capacidade]
# Exemplo
valores = [60, 100, 120]
pesos = [10, 20, 30]
capacidade = 50
print(mochila(valores, pesos, capacidade)) # 220
# Reconstruir a subsequência
subsequencia = []
i, j = m, n
while i > 0 and j > 0:
if texto1[i-1] == texto2[j-1]:
[Link](texto1[i-1])
i -= 1
j -= 1
elif dp[i-1][j] > dp[i][j-1]:
i -= 1
else:
j -= 1
return ''.join(reversed(subsequencia))
# Exemplo
print(lcs("ABCDGH", "AEDFHR")) # "ADH"
Padrões Comuns
1. Subsequência Crescente Mais Longa (LIS)
Guia deifAlgoritmos
def lis(nums):
not nums:
return 0
n = len(nums)
dp = [1] * n # dp[i] é o tamanho da LIS terminando em nums[i]
return max(dp)
def lis_otimizado(nums):
pilha = []
return len(pilha)
# Exemplo
nums = [10, 9, 2, 5, 3, 7, 101, 18]
print(lis(nums)) # 4 ([2, 3, 7, 101])
m, n = len(grid), len(grid[0])
dp = [[float('inf')] * (n + 1) for _ in range(m + 1)]
dp[0][1] = dp[1][0] = 0
return dp[m][n]
m, n = len(grid), len(grid[0])
dp = [float('inf')] * (n + 1)
dp[1] = 0
return dp[n]
# Exemplo
grid = [
[1, 3, 1],
[1, 5, 1],
[4, 2, 1]
]
print(caminho_minimo(grid)) # 7
Próximos passos
Após dominar os conceitos básicos de DP, explore problemas mais complexos como programação
dinâmica em árvores, problemas de particionamento e otimização de expressões. Também é
importante estudar a relação entre DP e outros paradigmas como algoritmos gulosos e divisão e
conquista.
Trie
Implementação Básica
Guia dedefAlgoritmos
class TrieNode:
__init__(self):
[Link] = {} # Mapa de caractere para nó filho
self.is_end = False # Marca fim de palavra
class Trie:
def __init__(self):
[Link] = TrieNode()
# Exemplo de uso
trie = Trie()
[Link]("apple")
print([Link]("apple")) # True
print([Link]("app")) # False
print(trie.starts_with("app")) # True
Aplicações Comuns
1. Autocompletar
class TrieNode:
def __init__(self):
[Link] = {}
self.is_end = False
[Link] = [] # Lista de sugestões para autocompletar
class Autocomplete:
def __init__(self):
Guia de Algoritmos
[Link] = TrieNode()
# Exemplo
auto = Autocomplete()
palavras = ["amor", "amigo", "amizade", "amar", "alegria"]
for palavra in palavras:
[Link](palavra)
print([Link]("am")) # ['amor', 'amigo', 'amizade']
2. Verificador Ortográfico
class SpellChecker:
def __init__(self):
[Link] = Trie()
# Exemplo
checker = SpellChecker()
checker.add_to_dictionary(["casa", "carro", "carta", "caso"])
print(checker.suggest_corrections("casa")) # ['casa']
print(checker.suggest_corrections("caza")) # ['casa']
Otimizações Avançadas
class CompressedTrieNode:
def __init__(self):
[Link] = {}
self.is_end = False
[Link] = "" # Armazena prefixo comprimido
class CompressedTrie:
def __init__(self):
[Link] = CompressedTrieNode()
node = [Link][word[i]]
prefix = [Link]
if j < len(prefix):
# Divide o nó atual
novo_no = CompressedTrieNode()
novo_no.prefix = prefix[j:]
novo_no.is_end = node.is_end
novo_no.children = [Link]
[Link] = prefix[:j]
node.is_end = (i == len(word))
[Link] = {prefix[j]: novo_no}
if i < len(word):
continue
else:
node.is_end = True
break
# Exemplo
trie = CompressedTrie()
[Link]("romane")
[Link]("romanus")
[Link]("romulus")
[Link]("rubens")
Próximos passos
Após dominar os conceitos básicos de Trie, explore variações como Ternary Search Tree, Radix Tree
Guiae Suffix
de Algoritmos
Tree. Estude também aplicações práticas em sistemas de autocompletar, corretores
ortográficos e processamento de linguagem natural.