24 de Março
Adicionar e buscar palavras com árvore de prefixos (trie)
Como usar uma árvore de prefixos (trie) com busca em profundidade (DFS) para suportar o curinga . com ramificação controlada.
Árvore de Prefixos Intermediario 25 min TypeScript
Projete uma estrutura que suporte:
addWord(word)search(word)
search deve aceitar . como curinga que representa exatamente um caractere qualquer.
Na versão original, isso costuma vir como uma classe chamada WordDictionary. Aqui no SeniorPath, use:
operations: lista de operaçõesvalues: palavra associada a cada operação, ounullquando não houver
E retorne um array com as respostas de cada operação. Operações que não retornam valor devem devolver null.
Exemplo:
Input:
operations = ["WordDictionary","addWord","addWord","addWord","search","search","search","search"]
values = [null,"bad","dad","mad","pad","bad",".ad","b.."]
Output:
[null,null,null,null,false,true,true,true]
O que perceber antes de codar
- O curinga `.` vale por um único caractere, não por qualquer sufixo inteiro.
- O problema fica melhor quando você pensa em prefixo compartilhado, não em lista de palavras soltas.
- Quando aparece `.`, a busca deixa de seguir um caminho só e passa a testar vários filhos a partir do no atual.
Erros comuns
- guardar tudo em um array e comparar palavra por palavra em cada busca
- tratar `.` como "qualquer resto da palavra" em vez de "qualquer caractere único"
Passo 1 - Entender por que o array e um ponto de partida fraco
Se você guardar todas as palavras em uma lista, addWord e fácil.
Mas search vira:
- percorre cada palavra
- compara caractere por caractere
- trata
.como exceção
Funciona, mas a busca piora conforme o dicionario cresce.
Passo 2 - Perguntar o que o problema quer reaproveitar
Palavras começam igual o tempo todo:
badbagbat
Todas compartilham o prefixo ba.
Se você guarda esse prefixo uma vez só, a estrutura fica mais inteligente.
Passo 3 - Chegar na árvore de prefixos (trie)
Em uma árvore de prefixos (trie):
- cada aresta representa um caractere
- cada no representa um prefixo
- um marcador diz se aquele no fecha uma palavra completa
addWord anda caractere por caractere criando filhos quando precisar.
Passo 4 - Entender por que o curinga pede busca em profundidade (DFS)
Quando o caractere e normal, a busca sabe exatamente qual filho seguir.
Quando aparece ., ela precisa tentar todos os filhos possiveis daquele no.
Isso transforma search em uma pequena busca em profundidade via busca em profundidade (DFS) sobre essa árvore de prefixos.
O editor interativo precisa de JavaScript. Voce ainda pode ler o desafio e copiar o codigo inicial abaixo.
Codigo inicial
export function runWordDictionary(operations, values) {
// operations example: ["WordDictionary", "addWord", "search"]
// values example: [null, "bad", ".ad"]
// sua solução aqui
return []
}
Ainda não resolveu?
Ver a solução agora pode reduzir o aprendizado. Vale a pena tentar mais um pouco.
Abrir a solucao de referencia
Sem JavaScript, a solucao de referencia aparece inline em vez de um dialogo.
Solução
Complexidade final
Tempo: O(word.length) para addWord; search depende do número de nos explorados pelos curingas
Espaço: O(total de caracteres inseridos)
Solução 1 - Array de palavras com busca linear
Bom ponto de partida porque resolve o problema sem mudar a estrutura de dados.
export function runWordDictionary(operations, values) {
const words = []
const result = []
function matches(pattern, word) {
if (pattern.length !== word.length) {
return false
}
for (let index = 0; index < pattern.length; index += 1) {
if (pattern[index] !== "." && pattern[index] !== word[index]) {
return false
}
}
return true
}
for (let index = 0; index < operations.length; index += 1) {
const operation = operations[index]
const value = values[index]
if (operation === "WordDictionary") {
result.push(null)
} else if (operation === "addWord") {
words.push(value)
result.push(null)
} else if (operation === "search") {
result.push(words.some((word) => matches(value, word)))
}
}
return result
}
Ela funciona.
Mas toda busca passa pela lista inteira, mesmo quando várias palavras compartilham prefixo.
Solução 2 - Árvore de prefixos (trie) com busca em profundidade (DFS) para curinga
Agora a estrutura conversa com o problema:
addWorddesce pela árvore criando nossearchfaz busca guiada pelo padrão.abre vários ramos, mas só a partir do no atual
type TrieNode = {
children: Map<string, TrieNode>
isWord: boolean
}
function createNode(): TrieNode {
return {
children: new Map(),
isWord: false,
}
}
export function runWordDictionary(operations, values) {
const root = createNode()
const result = []
function addWord(word: string) {
let node = root
for (const char of word) {
if (!node.children.has(char)) {
node.children.set(char, createNode())
}
node = node.children.get(char)!
}
node.isWord = true
}
function search(word: string): boolean {
function dfs(node: TrieNode, index: number): boolean {
if (index === word.length) {
return node.isWord
}
const char = word[index]
if (char !== ".") {
const next = node.children.get(char)
return next ? dfs(next, index + 1) : false
}
for (const next of node.children.values()) {
if (dfs(next, index + 1)) {
return true
}
}
return false
}
return dfs(root, 0)
}
for (let index = 0; index < operations.length; index += 1) {
const operation = operations[index]
const value = values[index]
if (operation === "WordDictionary") {
result.push(null)
} else if (operation === "addWord") {
addWord(value)
result.push(null)
} else if (operation === "search") {
result.push(search(value))
}
}
return result
}
O ponto forte não e só “usar uma árvore de prefixos”.
E perceber que o curinga não pede comparar tudo.
Ele pede ramificar só quando realmente encontra ..
O que dizer na entrevista
Uma explicação curta e boa seria:
A versão inicial guarda palavras num array e, em cada busca, compara tudo caractere por caractere. Isso funciona, mas ignora prefixos compartilhados. A versão forte usa uma árvore de prefixos para guardar esses prefixos uma vez só. Em
search, caractere normal segue um único filho e.vira umabusca em profundidade (DFS)pelos filhos do no atual. Assim a estrutura reflete exatamente a regra do problema.
Isso mostra que você:
- pensou na estrutura certa para prefixo
- entendeu o custo do curinga
- explicou por que a
busca em profundidade (DFS)aparece - adaptou a interface orientada a classe para o formato usado aqui
Quando esse padrão aparece de novo
A mesma ideia volta quando o problema pede:
- busca por prefixo
- autocomplete
- dicionario de palavras
- curinga por caractere
Se a pergunta central for “como aproveitar prefixos em comum?”, uma árvore de prefixos entra forte na conversa.