MidТеория8 min

Trie, Radix и Patricia

Префиксные деревья: trie, compressed radix tree, Patricia trie. Autocomplete, IP routing, Redis keys

Мотивация

Hash map даёт O(1) point lookup, но:

  • Не поддерживает prefix search ("все ключи на dev*").
  • Не поддерживает sorted iteration.
  • Большой оверхед пустых bucket'ов.

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

Trie: простая версия

Trie (произносится "try") -- дерево, где каждый узел соответствует одной букве (или биту), а путь от корня до узла -- префикс ключа.

Ключи: {"cat", "car", "cart", "can", "dog", "do"}

             root
            /    \
           c      d
           |      |
           a      o * (значение для "do")
          /|\     |
         n t r    g *
         *  *|
           car*
           |
           t *

* -- нода помечена как конец ключа (имеет значение)

Search "car":

  • root → c → a → r → (помечен end-of-key) → найдено.

Insert "carp":

  • root → c → a → r (уже есть), добавляем дочку p → мечаем как end.

Свойства

  • Search / Insert: O(k), где k -- длина ключа. Не зависит от n (числа ключей)!
  • Память: O(total chars in all keys) в худшем случае.
  • Sorted iteration: DFS с предпочтением младшей буквы -- лексикографический порядок.
  • Prefix search: "найти все на ca*" = найти узел ca, dump subtree.

Проблема наивного trie

Каждая нода хранит массив детей. Если алфавит 256 символов (байты), нода = 256 × 8 байт = 2 KB. На короткие ключи trie может быть больше самих данных.

Решения:

  • Хеш-мап детей вместо массива.
  • Sorted array + binary search.
  • Bit-vector + popcount (как в FST).

Radix tree (compressed trie)

Radix tree (или compact prefix tree) объединяет цепочки узлов с одним ребёнком в одну ноду, подписанную всей подстрокой.

Обычный trie для {"romane", "romanus", "romulus"}:

   r-o-m-a-n-e *
           |
           u-s *
   r-o-m-u-l-u-s *

Radix tree:

   "rom" ───┬──"an" ─┬─ "e" *
            │         └─ "us" *
            └──"ulus" *

Экономия для ключей с длинными префиксами огромна.

Свойства

  • Search / Insert: O(k).
  • Memory: ~O(n × avg_compressed_depth), часто в 10× меньше обычного trie.
  • Branching factor небольшой (часто бинарный), но ребро подписано строкой.

Split при вставке

Вставка "roman": нужно разбить ноду "romulus" / "romane".

  1. Найти общий префикс между "roman" и существующим edge.
  2. Разбить edge на две ноды: одна с общим префиксом, две дочки с остатками.

Это единственная сложная операция в radix. Search тривиален.

Patricia trie

PATRICIA (Practical Algorithm To Retrieve Information Coded In Alphanumeric, Morrison 1968) -- бинарный radix tree, где:

  • Разветвление только по битам (0/1), не по буквам.
  • Каждая нода содержит bit-position для теста.
  • Нет "пустых" нод -- если нет разветвления, нода сливается с ребёнком.

Патриция очень эффективна для IP routing tables, где ключи -- битовые строки фиксированной длины.

Пример: IP routing

Routes:
  192.168.0.0/16  → gateway A
  192.168.1.0/24  → gateway B
  10.0.0.0/8      → gateway C

Patricia по битам:
                  root
              bit 0 / \ bit 1
                   ...
                  test bit 8
                  /        \
              10 prefix    192.168 prefix
              gw C         /     \
                       192.168  192.168.1
                       gw A     gw B

Linux kernel использует Patricia-like trie (struct fib_table) для маршрутизации. Lookup IP = O(32) бит = константа.

Сравнение

Свойство Trie Radix Patricia
Узлы Один символ Подстрока Бит-позиция
Ветвление По алфавиту (до 256) По символам подстроки Бинарное (0/1)
Память Большая Компактная Минимальная
Реализация Простая Средняя Сложная
Best for Маленький алфавит, автокомплит Общий prefix tree Битовые ключи (IP)

Где используется

Система Структура Назначение
Redis keys (dict) Radix tree Хранение ключей в Cluster, streams, KEYS pattern
Linux kernel FIB Patricia (variant) IP routing table
Elasticsearch / Lucene FST (automaton) Term dictionary, автокомплит
Aerospike Radix Primary index
PostgreSQL GIN B-tree поверх trie-like Text search
Ethereum Merkle Patricia Trie State tree
React Router Radix URL routing
Fast HTTP routers (Gorilla, gin, chi) Radix URL matching
Autocomplete (Google, Amazon) Trie/radix Prefix search
Prefix-based caching (Varnish) Radix URL prefix match

