Структура данных Trie в Java

Структура данных Trie в Java

1. обзор

Структуры данных представляют собой важный актив в компьютерном программировании, и очень важно знать, когда и зачем их использовать.

Эта статья представляет собой краткое введение в структуру данных trie (произносится как «попробуй»), ее реализацию и анализ сложности.

2. Trie

Trie - это дискретная структура данных, которая не совсем известна или широко не упоминается в типичных курсах алгоритмов, но, тем не менее, важна.

Древовидная структура (также известная как цифровое дерево), а иногда даже радикальное дерево или дерево префиксов (поскольку их можно искать по префиксам), представляет собой упорядоченную древовидную структуру, которая использует преимущества хранящихся в ней ключей - обычно строк.

Положение узла в дереве определяет ключ, с которым этот узел связан, что отличает попытки по сравнению с деревьями двоичного поиска, в которых узел хранит ключ, соответствующий только этому узлу.

Все потомки узла имеют общий префиксString, связанный с этим узлом, тогда как корень связан с пустымString.

Здесь у нас есть предварительный просмотрTrieNode, который мы будем использовать в нашей реализацииTrie:

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

   // ...
}

Могут быть случаи, когда три представляет собой двоичное дерево поиска, но в целом они различны. И бинарные деревья поиска, и попытки являются деревьями, но у каждого узла в бинарных деревьях поиска всегда есть два потомка, тогда как у узлов попыток, с другой стороны, может быть больше.

В дереве каждый узел (кроме корневого узла) хранит один символ или цифру. Переходя по дереву вниз от корневого узла к конкретному узлуn, можно сформировать общий префикс символов или цифр, который также используется другими ветвями дерева.

Путем перехода вверх по дереву от листового узла к корневому узлу можно сформироватьString или последовательность цифр.

Вот классTrie, который представляет реализацию структуры данных trie:

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

3. Общие операции

Теперь давайте посмотрим, как реализовать основные операции.

3.1. Вставка элементов

Первая операция, которую мы опишем, - это вставка новых узлов.

Прежде чем приступить к реализации, важно понять алгоритм:

  1. Установить текущий узел в качестве корневого узла

  2. Установить текущую букву в качестве первой буквы слова

  3. Если текущий узел уже имеет существующую ссылку на текущую букву (через один из элементов в поле «children»), тогда установите текущий узел на этот ссылочный узел. В противном случае создайте новый узел, установите букву, равную текущей букве, а также инициализируйте текущий узел этим новым узлом.

  4. Повторите шаг 3, пока ключ не пройден

Сложность этой операции составляетO(n), гдеn представляет размер ключа.

Вот реализация этого алгоритма:

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);
}

Теперь давайте посмотрим, как мы можем использовать этот метод для вставки новых элементов в дерево:

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;
}

Мы можем проверить, что три уже заполнены новыми узлами из следующего теста:

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

    assertFalse(trie.isEmpty());
}

3.2. Поиск элементов

Давайте теперь добавим метод, чтобы проверить, присутствует ли уже определенный элемент в дереве:

  1. Получить детей корня

  2. Перебрать каждый символString

  3. Проверьте, является ли этот символ уже частью поддерева. Если его нет в дереве, остановите поиск и вернитеfalse

  4. Повторяйте второй и третий шаг, пока вString. не останется ни одного символа. Если достигнут конецString, вернутьtrue

Сложность этого алгоритма составляетO(n), где n представляет длину ключа.

Реализация Java может выглядеть так:

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();
}

И в действии:

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

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

3.3. Удаление элемента

Очевидно, что помимо вставки и поиска элемента нам также необходимо иметь возможность удалять элементы.

Для процесса удаления нам нужно выполнить следующие шаги:

  1. Проверьте, является ли этот элемент уже частью дерева

  2. Если элемент найден, то удалите его из дерева

Сложность этого алгоритма составляетO(n), где n представляет длину ключа.

Давайте быстро посмотрим на реализацию:

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;
}

И в действии:

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

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

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

4. Заключение

В этой статье мы увидели краткое введение в структуру данных trie, ее наиболее распространенные операции и их реализацию.

Полный исходный код примеров, показанных в этой статье, можно найти вover on GitHub.