Реализация простого Trie для эффективного вычисления расстояния Левенштейна - 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's blog article "Fast and Easy Levenshtein
 * distance using a Trie" and Murilo Vasconcelo's 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, поэтому я следил за Версия алгоритма Стива Ханова Мурило Васконселос < / а>. Это не очень хорошо работает, но вот мой класс 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 ... так что мне есть чему поучиться на этом опыте.

Спасибо.

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


person Hristo    schedule 01.02.2011    source источник
comment
@Hristo - Вы упомянули блог Ника Джонсона, так что, возможно, вы уже видели его код автоматов Левенштейна. Код автоматов Левенштейна - самый эффективный из тех, что я когда-либо встречал. Вам просто нужно преобразовать его версию Python в Java. См. Это: blog.notdot.net/2010/07/ < / а>   -  person Taylor Leese    schedule 02.02.2011
comment
@Hristo Я могу думать, что Trie вам поможет, только если вы в любом случае собираетесь реализовать то же самое, что и автоматы Левенштейна. Trie - это просто частный случай DFA, который распознает слова в нем.   -  person Nick Johnson    schedule 02.02.2011
comment
if (currentRow[size - 1] < minCost && !node.isWord) { эта строка неверна. Вы можете обновить minCost, только если есть слово, заканчивающееся на этом узле, поэтому правильным является if (currentRow[size - 1] < minCost && node.isWord) {   -  person Murilo Vasconcelos    schedule 02.02.2011
comment
@Murilo ... это изменение приводит к StackOverflowError, я полагаю, из-за слишком большой рекурсии. В вашей версии C ++ у вас есть _2 _... что именно означает вторая часть этого? Что из себя представляет?   -  person Hristo    schedule 02.02.2011
comment
tree->word == "" означает, что в этом узле нет конца слова. Поэтому, если текущая стоимость меньше min_cost и одно или несколько слов заканчиваются на этом узле, мы должны обновить min_cost, указав текущую стоимость.   -  person Murilo Vasconcelos    schedule 03.02.2011
comment
StackOverflowError может быть потому, что у вас очень большие слова. Вы знаете, какова максимальная длина ваших слов? Кроме того, вы можете попробовать запустить мой код с вашими данными и посмотреть, произойдет ли такая же ошибка.   -  person Murilo Vasconcelos    schedule 03.02.2011
comment
@Murilo ... в словаре, который я использую, ~ 180 тыс. Слов, а максимальная длина слова в этом словаре - 15 символов. Но ввод может быть дольше, хотя это не гарантируется.   -  person Hristo    schedule 03.02.2011
comment
Итак, StackOverflowError не из-за рекурсии ... Максимальная глубина рекурсии - 15, что мало.   -  person Murilo Vasconcelos    schedule 04.02.2011


Ответы (11)


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

Изменить: я разместил его на своем блог.

person Murilo Vasconcelos    schedule 02.02.2011
comment
Да, я знаю C ++ намного лучше, чем Python. Если не будет куча хитрых, низкоуровневых вещей ... Думаю, со мной все будет в порядке. Вы можете вставить сюда или загрузить файл на gist.github.com. - person Hristo; 02.02.2011
comment
@Murilo ... Я обновил свой пост попыткой реализовать ваш алгоритм на Java. Не могли бы вы взглянуть, нет ли очевидных ошибок? Я немного застрял. - person Hristo; 02.02.2011
comment
В порядке. Также этот подход к поиску минимальной стоимости взят из моего блога, который немного отличается от подхода Стива Ханова. Буду рад, если вы процитируете :) - person Murilo Vasconcelos; 02.02.2011

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

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

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

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

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

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

Биссектрисы

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

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

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

Дерево точек обзора

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

person Robert    schedule 02.02.2011
comment
Очень хорошо составленный ответ ... +1 ... спасибо! Вы совершенно правы в том, что я ищу способ избавиться от стольких вычислений расстояния Левенштейна. Я изучу эти деревья и постараюсь понять их преимущества / недостатки. Однако это займет у меня время. А пока ... есть ли у вас какие-либо предложения или другие комментарии о преимуществах использования одного над другим? - person Hristo; 02.02.2011
comment
к сожалению, вы просите на пару месяцев раньше срока - проект My Honors именно такой, а я еще не закончил! - person Robert; 02.02.2011
comment
Есть ли шанс получить ссылку на / PDF вашего отличного проекта, который вы упомянули в комментариях? / cc @Hristo - person Regexident; 10.05.2013
comment
Хорошо, если вы действительно этого хотите ... @Regexident robertgmoss.co.uk/hons_project_report.pdf - person Robert; 11.05.2013

Вот пример Автоматы Левенштейна в Java (РЕДАКТИРОВАТЬ: перемещено в 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/trunk/lucene/src/test/org/apache/lucene/util/automaton/

РЕДАКТИРОВАТЬ: приведенные выше ссылки, похоже, переместились в 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/master/lucene/core/src/test/org/apache/lucene/util/automaton

Похоже, экспериментальный код 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");
person Taylor Leese    schedule 02.02.2011
comment
хахаха ... да! Мне было интересно, что такое Lev1ParametricDescription и Lev2ParametricDescription ... и другие классы - person Hristo; 02.02.2011
comment
Взгляните на javaodcs для пакета dk.brics.automaton. Я считаю, что Lucene только что включила этот пакет, поэтому вы можете использовать его напрямую. - person Taylor Leese; 02.02.2011
comment
аааа ... приятно! Все это выглядит великолепно. Спасибо! Мне нужно время, чтобы все это прочитать ... это выглядит очень сложно. Но об этом должно быть интересно узнать! - person Hristo; 02.02.2011
comment
@Taylor ... Мне сложно включить источник Lucene / .jar в Eclipse. Похоже, что загрузка версии 3.0.3 не включает в себя автомат. Какие-либо предложения? - person Hristo; 02.02.2011
comment
@Taylor ... пакет dk.brics.automaton не включает автоматы Левенштейна. Ссылки, которые вы отправили мне на apache, имеют источники, отличные от источников dk.brics.automaton - person Hristo; 02.02.2011
comment
+1 - материал Lucene был одним из ресурсов, с которыми я столкнулся при написании статьи. - person Nick Johnson; 02.02.2011
comment
@Nick ... Привет ... Я пытаюсь импортировать весь материал Lucene в Eclipse, но он не работает. Я загрузил исходный код Lucene, но в нем отсутствуют автоматы Левенштейна. Я загрузил dk.brics.automata, но он не сливается с материалом Lucene. Какие-либо предложения? - person Hristo; 02.02.2011
comment
@ Приятно ... классные статьи в блоге о BK-Tree / автоматах Левенштейна, кстати :) - person Hristo; 02.02.2011

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

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

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

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

person Kevin    schedule 02.07.2016

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

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

person Darius Bacon    schedule 02.02.2011

Насколько я понимаю, вы хотите перебрать все ветви дерева. Это не так уж и сложно с использованием рекурсивной функции. Я также использую дерево в моем алгоритме 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

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

person Folgert    schedule 03.02.2011
comment
Спасибо за ответ. У меня есть пара вопросов ... Вы можете объяснить параметры каждой функции? Что они собой представляют? - person Hristo; 04.02.2011

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

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

Или в Лиспе ...

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

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

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

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

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

person Folgert    schedule 04.02.2011
comment
Я уже реализовал Trie ... Я просто не понимал, что такое тестовый элемент и начальное расстояние. - person Hristo; 04.02.2011

Я просто оставлю это здесь на тот случай, если кто-то ищет еще одно решение этой проблемы:

http://code.google.com/p/oracleofwoodyallen/wiki/ApproximateStringMatching

person spieden    schedule 24.03.2012

Я смотрел ваше последнее обновление 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;
        }

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