Merkle Patricia Trie в Ethereum

Ethereum хранит state (accounts, storage) в модифицированной Patricia, где:

  • Ключи -- hash адреса (32 байта = 256 бит).
  • Каждая нода -- Merkle-нода (хеш содержимого).
  • State root = хеш корня = компактное представление всей истории состояния.

Это позволяет:

  • Light clients проверять membership через Merkle proof.
  • Детерминированно хешировать state для consensus.
  • Инкрементально обновлять при изменении одного аккаунта (O(log) работы).

Trie для autocomplete: реализация

<?php

declare(strict_types=1);

/**
 * Standard trie for string autocomplete.
 * For production at scale use FST (Finite State Transducer) or marisa-trie.
 */
final class TrieNode
{
    /** @var array<string, TrieNode> */
    public array $children = [];
    public bool $isEnd = false;
    public int $rank = 0; // popularity / frequency for ranking
}

final class Trie
{
    private TrieNode $root;

    public function __construct()
    {
        $this->root = new TrieNode();
    }

    public function insert(string $word, int $rank = 1): void
    {
        $node = $this->root;
        foreach (mb_str_split(mb_strtolower($word)) as $ch) {
            if (!isset($node->children[$ch])) {
                $node->children[$ch] = new TrieNode();
            }
            $node = $node->children[$ch];
        }
        $node->isEnd = true;
        $node->rank = max($node->rank, $rank);
    }

    public function contains(string $word): bool
    {
        $node = $this->descend($word);
        return $node !== null && $node->isEnd;
    }

    /**
     * Return up to $limit suggestions for given prefix, sorted by rank desc.
     * @return list<array{word: string, rank: int}>
     */
    public function autocomplete(string $prefix, int $limit = 10): array
    {
        $node = $this->descend($prefix);
        if ($node === null) {
            return [];
        }

        $results = [];
        $this->collect($node, mb_strtolower($prefix), $results);

        usort($results, fn(array $a, array $b): int => $b['rank'] <=> $a['rank']);

        return array_slice($results, 0, $limit);
    }

    private function descend(string $prefix): ?TrieNode
    {
        $node = $this->root;
        foreach (mb_str_split(mb_strtolower($prefix)) as $ch) {
            if (!isset($node->children[$ch])) {
                return null;
            }
            $node = $node->children[$ch];
        }
        return $node;
    }

    /** @param list<array{word: string, rank: int}> $results */
    private function collect(TrieNode $node, string $path, array &$results): void
    {
        if ($node->isEnd) {
            $results[] = ['word' => $path, 'rank' => $node->rank];
        }
        foreach ($node->children as $ch => $child) {
            $this->collect($child, $path . $ch, $results);
        }
    }
}

// Usage
// $trie = new Trie();
// $trie->insert('golang', rank: 1000);
// $trie->insert('google', rank: 5000);
// $trie->insert('go', rank: 500);
// print_r($trie->autocomplete('go', limit: 3));
// // [['word'=>'google', 'rank'=>5000], ['word'=>'golang', 'rank'=>1000], ['word'=>'go', 'rank'=>500]]
## Trie vs Hash map для prefix
Операция Trie Hash map
Exact lookup O(k) O(k) hash + O(1)
Prefix match all O(k + output) O(n) linear scan
Sorted iteration Native DFS Нужна sorted struct
Wildcard *abc* Поддерживается Нет
Память O(chars) O(n × k)
Cache locality Плохая (pointer chase) Хорошая

Hash map быстрее для exact lookup на практике. Trie выигрывает, когда запросы -- префиксные.

Production: FST (Finite State Transducer)

Lucene/Elasticsearch используют не trie, а FST -- детерминированный минимизированный automaton. Он сжимает общие суффиксы (trie сжимает только префиксы) и даёт ~5-10× меньшее потребление памяти на типичный dictionary.

Построение FST дороже, но индекс immutable, что делает его приемлемым.

Выводы

  • Trie -- дерево, где путь от корня до узла = ключ; O(k) на операции.
  • Radix tree -- сжатый trie: ребро подписано подстрокой, память ×10 меньше.
  • Patricia -- бинарный radix с bit-testing; оптимален для IP routing и битовых ключей.
  • Main use cases: autocomplete, prefix matching, IP routing, URL routing, term dictionaries.
  • Ethereum Merkle Patricia Trie = Patricia + Merkle = state tree с authentic proofs.
  • В production на больших словарях FST выигрывает у trie; Redis внутри использует radix tree для ключей.
  • Для point lookup hash map часто быстрее на практике; trie нужен именно для prefix / sorted операций.