Подтвердить что ты не робот

Реализация простой Trie для эффективного расчета расстояния Levenshtein - Java

ОБНОВЛЕНИЕ 3

Готово. Ниже приведен код, который наконец прошел все мои тесты. Опять же, это смоделировано после модифицированной версии алгоритма Стива Ханова Мурило Васконело. Спасибо всем, что помогло!

/**
 * Computes the minimum Levenshtein Distance between the given word (represented as an array of Characters) and the
 * words stored in theTrie. This algorithm is modeled after Steve Hanov blog article "Fast and Easy Levenshtein
 * distance using a Trie" and Murilo Vasconcelo revised version in C++.
 * 
 * http://stevehanov.ca/blog/index.php?id=114
 * http://murilo.wordpress.com/2011/02/01/fast-and-easy-levenshtein-distance-using-a-trie-in-c/
 * 
 * @param ArrayList<Character> word - the characters of an input word as an array representation
 * @return int - the minimum Levenshtein Distance
 */
private int computeMinimumLevenshteinDistance(ArrayList<Character> word) {

    theTrie.minLevDist = Integer.MAX_VALUE;

    int iWordLength = word.size();
    int[] currentRow = new int[iWordLength + 1];

    for (int i = 0; i <= iWordLength; i++) {
        currentRow[i] = i;
    }

    for (int i = 0; i < iWordLength; i++) {
        traverseTrie(theTrie.root, word.get(i), word, currentRow);
    }
    return theTrie.minLevDist;
}

/**
 * Recursive helper function. Traverses theTrie in search of the minimum Levenshtein Distance.
 * 
 * @param TrieNode node - the current TrieNode
 * @param char letter - the current character of the current word we're working with
 * @param ArrayList<Character> word - an array representation of the current word
 * @param int[] previousRow - a row in the Levenshtein Distance matrix
 */
private void traverseTrie(TrieNode node, char letter, ArrayList<Character> word, int[] previousRow) {

    int size = previousRow.length;
    int[] currentRow = new int[size];
    currentRow[0] = previousRow[0] + 1;

    int minimumElement = currentRow[0];
    int insertCost, deleteCost, replaceCost;

    for (int i = 1; i < size; i++) {

        insertCost = currentRow[i - 1] + 1;
        deleteCost = previousRow[i] + 1;

        if (word.get(i - 1) == letter) {
            replaceCost = previousRow[i - 1];
        } else {
            replaceCost = previousRow[i - 1] + 1;
        }

        currentRow[i] = minimum(insertCost, deleteCost, replaceCost);

        if (currentRow[i] < minimumElement) {
            minimumElement = currentRow[i];
        }
    }

    if (currentRow[size - 1] < theTrie.minLevDist && node.isWord) {
        theTrie.minLevDist = currentRow[size - 1];
    }

    if (minimumElement < theTrie.minLevDist) {

        for (Character c : node.children.keySet()) {
            traverseTrie(node.children.get(c), c, word, currentRow);
        }
    }
}

ОБНОВЛЕНИЕ 2

Наконец, мне удалось заставить это работать для большинства моих тестовых случаев. Моя реализация является практически прямым переводом с мурило C++ версии алгоритма Стива Ханова. Итак, как я должен реорганизовать этот алгоритм и/или провести оптимизацию? Ниже приведен код...

public int search(String word) {

    theTrie.minLevDist = Integer.MAX_VALUE;

    int size = word.length();
    int[] currentRow = new int[size + 1];

    for (int i = 0; i <= size; i++) {
        currentRow[i] = i;
    }
    for (int i = 0; i < size; i++) {
        char c = word.charAt(i);
        if (theTrie.root.children.containsKey(c)) {
            searchRec(theTrie.root.children.get(c), c, word, currentRow);
        }
    }
    return theTrie.minLevDist;
}
private void searchRec(TrieNode node, char letter, String word, int[] previousRow) {

    int size = previousRow.length;
    int[] currentRow = new int[size];
    currentRow[0] = previousRow[0] + 1;

    int insertCost, deleteCost, replaceCost;

    for (int i = 1; i < size; i++) {

        insertCost = currentRow[i - 1] + 1;
        deleteCost = previousRow[i] + 1;

        if (word.charAt(i - 1) == letter) {
            replaceCost = previousRow[i - 1];
        } else {
            replaceCost = previousRow[i - 1] + 1;
        }
        currentRow[i] = minimum(insertCost, deleteCost, replaceCost);
    }

    if (currentRow[size - 1] < theTrie.minLevDist && node.isWord) {
        theTrie.minLevDist = currentRow[size - 1];
    }

    if (minElement(currentRow) < theTrie.minLevDist) {

        for (Character c : node.children.keySet()) {
            searchRec(node.children.get(c), c, word, currentRow);

        }
    }
}

