09/11/2025
E aí, pessoal. Hoje eu quero falar sobre um daqueles desafios de código que parecem simples, mas que escondem uma armadilha que me fez bater a cabeça por um tempo. Sabe, um daqueles problemas do HackerRank (ou plataforma similar) onde o objetivo é simples, mas o código-base que eles te dão é... uma pegadinha.
Sem eufemismos: o problema não era complexo, mas o código fornecido era mal escrito e feito para te induzir ao erro.
O objetivo era simples: pegar uma série de frases e remover todas as palavras duplicadas consecutivas, ignorando se são maiúsculas ou minúsculas.
Goodbye bye bye world world worldGoodbye bye worldHello hello Ab aBHello AbSimples, né? "Ah, é só usar RegEx!", eu pensei. E eu estava certo. O problema é que eles não queriam só a RegEx. Eles te dão este esqueleto de código em Java 7:
// ...
String regex = "/* Escreva sua RegEx aqui */";
Pattern p = Pattern.compile(regex, /* Insira a flag correta aqui */);
// ...
while (m.find()) {
input = input.replaceAll(/* O regex para substituir */, /* A substituição */);
}
// ...
E é aí que mora o perigo.
while (m.find())Qualquer pessoa que trabalha com RegEx olharia para isso e pensaria: "Ué, para quê o loop while?".
A solução mais limpa e óbvia para esse problema seria UMA linha:
input = input.replaceAll(MINHA_REGEX, "$1");
...e pronto. O replaceAll já faria o trabalho na string inteira de uma vez.
Mas não. O desafio força você a usar esse loop while (m.find()). E se você tentar a solução óbvia (o replaceAll global) dentro do loop, tudo quebra. O Matcher (m) foi criado com a string original, mas o input é modificado a cada iteração. É uma bagunça que falha em vários casos de teste ocultos.
Depois de algumas tentativas, a ficha caiu. Eu não tinha que lutar contra o loop, eu tinha que usá-lo da forma mais literal e estranha possível.
A solução é dividida em três partes, exatamente como o stub pedia.
Primeiro, a expressão regular. Ela precisava encontrar palavras duplicadas e ignorar o caso.
String regex = "(?i)\\b([a-zA-Z]+)\\b(?:\\s+\\1\\b)+";
Vamos quebrar isso:
(?i): Esta é a flag "inline". Ela diz para a RegEx: "ignore maiúsculas/minúsculas". Isso é crucial.\\b([a-zA-Z]+)\\b: Isso encontra uma palavra (\b é a fronteira da palavra) e, graças aos parênteses (), captura essa palavra no Grupo 1.(?:\\s+\\1\\b)+: Esta é a mágica.\\s+: Encontra um ou mais espaços.\\1: Procura exatamente o texto que foi capturado no Grupo 1 (a nossa palavra).\\b: Outra fronteira de palavra.(?:...)+: O + significa "encontre uma ou mais repetições disso".Essa RegEx encontra bye bye bye (em Goodbye bye bye...) e Ab aB (em Hello hello Ab aB...) de uma só vez, graças ao (?i).
O segundo buraco era a flag do Pattern.compile.
Pattern p = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
"Mas espera," você diz, "você já não usou (?i)?". Sim. Isso é 100% redundante. Mas o desafio tem um buraco ali, e a resposta "correta" para aquele buraco é Pattern.CASE_INSENSITIVE. Então, a gente preenche o que eles pedem.
Aqui é onde a solução da armadilha se revela. Em vez de usar a nossa RegEx no replaceAll, nós usamos os resultados exatos do Matcher.
while (m.find()) {
input = input.replaceAll(m.group(0), m.group(1));
}
Vamos ver o que é isso, sem eufemismos:
m.find() (que é case-insensitive) encontra a primeira ocorrência. Digamos, Hello hello.m.group(0) retorna a string exata que ele encontrou: "Hello hello".m.group(1) retorna o que foi capturado no Grupo 1: "Hello".input.replaceAll("Hello hello", "Hello");m.find() (ainda na string original) acha a próxima: Ab aB.m.group(0) retorna "Ab aB".m.group(1) retorna "Ab".input.replaceAll("Ab aB", "Ab");Note que o replaceAll aqui está sendo usado de forma case-sensitive (o padrão), mas não importa, porque m.group(0) nos dá a string exata que foi encontrada pelo Matcher (que era case-insensitive).
Foi isso que passou em todos os testes.
A lição aqui não foi só sobre RegEx. Foi sobre como, às vezes, em desafios de código, você não está lutando contra o problema, está lutando contra um código-base mal feito. A solução não é a mais eficiente, mas é a única que se encaixa nas restrições bizarras do desafio.
RegEx é uma ferramenta poderosa, mas entender o contexto (e as armadilhas) onde ela é usada é o que realmente resolve o problema.
E vocês? Já caíram numa pegadinha de desafio assim? Deixa aí nos comentários.
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class DuplicateWords {
public static void main(String[] args) {
/* 1. O RegEx.
* (?i) -> Flag inline para case-insensitive.
* \b([a-zA-Z]+)\b -> Grupo 1: Captura a palavra.
* (?:\s+\1\b)+ -> Encontra uma ou mais repetições (com espaço antes).
*/
String regex = "(?i)\\b([a-zA-Z]+)\\b(?:\\s+\1\\b)+";
/* 2. A Flag de Compilação.
* Pattern.CASE_INSENSITIVE é a resposta correta aqui.
* Isso fará com que m.find() encontre "Hello hello" ou "Ab aB".
*/
Pattern p = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
Scanner in = new Scanner(System.in);
int numSentences = Integer.parseInt(in.nextLine());
while (numSentences-- > 0) {
String input = in.nextLine();
Matcher m = p.matcher(input);
// Itera por cada correspondência encontrada
while (m.find()) {
/* 3. O replaceAll.
* Desta vez, vamos substituir o texto exato que o matcher encontrou.
* m.group(0) -> O texto completo correspondente (ex: "Hello hello")
* m.group(1) -> O texto do primeiro grupo (ex: "Hello")
* O replaceAll aqui será case-sensitive, mas m.group(0)
* nos dá a string exata para substituir.
*/
input = input.replaceAll(m.group(0), m.group(1));
}
// Imprime a sentença modificada.
System.out.println(input);
}
in.close();
}
}