Реализация Patricia Trie для использования в качестве словаря

Я пытаюсь реализовать Patricia Trie с методами addWord(), isWord() и isPrefix() в качестве средства хранения большого словаря слов для быстрого поиска (включая поиск по префиксу). Я прочитал концепции, но они просто не проясняют реализацию. Я хочу знать (в коде Java или Python), как реализовать Trie, особенно узлы (или мне следует реализовать его рекурсивно). Я видел одного человека, который реализовал его с массивом из 26 дочерних узлов, для которых установлено значение null/None. Есть ли лучшая стратегия (например, обработка букв как битов) и как бы вы ее реализовали?


person Regis Frey    schedule 09.03.2010    source источник
comment
Это домашнее задание?   -  person Justin Peel    schedule 09.03.2010
comment
Он основан на домашнем задании, но объем этого вопроса составляет лишь часть этого задания и (особенно сейчас) больше относится к моему личному пониманию того, как работают попытки, чем к получению оценки.   -  person Regis Frey    schedule 09.03.2010
comment
Я просто хочу сказать, что однажды я тоже хотел исследовать попытки Патриции, но в итоге сдался. Я нашел изложение Кнута слишком кратким, а исходная статья не имела для меня никакого смысла.   -  person Jonathan Feinberg    schedule 09.03.2010
comment
Попробовав это в JavaScript, я думаю, что реалистичной реализацией Python на самом деле было бы расширение C. Тот же API для отсортированного массива был намного быстрее, чем trie в JavaScript, и я ожидаю, что то же самое будет справедливо и для Python.   -  person Steven Huwig    schedule 10.03.2010


Ответы (2)


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

Я начну с корневого узла. Корень - это просто словарь. В словаре есть ключи, состоящие из отдельных символов (первых букв слов), ведущих к ветвям. Значения, соответствующие каждому ключу, представляют собой списки, где первый элемент представляет собой строку, которая дает остальную часть строки, совпадающую с этой ветвью дерева, а второй элемент — это словарь, ведущий к дальнейшим ветвям от этого узла. Этот словарь также имеет односимвольные ключи, соответствующие первой букве остального слова, и процесс продолжается вниз по дереву.

Еще одна вещь, которую я должен упомянуть, это то, что если данный узел имеет ветви, но также является словом в самом дереве, то это обозначается наличием ключа '' в словаре, который ведет к узлу со списком ['',{}].

Вот небольшой пример, показывающий, как хранятся слова (корневой узел — это переменная _d):

>>> x = patricia()
>>> x.addWord('abcabc')
>>> x._d
{'a': ['bcabc', {}]}
>>> x.addWord('abcdef')
>>> x._d
{'a': ['bc', {'a': ['bc', {}], 'd': ['ef', {}]}]}
>>> x.addWord('abc')
{'a': ['bc', {'a': ['bc', {}], '': ['', {}], 'd': ['ef', {}]}]}

Обратите внимание, что в последнем случае в словарь был добавлен ключ '', чтобы обозначить, что 'abc' является словом в дополнение к 'abcdef' и 'abcabc'.

Исходный код

class patricia():
    def __init__(self):
        self._data = {}

    def addWord(self, word):
        data = self._data
        i = 0
        while 1:
            try:
                node = data[word[i:i+1]]
            except KeyError:
                if data:
                    data[word[i:i+1]] = [word[i+1:],{}]
                else:
                    if word[i:i+1] == '':
                        return
                    else:
                        if i != 0:
                            data[''] = ['',{}]
                        data[word[i:i+1]] = [word[i+1:],{}]
                return

            i += 1
            if word.startswith(node[0],i):
                if len(word[i:]) == len(node[0]):
                    if node[1]:
                        try:
                            node[1]['']
                        except KeyError:
                            data = node[1]
                            data[''] = ['',{}]
                    return
                else:
                    i += len(node[0])
                    data = node[1]
            else:
                ii = i
                j = 0
                while ii != len(word) and j != len(node[0]) and \
                      word[ii:ii+1] == node[0][j:j+1]:
                    ii += 1
                    j += 1
                tmpdata = {}
                tmpdata[node[0][j:j+1]] = [node[0][j+1:],node[1]]
                tmpdata[word[ii:ii+1]] = [word[ii+1:],{}]
                data[word[i-1:i]] = [node[0][:j],tmpdata]
                return

    def isWord(self,word):
        data = self._data
        i = 0
        while 1:
            try:
                node = data[word[i:i+1]]
            except KeyError:
                return False
            i += 1
            if word.startswith(node[0],i):
                if len(word[i:]) == len(node[0]):
                    if node[1]:
                        try:
                            node[1]['']
                        except KeyError:
                            return False
                    return True
                else:
                    i += len(node[0])
                    data = node[1]
            else:
                return False

    def isPrefix(self,word):
        data = self._data
        i = 0
        wordlen = len(word)
        while 1:
            try:
                node = data[word[i:i+1]]
            except KeyError:
                return False
            i += 1
            if word.startswith(node[0][:wordlen-i],i):
                if wordlen - i > len(node[0]):
                    i += len(node[0])
                    data = node[1]
                else:
                    return True
            else:
                return False

    def removeWord(self,word):
        data = self._data
        i = 0
        while 1:
            try:
                node = data[word[i:i+1]]
            except KeyError:
                print "Word is not in trie."
                return
            i += 1
            if word.startswith(node[0],i):
                if len(word[i:]) == len(node[0]):
                    if node[1]:
                        try:
                            node[1]['']
                            node[1].pop('')
                        except KeyError:
                            print "Word is not in trie."
                        return
                    data.pop(word[i-1:i])
                    return
                else:
                    i += len(node[0])
                    data = node[1]
            else:
                print "Word is not in trie."
                return


    __getitem__ = isWord