Спасибо всем, кто внес вклад в этот вопрос. Я пытался заставить работать автоматы Левенштейна, но не смог этого сделать.

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


ОБНОВЛЕНИЕ 1

Итак, я реализовал простую структуру данных Trie и пытался следовать учебному пособию по питону Стива Ханова для вычисления расстояния Левенштейна. На самом деле, я заинтересован в вычислении минимального расстояния Левенштейна между данным словом и словами в три, поэтому я следовал за Мурило Васконселосом версии алгоритма Стива Ханова. Это работает не очень хорошо, но вот мой класс Trie:

public class Trie {

    public TrieNode root;
    public int minLevDist;

    public Trie() {
        this.root = new TrieNode(' ');
    }

    public void insert(String word) {

        int length = word.length();
        TrieNode current = this.root;

        if (length == 0) {
            current.isWord = true;
        }
        for (int index = 0; index < length; index++) {

            char letter = word.charAt(index);
            TrieNode child = current.getChild(letter);

            if (child != null) {
                current = child;
            } else {
                current.children.put(letter, new TrieNode(letter));
                current = current.getChild(letter);
            }
            if (index == length - 1) {
                current.isWord = true;
            }
        }
    }
}

... и класс TrieNode:

public class TrieNode {

    public final int ALPHABET = 26;

    public char letter;
    public boolean isWord;
    public Map<Character, TrieNode> children;

    public TrieNode(char letter) {
        this.isWord = false;
        this.letter = letter;
        children = new HashMap<Character, TrieNode>(ALPHABET);
    }

    public TrieNode getChild(char letter) {

        if (children != null) {
            if (children.containsKey(letter)) {
                return children.get(letter); 
            }
        }
        return null;
    }
}

Теперь я попытался выполнить поиск, как это сделал Мурило Васконселос, но что-то не так, и мне нужна помощь в его отладке. Пожалуйста, дайте предложения о том, как реорганизовать это и/или укажите, где ошибки. Самое первое, что я хотел бы реорганизовать, - это глобальная переменная minCost, но это самая маленькая вещь. Во всяком случае, здесь код...

public void search(String word) {

    int size = word.length();
    int[] currentRow = new int[size + 1];

    for (int i = 0; i <= size; i++) {
        currentRow[i] = i;
    }
    for (int i = 0; i < size; i++) {
        char c = word.charAt(i);
        if (theTrie.root.children.containsKey(c)) {
            searchRec(theTrie.root.children.get(c), c, word, currentRow);
        }
    }
}

private void searchRec(TrieNode node, char letter, String word, int[] previousRow) {

    int size = previousRow.length;
    int[] currentRow = new int[size];
    currentRow[0] = previousRow[0] + 1;

    int replace, insertCost, deleteCost;

    for (int i = 1; i < size; i++) {

        char c = word.charAt(i - 1);

        insertCost = currentRow[i - 1] + 1;
        deleteCost = previousRow[i] + 1;
        replace = (c == letter) ? previousRow[i - 1] : (previousRow[i - 1] + 1);

        currentRow[i] = minimum(insertCost, deleteCost, replace);
    }

    if (currentRow[size - 1] < minCost && !node.isWord) {
        minCost = currentRow[size - 1];
    }
    Integer minElement = minElement(currentRow);
    if (minElement < minCost) {

        for (Map.Entry<Character, TrieNode> entry : node.children.entrySet()) {
            searchRec(node, entry.getKey(), word, currentRow);
        }
    }
}

Прошу прощения за отсутствие комментариев. Так что я делаю не так?