person zdlgrj    schedule 13.11.2014

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

Сделайте дерево максимально простым. Не набивайте в него струны.

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

person Mike Dunlavey    schedule 02.02.2011
comment
@ Майк ... спасибо за совет. Я обновил свой пост реализацией Trie на Java. Я также включил метод поиска, который использую для вычисления минимального расстояния Левенштейна между заданным словом и словами в Trie. Это работает не совсем правильно ... не могли бы вы взглянуть, можете ли вы отловить какие-либо очевидные ошибки? Я немного застрял. - person Hristo; 02.02.2011
comment
@Hristo: Я думаю, ты делаешь это немного иначе, чем я. В моем подходе матрица со строками не использовалась. Основная форма программы - это функция обхода дерева сначала в глубину, которая затем оформляется добавлением аргументов. Один аргумент - это оставшийся бюджет ошибок. По убыванию, если символ trie не соответствует ключевому символу, бюджет уменьшается на 1 для подчиненного вызова. То же самое и для других видов несоответствия. Затем процедура отсекает все вызовы с бюджетом <0. Затем существует внешний цикл, который выполняет обход несколько раз ... - person Mike Dunlavey; 03.02.2011
comment
@Hristo: ... сначала с бюджетом 0, затем с бюджетом 1 и т. Д. Каждый раз, когда прогулка достигает совпадения, она добавляет совпадение в список результатов. Внешний цикл может остановиться, как только в списке появятся совпадения. Поскольку время, затрачиваемое на небольшой бюджет B, экспоненциально зависит от B, не помешает совершить дополнительные прогулки с меньшим B. Таким образом, первые совпадения, которые вы получите, будут самыми дешевыми. - person Mike Dunlavey; 03.02.2011
comment
@ Майк ... спасибо за ответ. Допустим, я пытаюсь вычислить минимальное расстояние Левенштейна между словом tihs и Trie. Сможет ли ваш алгоритм вернуть значение 1, например, расстояние Левенштейна, равное 1, между tihs и связями? - person Hristo; 03.02.2011

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

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

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

traverseTrie(theTrie.root, ' ', word, currentRow);
person user4980248    schedule 06.06.2015