EnglishРусский Map

Быстрое вычисление расстояния Левенштейна с помощью trie

title
Быстрое вычисление расстояния Левенштейна с помощью trie
type
summary
summary
Поиск слов словаря в пределах N правок в 300 раз быстрее за счёт переиспользования строк таблицы Левенштейна по префиксам trie
tags
algorithms, search, data-structures
created
2026-04-16
updated
2026-04-16
lang
ru
translation_of
levenshtein-trie
source_updated
2026-04-16
translated
2026-09-01
translator
lllm/antigravity/gemini-3.7-flash-medium

Классический пост Стива Ханова 2009 года об эффективном нечётком поиске по словарю. Задача: для слова с опечаткой найти все словарные слова в пределах N правок. Наивный подход слишком медлителен для больших словарей. Подход на основе trie работает в 300 раз быстрее.

Наивный подход

Сравнить запрос с каждым словом в словаре, используя стандартный алгоритм динамического программирования для расстояния Левенштейна. Для каждой пары слов заполняется таблица N×M, где каждая ячейка равна минимуму из:

  • Ячейка сверху + 1 (удаление)
  • Ячейка слева + 1 (вставка)
  • Ячейка по диагонали сверху-слева + 0 или 1 (совпадение или замена)

Сложность: O(words × max_length²). Для словаря из 98 568 слов: 4.5 секунды.

Основная идея

При сравнении "food" со словом "care", а затем с "cars", наивный подход пересчитывает всю таблицу с нуля. Но у "care" и "cars" общий префикс. Строки таблицы Левенштейна для "c", "ca", "car" полностью совпадают - отличается только последняя строка.

Trie хранит слова, объединяя общие префиксы в единые пути. Если обходить trie в глубину, слова обрабатываются именно в том порядке, который максимизирует повторное использование строк.

Алгоритм

Рекурсивный обход в глубину (DFS) по trie. Каждый вызов принимает:

  • Текущий узел trie
  • Букву в этом узле
  • Искомое слово
  • Предыдущую строку (из родительского узла)
  • Аккумулятор результатов
  • Максимально допустимое редакционное расстояние

Каждый узел вычисляет одну строку:

def searchRecursive(node, letter, word, previousRow, results, maxCost):
    currentRow = [previousRow[0] + 1]
    
    for column in range(1, len(word) + 1):
        insertCost = currentRow[column - 1] + 1
        deleteCost = previousRow[column] + 1
        replaceCost = previousRow[column - 1]
        if word[column - 1] != letter:
            replaceCost += 1
        currentRow.append(min(insertCost, deleteCost, replaceCost))
    
    # If this node completes a word and cost is acceptable
    if node.word and currentRow[-1] <= maxCost:
        results.append((node.word, currentRow[-1]))
    
    # Early termination: if min(row) > maxCost, no word below can match
    if min(currentRow) <= maxCost:
        for letter, child in node.children.items():
            searchRecursive(child, letter, word, currentRow, results, maxCost)

Раннее отсечение - ключевой момент. Если минимальное значение в текущей строке превышает maxCost, любое слово в этом поддереве окажется слишком далеко. Вся ветка отсекается целиком.

Производительность

Тот же словарь на 98 568 слов: 0.014 секунды. Более чем в 300 раз быстрее.

Новая оценка сложности: O(max_length × trie_nodes). В trie меньше узлов, чем суммарное количество символов во всём словаре, поскольку префиксы объединены.

Практическое применение

Автор использовал этот подход для RhymeBrain. После импорта набора данных Google N-grams (2.6 миллиона слов) запросы всё ещё выполнялись всего за 19-50 мс на нетбуке с процессором 1 ГГц. Без кэширования, без предварительных вычислений - только trie и алгоритм.

Альтернативы

Корректор опечаток Питера Норвига - генерация всех возможных мутаций запроса с 1 правкой и проверка их наличия в словаре. Быстро работает для расстояния редактирования 1, но "быстро ломается" на больших расстояниях, так как количество мутаций растёт экспоненциально.

Автоматы Левенштейна - построение регулярного выражения / NFA, сопоставляющего все строки в пределах N правок, с последующим пересечением со словарём. Сложнее в реализации ("огромные горы кода"), но может работать быстрее на очень больших словарях.

Память

Trie потребляет много памяти. В статье упоминаются последующие работы со структурами DAWG (Directed Acyclic Word Graph), которые объединяют не только префиксы, но и суффиксы, кардинально снижая число узлов.

См. также inverted-index о другом подходе к поисковому индексированию, full-text-search-indexeddb о ещё одной реализации нечёткого поиска.