A estrutura de dados Trie em Java

A estrutura de dados Trie em Java

1. Visão geral

As estruturas de dados representam um ativo crucial na programação de computadores, e é importante saber quando e por que usá-las.

Este artigo é uma breve introdução à estrutura de dados trie (pronunciada "tentativa"), sua implementação e análise de complexidade.

2. Trie

Um trie é uma estrutura de dados discreta que não é muito conhecida ou amplamente mencionada em cursos de algoritmo típicos, mas, no entanto, é importante.

Um trie (também conhecido como árvore digital) e, às vezes, até árvore de raiz ou árvore de prefixo (como eles podem ser pesquisados ​​por prefixos), é uma estrutura de árvore ordenada, que tira proveito das chaves que ele armazena - geralmente strings.

A posição de um nó na árvore define a chave com a qual esse nó está associado, o que faz tentativas diferentes em comparação às árvores de busca binárias, nas quais um nó armazena uma chave que corresponde apenas a esse nó.

Todos os descendentes de um nó têm um prefixo comum deString associado a esse nó, enquanto a raiz está associada a umString. vazio

Aqui temos uma prévia deTrieNode que usaremos em nossa implementação doTrie:

public class TrieNode {
    private HashMap children;
    private String content;
    private boolean isWord;

   // ...
}

Pode haver casos em que um trie é uma árvore de pesquisa binária, mas, em geral, eles são diferentes. As árvores de pesquisa binária e as tentativas são árvores, mas cada nó nas árvores de pesquisa binária sempre tem dois filhos, enquanto os nós das tentativas, por outro lado, podem ter mais.

Em uma tentativa, cada nó (exceto o nó raiz) armazena um caractere ou um dígito. Percorrendo o teste do nó raiz até um nó específicon, um prefixo comum de caracteres ou dígitos pode ser formado, o qual também é compartilhado por outras ramificações do teste.

Percorrendo o trie de um nó folha para o nó raiz, umString ou uma sequência de dígitos pode ser formado.

Aqui está a classeTrie, que representa uma implementação da estrutura de dados trie:

public class Trie {
    private TrieNode root;
    //...
}

3. Operações Comuns

Agora, vamos ver como implementar operações básicas.

3.1. Inserindo elementos

A primeira operação que descreveremos é a inserção de novos nós.

Antes de iniciarmos a implementação, é importante entender o algoritmo:

  1. Definir um nó atual como nó raiz

  2. Defina a letra atual como a primeira letra da palavra

  3. Se o nó atual já tiver uma referência existente à letra atual (através de um dos elementos no campo "filhos"), defina o nó atual para o nó referenciado. Caso contrário, crie um novo nó, defina a letra igual à letra atual e também inicialize o nó atual para esse novo nó

  4. Repita a etapa 3 até a chave ser movimentada

A complexidade desta operação éO(n), onden representa o tamanho da chave.

Aqui está a implementação deste algoritmo:

public void insert(String word) {
    TrieNode current = root;

    for (int i = 0; i < word.length(); i++) {
        current = current.getChildren()
          .computeIfAbsent(word.charAt(i), c -> new TrieNode());
    }
    current.setEndOfWord(true);
}

Agora vamos ver como podemos usar este método para inserir novos elementos em um trie:

private Trie createExampleTrie() {
    Trie trie = new Trie();

    trie.insert("Programming");
    trie.insert("is");
    trie.insert("a");
    trie.insert("way");
    trie.insert("of");
    trie.insert("life");

    return trie;
}

Podemos testar se a trie já foi preenchida com novos nós a partir do seguinte teste:

@Test
public void givenATrie_WhenAddingElements_ThenTrieNotEmpty() {
    Trie trie = createTrie();

    assertFalse(trie.isEmpty());
}

3.2. Localizando elementos

Agora vamos adicionar um método para verificar se um determinado elemento já está presente em um trie:

  1. Obter filhos da raiz

  2. Repita cada caractere doString

  3. Verifique se esse personagem já faz parte de um subtrie. Se não estiver presente em nenhum lugar do teste, pare a pesquisa e retornefalse

  4. Repita a segunda e a terceira etapa até que não haja mais nenhum caractere emString. Se o final deString for alcançado, retornetrue

A complexidade desse algoritmo éO(n), onde n representa o comprimento da chave.

A implementação Java pode se parecer com:

public boolean find(String word) {
    TrieNode current = root;
    for (int i = 0; i < word.length(); i++) {
        char ch = word.charAt(i);
        TrieNode node = current.getChildren().get(ch);
        if (node == null) {
            return false;
        }
        current = node;
    }
    return current.isEndOfWord();
}

E em ação:

@Test
public void givenATrie_WhenAddingElements_ThenTrieContainsThoseElements() {
    Trie trie = createExampleTrie();

    assertFalse(trie.containsNode("3"));
    assertFalse(trie.containsNode("vida"));
    assertTrue(trie.containsNode("life"));
}

3.3. Excluindo um Elemento

Além de inserir e encontrar um elemento, é óbvio que também precisamos ser capazes de excluir elementos.

Para o processo de exclusão, precisamos seguir as etapas:

  1. Verifique se este elemento já faz parte do trie

  2. Se o elemento for encontrado, remova-o da árvore

A complexidade desse algoritmo éO(n), onde n representa o comprimento da chave.

Vamos dar uma olhada rápida na implementação:

public void delete(String word) {
    delete(root, word, 0);
}

private boolean delete(TrieNode current, String word, int index) {
    if (index == word.length()) {
        if (!current.isEndOfWord()) {
            return false;
        }
        current.setEndOfWord(false);
        return current.getChildren().isEmpty();
    }
    char ch = word.charAt(index);
    TrieNode node = current.getChildren().get(ch);
    if (node == null) {
        return false;
    }
    boolean shouldDeleteCurrentNode = delete(node, word, index + 1) && !node.isEndOfWord();

    if (shouldDeleteCurrentNode) {
        current.getChildren().remove(ch);
        return current.getChildren().isEmpty();
    }
    return false;
}

E em ação:

@Test
void whenDeletingElements_ThenTreeDoesNotContainThoseElements() {
    Trie trie = createTrie();

    assertTrue(trie.containsNode("Programming"));

    trie.delete("Programming");
    assertFalse(trie.containsNode("Programming"));
}

4. Conclusão

Neste artigo, vimos uma breve introdução à estrutura de dados trie e suas operações mais comuns e sua implementação.

O código-fonte completo dos exemplos mostrados neste artigo pode ser encontradoover on GitHub.