← Painel 60 min
Base

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.

VISUALIZAÇÃOVisualização passo a passo
1 / 6
O mapa: toda pergunta começa com 'qual é a forma da entrada?' — ordenada, sequência, hierarquia ou escolhas Visualizacao deterministica do tipo tree ?ord.seq.árv.esc.BS2PSWPSDFSBFSBT O mapa: toda pergunta começa com 'qual é a forma da entrada?' — ordenada, sequência, hierarquia ou escolhas

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=12;
  • values=[4 7 1 9], target=5-1;
  • values=[3 3 3], target=30 (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 segundo 5 chega no índice 3; o segundo 2 só 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=9true (8 + 1);
  • values=[6], target=12false (o mesmo elemento não pode ser usado duas vezes);
  • values=[6 6], target=12true (dois elementos distintos com o mesmo valor);
  • values=[-4 10 2], target=6true (−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 map usado 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.
Essa lição ficou clara?