Pular para o conteudo principal

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.

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ções
  • values: palavra associada a cada operação, ou null quando 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

Erros comuns

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:

  • bad
  • bag
  • bat

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 []
}
solution.ts
Travado? Dicas disponiveis.

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:

  • addWord desce pela árvore criando nos
  • search faz 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 uma busca 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.

Desafio anterior Soma de combinações

Desafios relacionados

Artigos relacionados