Реализация простого Trie для эффективного вычисления расстояния Левенштейна - Java - PullRequest
37 голосов
/ 02 февраля 2011

ОБНОВЛЕНИЕ 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 и пытался следовать учебному пособию Стива Ханова по питону для вычисления расстояния Левенштейна. На самом деле, мне интересно вычислить минимум расстояние Левенштейна между данным словом и словами в три, поэтому я следовал версии Стива Ханова, предложенной Мурило Васконселосом алгоритм . Это не очень хорошо работает, но вот мой класс Три:

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

Теперь я попытался выполнить поиск, так как Murilo Vasconcelos имеет его, но что-то не так, и мне нужна помощь в его отладке. Пожалуйста, дайте предложения о том, как реорганизовать это и / или укажите, где ошибки. Самое первое, что я хотел бы реорганизовать, - это глобальная переменная 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, как предложено в блоге Ника Джонсона , но это не так эффективно, как я думаю, что это может быть ... или, возможно, моя реализация неверна.

Ответы [ 11 ]

0 голосов
/ 02 февраля 2011

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

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

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

...