Вы могли заметить, что в конце я установил __getitem__ для метода isWord. Это означает, что

x['abc']

вернет ли 'abc' в дереве или нет.

Я думаю, что, возможно, мне следует сделать из этого модуль и отправить его в PyPI, но для этого нужно больше тестирования и, по крайней мере, метод removeWord. Если вы обнаружите какие-либо ошибки, дайте мне знать, но, похоже, это работает довольно хорошо. Кроме того, если вы видите какие-либо большие улучшения в эффективности, я также хотел бы услышать о них. Я думал сделать что-нибудь с пустыми словарями внизу каждой ветки, но пока оставляю это. Эти пустые словари могут быть заменены данными, связанными со словом, например, для расширения использования реализации.

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

person Justin Peel    schedule 09.03.2010
comment
@John Да, но я пометил их таким образом, чтобы другим было легче понять код. Я мог бы внести такие изменения в финальную версию. Спасибо за отзыв. - person Justin Peel; 10.03.2010
comment
@Justin: Извините, в спешке я пропустил один: wlen -› l - person John Machin; 10.03.2010
comment
@John Да, все это хорошо для уменьшения размера файла .py (что на самом деле не было моей целью на данный момент), но как это действительно помогает эффективности? - person Justin Peel; 10.03.2010
comment
@Justin: Пожалуйста, проверьте свой детектор иронии; он, кажется, работает со сбоями. - person John Machin; 10.03.2010
comment
@John Irony плохо передает текст. Вы могли бы быть кем-то, кто действительно пытается быть полезным, как и большинство людей здесь, и я стараюсь отвечать вежливо, даже если это не очень хороший комментарий / идея. Пожалуйста, не тратьте мое время на такие глупости. - person Justin Peel; 10.03.2010
comment
@Justin, см. stackoverflow.com/ вопросов/3121916/ - в этом вашем коде есть ошибка, с которой столкнулся ОП; Я считаю, что нашел ошибку и разместил ее в ответе там - вы можете проверить мое исправление и либо указать правильное, либо подтвердить, что мое правильно (и в любом случае исправить код в этом ответе тоже!-) -- Благодарность. - person Alex Martelli; 26.06.2010
comment
@ Алекс, хорошо подмечено. Я вставил исправление ошибки. Я не в том месте, где я мог бы действительно легко проверить это в данный момент, так как моя материнская плата сгорела на этой неделе (в данный момент я использую материнскую плату моей жены и стараюсь не загромождать ее компьютер, пока я ищу замену), но это явно проблема. Я даже не знаю, действительно ли это достойная реализация patricia trie, но она проста и, по крайней мере, основана на patricia trie. - person Justin Peel; 26.06.2010
comment
@John Использование коротких имен переменных вообще не оптимизирует код с точки зрения обработки, а использование плохих имен переменных, таких как «x» и «y», является просто артефактом ленивого программирования. Это никак не помогает и делает код менее читаемым. - person monokrome; 30.03.2011
comment
@monokrome: см. иронию выше. - person John Machin; 30.03.2011
comment
@John: Скажи что-нибудь интересное или полезное. - person monokrome; 05.04.2011

Вот рекурсивная реализация с использованием более pythonic методов:

def matching_prefix_index(word1, word2):
    max_len = min(len(word1),len(word2))
    for i in range(max_len):
        if word2[i] != word1[i]:
            return i
    return max_len

class PatriciaTrie(object):
    def __init__(self):
        self._storage = {}
        self._complete_prefix_flag = False

    def _find_storage_key(self, word):
        for key in self._storage:
            prefix_index = matching_prefix_index(key, word)
            if prefix_index > 0:
                return (key, prefix_index)
        return (None, None)

    def add(self, word):
        if word == '':
            self._complete_prefix_flag = True
            return True

        key, prefix_index = self._find_storage_key(word)
        if key is not None:
            if prefix_index == len(key):
                return self._storage[key].add(word[len(key):])
            else:
                new_tree = PatriciaTrie()
                new_tree._storage[key[prefix_index:]] = self._storage.pop(key)
                self._storage[key[0:prefix_index]] = new_tree
                return new_tree.add(word[prefix_index:])
        else:
            self._storage[word] = PatriciaTrie()
            self._storage[word].add('')
            return True

    def remove(self, word):
        if word == '':
            self._complete_prefix_flag = False
            return True

        key, prefix_index = self._find_storage_key(word)
        if key is None or prefix_index != len(key):
            return False

        subword = word[prefix_index:]
        subtrie = self._storage[key]
        if subtrie.remove(subword):
            if (not subtrie._complete_prefix_flag) and len(subtrie._storage) == 0:
                self._storage.pop(key)
            return True
        else:
            return False

    def __contains__(self, word):
        if word == '':
            return self._complete_prefix_flag

        key, prefix_index = self._find_storage_key(word)
        if key is None or prefix_index != len(key):
            return False
        else:
            return (word[prefix_index:] in self._storage[key])

    def has_prefix(self, word):
        if word == '':
            return True

        key, prefix_index = self._find_storage_key(word)
        if key is None:
            return False
        elif len(key) > len(word):
            return (prefix_index == len(word))
        elif len(key) != prefix_index:
            return False
        else:
            return self._storage[key].has_prefix(word[prefix_index:])
person Jeff Edwards    schedule 16.04.2013