НАЧАЛЬНЫЙ ПОЧТА

Я читал статью " Быстрое и простое расстояние Левенштейна с использованием Trie", в надежде найти эффективный способ вычисления расстояния Левенштейна между двумя строками. Моя главная цель в этом заключается в том, чтобы, учитывая большой набор слов, найти минимальное расстояние Левенштейна между входным словом (ями) и этим набором слов.

В моей тривиальной реализации я вычисляю расстояние Левенштейна между входным словом и набором слов для каждого входного слова и возвращаю минимум. Это работает, но это не эффективно...

Я искал реализации Trie на Java и наткнулся на два, казалось бы, хороших источника:

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

Итак, как мне реализовать простую структуру данных Trie в Java? Моя интуиция подсказывает мне, что каждый TrieNode должен хранить строку, которую он представляет, а также ссылки на буквы алфавита, не обязательно все буквы. Правильна ли моя интуиция?

Как только это будет реализовано, следующая задача - вычислить расстояние Левенштейна. Я прочитал пример кода Python в статье выше, но я не говорю на Python, и моей реализации Java не хватает памяти кучи, как только я запускаю рекурсивный поиск. Итак, как бы я вычислил расстояние Левенштейна, используя структуру данных Trie? У меня есть тривиальная реализация, смоделированная после этого исходного кода, но она не использует Trie... она неэффективна.

Было бы очень приятно увидеть код в дополнение к вашим комментариям и предложениям. В конце концов, это процесс обучения для меня... Я никогда не использовал Trie... поэтому у меня есть чему поучиться на этом опыте.

Благодарю.

PS Я могу предоставить любой исходный код, если это будет необходимо. Кроме того, я уже прочитал и попытался использовать BK-Tree, как предложено в блоге Ника Джонсона, но он не настолько эффективен, как я думаю, может быть... или, возможно, моя реализация неверна.

4b9b3361

Ответ 1

Я реализовал алгоритм, описанный в статье "Быстрое и легкое использование Levenshtein с помощью статьи Trie" на С++, и это очень быстро. Если вы хотите (понимаете С++ лучше, чем Python), я могу где-то пропустить код.

Edit: Я разместил его в блоге .

Ответ 2

Из того, что я могу вам сказать, вам не нужно повышать эффективность Levenshtein Distance, вам нужно хранить ваши строки в структуре, которая перестает вам требовать многомерных вычислений расстояния, т.е. путем обрезки пространства поиска.

Поскольку расстояние Левенштейна является метрикой, вы можете использовать любые индексы метрических пространств, которые используют преимущество неравенства треугольника - вы упомянули BK-деревья, но есть и другие, например. Точка обстрел дерева, Fixed-запросы Дерева, Биссектриса дерева, Пространственная Аппроксимация дерева. Вот их описания:

Дерево Буркхарда-Келлера

Узлы вставляются в дерево следующим образом: Для корня node выберите произвольный элемент из пространства; добавить уникальную маркировку по краям детей, так что значение каждого ребра расстояние от оси вращения до точки элемент; применять рекурсивно, выбирая ребенок как стержень, когда край уже существует.

Дерево с фиксированными запросами

Как и в случае BKT, кроме: Элементы хранятся у листьев; Каждый лист имеет несколько элементов; Для каждого уровня дерева тот же самый стержень б.

Биссекторное дерево

Каждый node содержит два сводных элемента с их радиусом покрытия (максимум расстояние между центральным элементом и любой из его элементов поддерева); Фильтровать по двум устанавливает те элементы, которые наиболее близки к первый стержень и те, которые ближе всего к во-вторых, и рекурсивно построить два поддерева из этих множеств.

Дерево пространственной аппроксимации

Первоначально все элементы находятся в сумке; выберите произвольный элемент - стержень; строить коллекция ближайших соседей внутри диапазон стержня; Положите оставшиеся элемент в сумку ближайшего элемент к нему из только что построенной коллекции; Рекурсивно формировать поддерево из каждого элемент этой коллекции.

Дерево Vantage Point

Выберите стержень из набора по-абстракции; Вычислить медианное расстояние между этим pivot и каждый элемент остальных задавать; Фильтровать элементы из набора влево и правые рекурсивные поддеревья, такие, что с расстояниями, меньшими или равными медианная форма слева и те форму справа.

