Como passar em entrevistas de algoritmos: o mapa de padrões
Intuição
Uma entrevista de algoritmos parece um teste de criatividade: você recebe um problema que nunca viu e precisa inventar uma solução elegante em 40 minutos, com uma pessoa olhando. Visto assim, é assustador — e é exatamente a leitura errada. Quem entrevista há alguns anos sabe que as perguntas não são infinitas. Elas são variações de um punhado de padrões: binary search, two pointers, sliding window, DFS, BFS, backtracking e mais meia dúzia de técnicas de apoio. O problema muda de roupa — “encontre o menor valor no array rotacionado”, “ache a maior substring sem repetição” — mas o esqueleto por baixo se repete.
Isso muda o jogo. Preparar-se para entrevistas não é resolver 500 problemas aleatórios até a sorte sorrir; é aprender a reconhecer qual padrão o problema está vestindo e aplicar um esqueleto que você domina. É a diferença entre memorizar o caminho de casa até 500 endereços e aprender a ler um mapa. O primeiro esforço não transfere: o problema 501 continua sendo um mistério. O segundo transfere sempre, porque a pergunta que você faz diante de qualquer enunciado passa a ser objetiva: qual é a forma da entrada, e o que ela permite descartar?
Existe um segundo motivo, mais traiçoeiro, para não decorar soluções: a entrevista não termina quando o código compila. A pessoa do outro lado quase sempre pergunta “por que left = mid + 1 e não left = mid?” ou “o que acontece se o array estiver vazio?”. Quem decorou trava; quem entende o invariante do padrão responde sem esforço. Os padrões desta trilha são ensinados pelo mecanismo — com visualização passo a passo do estado, não só o código final — precisamente porque é o mecanismo que sustenta essas perguntas de acompanhamento.
Há ainda uma dimensão que cursos de algoritmos costumam ignorar: a linguagem em que você vai escrever. Fazer entrevista em Go é uma vantagem real — a sintaxe é pequena, não há surpresas de tipagem dinâmica e o entrevistador lê seu código sem esforço — mas exige saber de cor a caixa de ferramentas: como um slice cresce, o custo de indexar um map, o que strings.Builder resolve. As aulas 2 e 3 desta trilha cobrem exatamente isso, e o módulo final trata do diferencial que só Go oferece: perguntas de concorrência com goroutines e channels, cada vez mais comuns em entrevistas para vagas de backend.
O risco de ignorar tudo isso tem nome: a solução de força bruta que funciona no exemplo pequeno e estoura o tempo no caso grande. Quase todo problema de entrevista aceita uma resposta O(n²) óbvia — dois loops aninhados testando todas as combinações. Ela raramente é a resposta esperada. O padrão certo derruba o custo para O(n log n), O(n) ou O(log n), e a diferença entre esses números é o assunto da próxima aula. Nesta, você vai ver o mapa completo, entender como cada território se conecta e resolver os três primeiros problemas — incluindo o clássico que transforma um par de loops em uma única passada.
Código anotado
Antes do mapa, uma prova concreta de que padrão não é estética: é custo. O programa abaixo procura um valor em um array ordenado de duas maneiras — percorrendo tudo e cortando pela metade — e conta quantas comparações cada uma gasta. É o mesmo resultado, com esforços muito diferentes.
package main
import "fmt"A busca linear é a força bruta honesta: olha cada posição até achar. O contador registra cada comparação com um elemento do array.
// linearSteps conta comparações da busca linear até achar target.
func linearSteps(values []int, target int) int {
steps := 0
for _, value := range values {
steps++
if value == target {
return steps
}
}
return steps
}A busca binária usa a informação que a linear desperdiça: o array está ordenado. Se o elemento do meio é menor que o alvo, todo mundo à esquerda dele também é — dá para descartar metade do array com uma única comparação. Essa é a essência do padrão que você vai dominar nas aulas 4 a 9.
// binarySteps conta comparações da busca binária no mesmo array.
func binarySteps(values []int, target int) int {
steps := 0
left, right := 0, len(values)-1
for left <= right {
mid := left + (right-left)/2
steps++
if values[mid] == target {
return steps
}Quando values[mid] não é o alvo, uma metade inteira morre. Repare que mid já foi testado, então os novos limites pulam sobre ele — esse detalhe evita o loop infinito, uma armadilha que a aula de binary search disseca.
if values[mid] < target {
left = mid + 1
} else {
right = mid - 1
}
}
return steps
}O main monta um array ordenado com 1.000.000 de elementos e procura o pior caso da busca linear: o último valor. Um milhão de comparações contra ~20. É essa razão, não o gosto pessoal, que faz o entrevistador esperar o padrão certo.
func main() {
values := make([]int, 1_000_000)
for index := range values {
values[index] = index * 2
}
target := values[len(values)-1]
fmt.Println("linear:", linearSteps(values, target), "comparações")
fmt.Println("binária:", binarySteps(values, target), "comparações")
}A saída mostra linear: 1000000 e binária: 20 (ou perto disso, conforme a posição do alvo nas divisões). Dobrar o tamanho do array dobra o custo da linear e adiciona uma comparação à binária. Guarde essa assimetria: ela é o que O(n) contra O(log n) significa na prática, e a aula 2 dá o vocabulário completo.
O mapa abaixo é a espinha dorsal da trilha. Cada pergunta de entrevista começa com “qual é a forma da entrada?” — e cada resposta abre um território com dois ou três padrões. Avance os passos para ver como os módulos M1 a M4 se encaixam.
Traduzindo o mapa em módulos: entrada ordenada (ou que separa em “inviável | viável”) leva a binary search (aulas 4–9). Problemas de subarray, substring ou pares em uma sequência levam a two pointers, sliding window e prefix sums (aulas 10–15). Estruturas hierárquicas ou conectadas — árvores, grafos, matrizes — pedem DFS (16–21) ou BFS (28–32), e a escolha entre eles depende de você precisar explorar tudo ou achar o caminho mais curto. Problemas de gerar ou contar combinações de escolhas pedem backtracking (22–27). Por fim, o módulo de concorrência (33–36) cobre as perguntas de goroutines e channels que diferenciam uma vaga Go.
Experimente
Antes de rodar, preveja os dois números que o programa imprime para size = 1000. Depois teste size igual a 10.000 e 100.000: a coluna da binária deve crescer pouquíssimo enquanto a linear explode. Por fim, mude target para values[0] — por que a linear vence nesse caso específico, e por que isso não muda a análise de pior caso?
package main
import "fmt"
func steps(values []int, target int) (linear, binary int) {
for _, value := range values {
linear++
if value == target {
break
}
}
left, right := 0, len(values)-1
for left <= right {
mid := left + (right-left)/2
binary++
if values[mid] == target {
return linear, binary
}
if values[mid] < target {
left = mid + 1
} else {
right = mid - 1
}
}
return linear, binary
}
func main() {
size := 1000
values := make([]int, size)
for index := range values {
values[index] = index
}
target := values[size-1]
linear, binary := steps(values, target)
fmt.Println("linear:", linear, "| binária:", binary)
}Exercício 1
Busca linear: o ponto de partida honesto
Todo padrão desta trilha é uma resposta à pergunta “como fazer melhor que percorrer tudo?”. Para responder, você precisa primeiro dominar o percorrer tudo. Implemente LinearSearch(values []int, target int) int, que devolve o índice da primeira ocorrência de target em values, ou -1 quando ele não aparece.
Entrada: um slice de int (pode ser vazio ou nil) e um alvo. Saída: o índice da primeira ocorrência ou -1.
Exemplos:
values=[4 7 1 9], target=1→2;values=[4 7 1 9], target=5→-1;values=[3 3 3], target=3→0(a primeira ocorrência, não qualquer uma).
Restrições: uma única passada, sem ordenar e sem estruturas auxiliares. O caso vazio não pode causar panic.
Solução & Explicação
Um range com retorno antecipado resolve. O ponto que separa uma implementação limpa de uma com bug é a posição do -1: ele só pode existir depois do loop, quando todas as posições já falharam.
func LinearSearch(values []int, target int) int {
for index, value := range values {
if value == target {
return index
}
}
return -1
}Complexidade: O(n) tempo no pior caso — o alvo está no fim ou não está —, O(1) espaço. Para um slice nil ou vazio o range não executa nenhuma iteração e a função devolve -1 direto, sem caso especial.
A armadilha da alternativa: escrever if value == target { return index } else { return -1 } dentro do loop. Esse código compila e passa no teste em que o alvo está na posição 0 — e devolve -1 errado para qualquer outra posição, porque decide o fracasso olhando só o primeiro elemento. É o bug mais comum de quem está começando, e o motivo de os casos de teste incluírem o alvo no meio e no fim. A regra que você vai reutilizar a trilha inteira: só declare fracasso quando o espaço de busca acabar.
Exercício 2
Primeira repetição: trocando tempo por memória
Este é o primeiro “upgrade de padrão” da trilha. Implemente FirstDuplicate(values []int) int, que devolve o primeiro valor que aparece pela segunda vez ao percorrer values da esquerda para a direita, ou -1 se todos os valores forem únicos.
Entrada: um slice de int. Saída: o valor cuja segunda ocorrência aparece primeiro, ou -1.
Exemplos:
values=[2 5 3 5 2]→5(o segundo5chega no índice 3; o segundo2só no índice 4);values=[1 2 3 4]→-1;values=[7 7]→7.
Repare no primeiro exemplo: a resposta não é o valor que aparece mais cedo (2), e sim o que repete mais cedo. Restrição: sua solução deve fazer uma única passada — a versão com dois loops aninhados funciona, mas o objetivo aqui é derrubá-la.
Solução & Explicação
A pergunta que destrava o problema é “já vi este valor antes?” — e a estrutura de Go que responde isso em O(1) é um map usado como conjunto. Percorra uma vez; se o valor já está no conjunto, ele é a resposta; senão, registre e siga.
func FirstDuplicate(values []int) int {
seen := make(map[int]bool, len(values))
for _, value := range values {
if seen[value] {
return value
}
seen[value] = true
}
return -1
}Complexidade: O(n) tempo e O(n) espaço — o conjunto pode guardar quase todos os valores no pior caso. Consultar uma chave ausente em um map devolve o zero value de bool (false), então não é preciso o idioma valor, ok := aqui; a aula 3 detalha quando ele é obrigatório.
A armadilha da alternativa: o par de loops aninhados — para cada i, procurar values[i] em values[i+1:] — tem dois problemas. O óbvio é o custo O(n²), que estoura o tempo em entradas grandes. O sutil é que ele calcula outra coisa: encontra o valor cuja primeira ocorrência vem mais cedo entre os repetidos, não o que repete mais cedo. Em [2 5 3 5 2] a versão aninhada devolve 2; a especificação pede 5. Força bruta que devolve a resposta errada com convicção é pior que força bruta lenta — e é por isso que reformular o problema (“primeira segunda-ocorrência”) antes de codar vale tanto quanto conhecer a estrutura de dados.
Exercício 3
Par com soma alvo: o aquecimento do clássico
Este é possivelmente o problema mais famoso de entrevistas, na versão de aquecimento. Implemente HasPairSum(values []int, target int) bool, que informa se existem dois índices diferentes i e j com values[i] + values[j] == target.
Entrada: um slice de int (valores podem ser negativos) e um alvo. Saída: true ou false.
Exemplos:
values=[3 8 5 1], target=9→true(8 + 1);values=[6], target=12→false(o mesmo elemento não pode ser usado duas vezes);values=[6 6], target=12→true(dois elementos distintos com o mesmo valor);values=[-4 10 2], target=6→true(−4 + 10).
Restrições: uma única passada, combinando o que os dois exercícios anteriores ensinaram. Os casos 3 e 4 derrubam soluções que confundem “mesmo valor” com “mesmo elemento”.
Solução & Explicação
Reformule: para cada value, o parceiro necessário é exatamente target - value. Se esse complemento já passou pelo loop, o par existe. Um conjunto dos valores já vistos responde a consulta em O(1) — e a ordem das operações resolve o caso [6 6] de graça.
func HasPairSum(values []int, target int) bool {
seen := make(map[int]bool, len(values))
for _, value := range values {
if seen[target-value] {
return true
}
seen[value] = true
}
return false
}Complexidade: O(n) tempo, O(n) espaço. A consulta acontece antes da inserção: quando o loop chega ao primeiro 6 de [6 6] com alvo 12, seen ainda está vazio e a consulta por 12-6=6 falha; o 6 entra no conjunto; no segundo 6, a consulta encontra o primeiro. Dois elementos distintos, mesmo valor — correto. Se você invertesse (inserir antes de consultar), o primeiro 6 encontraria a si mesmo e values=[6], target=12 devolveria true errado.
A armadilha da alternativa: além do loop duplo O(n²), existe uma alternativa genuinamente boa — ordenar e usar two pointers nas pontas, O(n log n) de tempo com O(1) de espaço extra. Ela é o tema da aula 12 (direções opostas), e em array já ordenado é a resposta ideal. Aqui, com array sem ordem, ordenar custa mais que a passada única com map e ainda destrói os índices originais — o que mataria a variação da pergunta que pede as posições do par, não só a existência. Saber as duas e justificar a escolha é exatamente o que o entrevistador quer ouvir.
Resumo
- Entrevistas de algoritmos repetem um punhado de padrões; reconhecer a forma da entrada (ordenada, sequência, hierarquia, escolhas) aponta o padrão antes de qualquer linha de código.
- Decorar soluções falha nas perguntas de acompanhamento; entender o mecanismo do padrão — o que ele descarta e por quê — sustenta o “por que
mid + 1?”. - A força bruta O(n²) quase sempre existe e quase nunca é a resposta: em 1.000.000 de elementos, a busca linear gastou 1.000.000 de comparações e a binária, ~20.
- Um
mapusado como conjunto troca tempo por memória e converte dois loops aninhados em uma passada — e a ordem consulta-depois-insere resolve o caso do par[6 6]. - Próxima aula: entrevistas/02-big-o-na-pratica-em-go dá nome e régua a essa diferença de custo — O(1), O(log n), O(n), O(n²) — medida em código Go de verdade.