Técnicas Algorítmicas
Para desenvolvedores sêniores que trabalham intensamente com frameworks como Spring Boot e soluções de persistência como Hibernate, os algoritmos e suas estruturas de projeto subjacentes podem parecer distantes do cotidiano. No entanto, o conhecimento profundo de recursão, divisão e conquista, algoritmos gulosos, backtracking e programação dinâmica é o que diferencia o arquiteto capaz de resolver problemas complexos de desempenho e otimização de recursos daqueles que apenas empilham dependências. Este módulo revisita estes conceitos vitais sob a ótica da JVM, avaliando limitações físicas, trade-offs e corretude.
Recursão e Backtracking — O Fio de Ariadne no Labirinto
Imagine entrar em um labirinto escuro procurando um tesouro sem ter um mapa completo. Uma estratégia puramente recursiva é: avance um passo. Se for um beco sem saída, retorne. Mas como você se lembra do caminho de volta para não se perder? Você usa o 'Fio de Ariadne', que na JVM é representado pela Call Stack (pilha de chamadas). Cada decisão de direção cria um novo nó no fio. Se você bater em uma parede, você recolhe o fio até a última bifurcação (isso é o 'unchoose' ou backtracking) e tenta o outro caminho. O perigo? Se o labirinto for circular e você esquecer de marcar por onde já passou (caso base ou controle de ciclo), o fio acabará de forma catastrófica (o temido StackOverflowError).
Recursão, Call Stack e o Framework de Backtracking
A recursão divide um problema chamando a si mesma com dados reduzidos até atingir o caso base. Sem ele, a execução gera recursão infinita, estourando os limites de memória alocados para a stack da thread (StackOverflowError). Cada chamada recursiva aloca um novo stack frame contendo variáveis locais e o endereço de retorno. Vale ressaltar que a JVM padrão (HotSpot) não realiza Otimização de Chamada de Cauda (Tail Call Optimization - TCO) para poupar memória, pois exige a preservação do stack trace completo para auditoria de segurança baseada em pilha e geração de relatórios de depuração. O Backtracking estende a recursão para problemas de busca em espaços de estados, operando em um ciclo contínuo de: (1) Choose (efetuar alteração no estado), (2) Explore (chamar recursivamente para validar ramificações) e (3) Unchoose (reverter a alteração de estado ao retornar, liberando o caminho para testes alternativos).
Gerador de Permutações via Backtracking
import java.util.*;
public class Permutador {
public static List<List<Integer>> permutar(int[] nums) {
List<List<Integer>> resultado = new ArrayList<>();
// Controlamos o estado usado para poda (pruning)
backtrack(resultado, new ArrayList<>(), nums, new boolean[nums.length]);
return resultado;
}
private static void backtrack(List<List<Integer>> res, List<Integer> temp, int[] nums, boolean[] usado) {
// Caso Base: completou uma permutação de tamanho N
if (temp.size() == nums.length) {
res.add(new ArrayList<>(temp)); // Cria cópia profunda pois temp é mutável e compartilhado
return;
}
for (int i = 0; i < nums.length; i++) {
if (usado[i]) continue; // Poda (pruning): evita processar elementos já presentes
// 1. Choose (Escolher)
usado[i] = true;
temp.add(nums[i]);
// 2. Explore (Explorar)
backtrack(res, temp, nums, usado);
// 3. Unchoose (Desfazer a escolha para o próximo loop)
temp.remove(temp.size() - 1);
usado[i] = false;
}
}
}Algoritmos Gulosos vs DP — A Escalada da Montanha
Imagine escalar uma montanha sob neblina densa. Um algoritmo Guloso (Greedy) equivale a olhar apenas para os seus pés e, a cada bifurcação, escolher a trilha que sobe de forma mais íngreme naquele instante (escolha localmente ótima). É uma estratégia extremamente rápida e simples, mas que pode te deixar preso em um pequeno monte secundário (máximo local) sem nunca alcançar o cume real da montanha. Já a Programação Dinâmica (DP) é como mapear a montanha com um drone. O drone analisa todas as rotas possíveis, anota as altitudes de todos os acampamentos intermediários em uma planilha e calcula a trilha perfeita para o cume verdadeiro, mesmo que isso exija descer alguns metros antes de subir novamente.
Algoritmos Gulosos e a Prova de Corretude por Troca
Algoritmos Gulosos (Greedy) tomam a melhor decisão local a cada passo na expectativa de encontrar o ótimo global. Eles são altamente eficientes porque não reavaliam decisões tomadas. Contudo, só garantem a otimalidade em problemas que exibem a propriedade da escolha gulosa (uma escolha localmente ótima leva a uma solução globalmente ótima) e subestrutura ótima (a solução ótima para o problema contém as soluções ótimas para os subproblemas). A prova matemática clássica de corretude de um algoritmo guloso utiliza o Exchange Argument (Argumento de Troca), demonstrando que se existisse uma solução ótima alternativa que difere da nossa em um passo, poderíamos realizar uma troca para alinhar com a escolha gulosa sem degradar o valor global da solução.
Seleção de Atividades (Algoritmo Guloso)
import java.util.*;
record Atividade(int inicio, int fim) {}
public class SelecaoAtividades {
public static List<Atividade> selecionarMaximo(List<Atividade> atividades) {
List<Atividade> selecionadas = new ArrayList<>();
if (atividades.isEmpty()) return selecionadas;
// Critério guloso provado ótimo: ordenar atividades pelo horário de término (fim)
atividades.sort(Comparator.comparingInt(Atividade::fim));
// Seleciona a primeira atividade (a que termina mais cedo)
Atividade ultima = atividades.get(0);
selecionadas.add(ultima);
for (int i = 1; i < atividades.size(); i++) {
Atividade atual = atividades.get(i);
// Se a atividade atual começa após o término da última, é compatível
if (atual.inicio() >= ultima.fim()) {
selecionadas.add(atual);
ultima = atual; // Escolha gulosa atualizada
}
}
return selecionadas;
}
}Programação Dinâmica — O Caderno de Rascunho
Se você perguntar a uma criança quanto é '1 + 1 + 1 + 1 + 1', ela contará nos dedos e responderá '5'. Se você imediatamente adicionar '+ 1' no final e perguntar 'e agora?', ela dirá instantaneamente '6'. Ela não contou tudo de novo do zero; ela simplesmente lembrou que a soma dos primeiros números era 5 e somou 1 a esse resultado. Isso é Programação Dinâmica. Em vez de recalcular subproblemas sobrepostos repetidamente e desperdiçar tempo de processamento, nós escrevemos as respostas intermediárias em um 'caderno de rascunho' (cache de memoização ou matriz de tabulação) para que possamos consultá-las instantaneamente quando necessário.
DP: Memoization (Top-Down) vs Tabulation (Bottom-Up)
A Programação Dinâmica (DP) aplica-se a problemas que possuem subestrutura ótima e subproblemas sobrepostos. Existem duas formas principais de implementação: (1) Top-Down com Memoization: Mantém a estrutura recursiva natural do problema, mas intercepta cada retorno gravando o resultado em um cache (array ou hash map). É uma abordagem preguiçosa (lazy) porque calcula apenas os estados estritamente necessários. No entanto, sofre com a sobrecarga de stack frames na JVM. (2) Bottom-Up com Tabulation: Remove a recursão completamente. Resolve primeiro os casos base e preenche sistematicamente uma tabela física (array) de baixo para cima. É uma abordagem iterativa e mais performática na JVM. Adicionalmente, a tabulação ordenada permite técnicas de otimização de espaço, reduzindo a complexidade de memória (por exemplo, de O(N) para O(1) em Fibonacci, mantendo apenas as duas últimas posições).
Coin Change (DP Bottom-Up com Otimização)
import java.util.Arrays;
public class CoinChange {
public static int calcularMinimoMoedas(int[] moedas, int valorAlvo) {
// dp[i] armazenará o menor número de moedas para alcançar o valor i
int[] dp = new int[valorAlvo + 1];
// Preenche o array com valor sentinela (simulando infinito)
Arrays.fill(dp, valorAlvo + 1);
dp[0] = 0; // Caso base: 0 moedas para valor 0
// Tabulação Bottom-Up iterativa
for (int i = 1; i <= valorAlvo; i++) {
for (int moeda : moedas) {
if (moeda <= i) {
// Transição: dp[i] é o menor entre o valor atual e usar a moeda
dp[i] = Math.min(dp[i], dp[i - moeda] + 1);
}
}
}
// Se o valor contido no alvo ainda for o sentinela, é impossível dar o troco
return dp[valorAlvo] > valorAlvo ? -1 : dp[valorAlvo];
}
}