Ответ 3

Вот пример Levenshtein Automata в Java (EDIT: перенесен на github). Это, вероятно, также будет полезно:

http://svn.apache.org/repos/asf/lucene/dev/trunk/lucene/src/java/org/apache/lucene/util/automaton/ http://svn.apache.org/repos/asf/Lucene/DEV/багажник /Lucene/SRC/тест/орг /Apache/Lucene/Util/автомат/

РЕДАКТИРОВАТЬ: вышеуказанные ссылки, кажется, перешли на GitHub:

https://github.com/apache/lucene-solr/tree/master/lucene/core/src/java/org/apache/lucene/util/automaton https://github.com/apache/lucene-solr/tree/ведущий /Lucene/ядро /SRC/тест/орг /Apache/Lucene/Util/автомат

Похоже, что экспериментальный код Lucene основан на пакете dk.brics.automaton.

Использование выглядит примерно так:

LevenshteinAutomata builder = new LevenshteinAutomata(s);
Automaton automata = builder.toAutomaton(n);
boolean result1 = BasicOperations.run(automata, "foo");
boolean result2 = BasicOperations.run(automata, "bar");

Ответ 4

Во многом алгоритм Стива Ханова (представленный в первой статье, связанной с вопросом, Быстрое и простое расстояние Левенштейна с помощью Trie), порты алгоритма, сделанные Мурило и вами (OP), и, вполне возможно, каждый подходящий алгоритм, включающий Trie или подобную структуру, функционируют так же, как и Levenshtein Automaton (который упоминался здесь несколько раз):

Given:
       dict is a dictionary represented as a DFA (ex. trie or dawg)
       dictState is a state in dict
       dictStartState is the start state in dict
       dictAcceptState is a dictState arrived at after following the transitions defined by a word in dict
       editDistance is an edit distance
       laWord is a word
       la is a Levenshtein Automaton defined for laWord and editDistance
       laState is a state in la
       laStartState is the start state in la
       laAcceptState is a laState arrived at after following the transitions defined by a word that is within editDistance of laWord
       charSequence is a sequence of chars
       traversalDataStack is a stack of (dictState, laState, charSequence) tuples

Define dictState as dictStartState
Define laState as laStartState
Push (dictState, laState, "") on to traversalDataStack
While traversalDataStack is not empty
    Define currentTraversalDataTuple as the the product of a pop of traversalDataStack
    Define currentDictState as the dictState in currentTraversalDataTuple
    Define currentLAState as the laState in currentTraversalDataTuple
    Define currentCharSequence as the charSequence in currentTraversalDataTuple
    For each char in alphabet
        Check if currentDictState has outgoing transition labeled by char
        Check if currentLAState has outgoing transition labeled by char
        If both currentDictState and currentLAState have outgoing transitions labeled by char
            Define newDictState as the state arrived at after following the outgoing transition of dictState labeled by char
            Define newLAState as the state arrived at after following the outgoing transition of laState labeled by char
            Define newCharSequence as concatenation of currentCharSequence and char
            Push (newDictState, newLAState, newCharSequence) on to currentTraversalDataTuple
            If newDictState is a dictAcceptState, and if newLAState is a laAcceptState
                Add newCharSequence to resultSet
            endIf
        endIf
    endFor
endWhile

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

Если вы (или кто-либо еще) заинтересованы в формальном решении Leventhtein Automaton, посмотрите LevenshteinAutomaton. Он реализует вышеупомянутый алгоритм с параметрическим состоянием, а также чистый алгоритм, основанный на конкретном состоянии, описанный выше, и алгоритмы на основе динамического программирования (как для определения расстояния, так и для определения соседей). Он поддерживается вашим поистине:).

Ответ 5

Моя интуиция подсказывает мне, что каждый TrieNode должен хранить String, который он представляет, а также ссылки на буквы алфавита, не обязательно все буквы. Является ли моя интуиция правильной?

Нет, trie не представляет String, он представляет собой набор строк (и всех их префиксов). Элемент trie node отображает входной символ в другой trie node. Поэтому он должен содержать нечто вроде массива символов и соответствующего массива ссылок TrieNode. (Возможно, это не точное представление, в зависимости от эффективности вашего конкретного использования.)

Ответ 6

Как я понимаю, вы хотите перебрать все ветки trie. Это не так сложно, используя рекурсивную функцию. Я использую trie также в моем алгоритме k-ближайшего соседа, используя ту же функцию. Однако я не знаю Java, но здесь есть псевдокод:

function walk (testitem trie)
   make an empty array results
   function compare (testitem children distance)
     if testitem = None
        place the distance and children into results
     else compare(testitem from second position, 
                  the sub-children of the first child in children,
                  if the first item of testitem is equal to that 
                  of the node of the first child of children 
                  add one to the distance (! non-destructive)
                  else just the distance)
        when there are any children left
             compare (testitem, the children without the first item,
                      distance)
    compare(testitem, children of root-node in trie, distance set to 0)
    return the results

Надеюсь, что это поможет.

Ответ 7

Функция walk принимает testitem (например, индексируемую строку или массив символов) и trie. Trie может быть объектом с двумя слотами. Один из них указывает на node trie, другой - на дочерние элементы node. Дети тоже стараются. В python это будет примерно так:

class Trie(object):
    def __init__(self, node=None, children=[]):
        self.node = node
        self.children = children

Или в Lisp...

(defstruct trie (node nil) (children nil))

Теперь trie выглядит примерно так:

(trie #node None
      #children ((trie #node f
                       #children ((trie #node o
                                        #children ((trie #node o
                                                         #children None)))
                                  (trie #node u
                                        #children ((trie #node n
                                                         #children None)))))))

Теперь внутренняя функция (которую вы также можете записать отдельно) принимает testitem, дочерние элементы корня node дерева (из которых значение node равно None или что-то еще), а начальное расстояние, установленное на 0.

Затем мы просто рекурсивно пересекаем обе ветки дерева, начиная с левого и правого.

Ответ 9

Я смотрел ваше последнее обновление 3, алгоритм, похоже, не работает хорошо для меня.

Пусть у вас есть ниже тестовых примеров:

    Trie dict = new Trie();
    dict.insert("arb");
    dict.insert("area");

    ArrayList<Character> word = new ArrayList<Character>();
    word.add('a');
    word.add('r');
    word.add('c');

В этом случае минимальное расстояние редактирования между "arc" и dict должно быть 1, что является расстоянием редактирования между "arc" и "arb", но вместо этого алгоритмы вернут 2.

Я просмотрел следующую часть кода:

        if (word.get(i - 1) == letter) {
            replaceCost = previousRow[i - 1];
        } else {
            replaceCost = previousRow[i - 1] + 1;
        }

Как минимум для первого цикла, буква является одним из символов в слове, но вместо этого вы должны сравнивать узлы в trie, поэтому будет один дубликат строки с первым символом в слове, это так? каждая матрица DP имеет первую строку как дубликат. Я выполнил тот же код, который вы положили на решение.

Ответ 10

Ну, вот как я это сделал давным-давно. Я сохранил словарь как trie, который является просто конечным автоматом, ограниченным формой дерева. Вы можете улучшить его, не делая этого ограничения. Например, обычные суффиксы могут просто быть общим поддеревом. У вас могут быть даже петли, чтобы захватить такие вещи, как "нация", "национальный", "национализировать", "национализация",...

Держите trie как можно более простым. Не входите в него строки.

Помните, что вы не делаете этого, чтобы найти расстояние между двумя заданными строками. Вы используете его, чтобы найти строки в словаре, которые ближе всего к одной заданной строке. Время, затраченное на это, зависит от того, сколько левенштейновского расстояния вы можете терпеть. Для нулевого расстояния это просто O (n), где n - длина слова. Для произвольного расстояния это O (N), где N - количество слов в словаре.

Ответ 11

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

for (int i = 0; i < iWordLength; i++) {
    traverseTrie(theTrie.root, word.get(i), word, currentRow);
}

Вы должны называть traverseTrie только один раз, потому что внутри traverseTrie вы уже перебираете все слово. Код должен быть только следующим:

traverseTrie(theTrie.root, ' ', word, currentRow);