Как создать дерево на Python


125

Меня интересуют попытки и DAWG (прямой ациклический график слов), я много читал о них, но не понимаю, как должно выглядеть выходное дерево или файл DAWG.

  • Должно ли дерево быть объектом вложенных словарей? Где каждая буква делится на буквы и так далее?
  • Будет ли поиск, выполняемый в таком словаре, быстрым, если есть 100 000 или 500 000 записей?
  • Как реализовать блоки слов, состоящие более чем из одного слова, разделенных -пробелом или символом?
  • Как связать префикс или суффикс слова с другой частью структуры? (для DAWG)

Я хочу понять лучшую структуру вывода , чтобы понять, как ее создать и использовать.

Я также был бы признателен, каким должен быть результат DAWG вместе с trie .

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


5
Прочтите kmike.ru/python-data-structures, чтобы узнать об экзотических структурах данных в Python
полковник Паник

Ответы:


161

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

>>> _end = '_end_'
>>> 
>>> def make_trie(*words):
...     root = dict()
...     for word in words:
...         current_dict = root
...         for letter in word:
...             current_dict = current_dict.setdefault(letter, {})
...         current_dict[_end] = _end
...     return root
... 
>>> make_trie('foo', 'bar', 'baz', 'barz')
{'b': {'a': {'r': {'_end_': '_end_', 'z': {'_end_': '_end_'}}, 
             'z': {'_end_': '_end_'}}}, 
 'f': {'o': {'o': {'_end_': '_end_'}}}}

Если вы не знакомы с ним setdefault, он просто ищет ключ в словаре (здесь letterили _end). Если ключ присутствует, он возвращает связанное значение; если нет, он присваивает этому ключу значение по умолчанию и возвращает значение ( {}или _end). (Это похоже на версию, getкоторая также обновляет словарь.)

Затем функция для проверки, есть ли слово в дереве:

>>> def in_trie(trie, word):
...     current_dict = trie
...     for letter in word:
...         if letter not in current_dict:
...             return False
...         current_dict = current_dict[letter]
...     return _end in current_dict
... 
>>> in_trie(make_trie('foo', 'bar', 'baz', 'barz'), 'baz')
True
>>> in_trie(make_trie('foo', 'bar', 'baz', 'barz'), 'barz')
True
>>> in_trie(make_trie('foo', 'bar', 'baz', 'barz'), 'barzz')
False
>>> in_trie(make_trie('foo', 'bar', 'baz', 'barz'), 'bart')
False
>>> in_trie(make_trie('foo', 'bar', 'baz', 'barz'), 'ba')
False

Я оставлю вам вставку и удаление в качестве упражнения.

Конечно, предложение Unwind было бы не намного сложнее. Может быть небольшой недостаток скорости в том, что для поиска правильного подузла потребуется линейный поиск. Но поиск будет ограничен количеством возможных символов - 27, если мы включим _end. К тому же, как он предлагает, создание огромного списка узлов и доступ к ним по индексу ничего не даст; вы можете просто вложить списки.

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


1
Вот и изменения. Я бы придерживался dict.setdefault()(он недостаточно используется и малоизвестен), отчасти потому, что он помогает предотвратить ошибки, которые слишком легко создать с помощью defaultdict(где вы не получите KeyErrorдля несуществующих ключей при индексировании). Единственное, что сделало бы его пригодным для производственного кода, - это использовать _end = object():-)
Мартейн Питерс

@MartijnPieters хммм, я специально решил не использовать объект, но не могу вспомнить почему. Может быть, потому, что это будет сложно интерпретировать в демо-версии? Думаю, я мог бы создать конечный объект с настраиваемым представлением
senderle

27

Посмотри на это:

https://github.com/kmike/marisa-trie

Статические структуры Trie с эффективным использованием памяти для Python (2.x и 3.x).

Строковые данные в MARISA-trie могут занимать в 50-100 раз меньше памяти, чем в стандартном Python dict; скорость сырого поиска сопоставима; trie также предоставляет быстрые расширенные методы, такие как поиск по префиксу.

На основе библиотеки Marisa-trie C ++.

Вот сообщение в блоге компании, успешно использующей marisa trie:
https://www.repustate.com/blog/sharing-large-data-structure-across-processes-python/

В Repustate большая часть наших моделей данных, которые мы используем при анализе текста, могут быть представлены в виде простых пар ключ-значение или словарей на жаргоне Python. В нашем конкретном случае наши словари огромны, по несколько сотен МБ каждый, и к ним нужно постоянно обращаться. Фактически, для данного HTTP-запроса могут быть доступны 4 или 5 моделей, каждая из которых выполняет 20-30 поисков. Итак, проблема, с которой мы сталкиваемся, заключается в том, как сделать так, чтобы работа клиента была быстрой, а сервер - максимально легкой.

...

Я нашел этот пакет, marisa пытается, который представляет собой оболочку Python для реализации марисы trie на C ++. «Мариса» - это аббревиатура от «Алгоритм сопоставления с рекурсивно реализуемым StorAge». Что замечательно в Марисе Трис, так это то, что механизм хранения действительно сокращает объем памяти, который вам нужен. Автор плагина Python заявил об уменьшении размера в 50-100 раз - наш опыт аналогичен.

Что замечательно в пакете marisa trie, так это то, что базовая структура trie может быть записана на диск, а затем прочитана через объект, отображаемый в память. Благодаря отображению памяти marisa trie все наши требования теперь выполнены. Использование памяти нашим сервером резко снизилось, примерно на 40%, и наша производительность не изменилась по сравнению с тем, когда мы использовали реализацию словаря Python.

Существует также пара реализаций чистого python, хотя, если вы не находитесь на ограниченной платформе, вы бы хотели использовать реализацию с поддержкой C ++, описанную выше, для лучшей производительности:


последняя фиксация была в апреле 2018 года, последняя крупная фиксация была примерно в 2017 году
Борис

25

Вот список пакетов Python, реализующих Trie:

  • marisa-trie - реализация на C ++.
  • python-trie - простая реализация на чистом питоне.
  • PyTrie - более продвинутая реализация на чистом питоне .
  • pygtrie - чистая реализация Python от Google.
  • datrie - реализация дерева двойного массива на основе libdatrie .

18

Изменено senderleс помощью метода (см. Выше). Я обнаружил, что Python defaultdictидеально подходит для создания дерева префиксов или дерева префиксов.

from collections import defaultdict

class Trie:
    """
    Implement a trie with insert, search, and startsWith methods.
    """
    def __init__(self):
        self.root = defaultdict()

    # @param {string} word
    # @return {void}
    # Inserts a word into the trie.
    def insert(self, word):
        current = self.root
        for letter in word:
            current = current.setdefault(letter, {})
        current.setdefault("_end")

    # @param {string} word
    # @return {boolean}
    # Returns if the word is in the trie.
    def search(self, word):
        current = self.root
        for letter in word:
            if letter not in current:
                return False
            current = current[letter]
        if "_end" in current:
            return True
        return False

    # @param {string} prefix
    # @return {boolean}
    # Returns if there is any word in the trie
    # that starts with the given prefix.
    def startsWith(self, prefix):
        current = self.root
        for letter in prefix:
            if letter not in current:
                return False
            current = current[letter]
        return True

# Now test the class

test = Trie()
test.insert('helloworld')
test.insert('ilikeapple')
test.insert('helloz')

print test.search('hello')
print test.startsWith('hello')
print test.search('ilikeapple')

Я понимаю космическую сложность O (n * m). Некоторые обсуждают здесь. stackoverflow.com/questions/2718816/…
dapangmao

5
@dapangmao u использует defaultdict только для первого символа. Остальные символы по-прежнему используют обычный dict. Было бы лучше использовать вложенный defaultdict.
lionelmessi

3
На самом деле, похоже, что код не «использует» defaultdict для первого символа, поскольку он не устанавливает default_factory и все еще использует set_default.
Studgeek 06

12

Нет никакого «следует»; тебе решать. Различные реализации будут иметь разные характеристики производительности, для реализации, понимания и исправления потребуется разное количество времени. На мой взгляд, это типично для разработки программного обеспечения в целом.

Я бы, вероятно, сначала попробовал создать глобальный список всех созданных на данный момент узлов trie и представить дочерние указатели в каждом узле в виде списка индексов в глобальном списке. Мне кажется, что наличие словаря только для представления дочерних ссылок кажется слишком тяжелым.


2
Еще раз благодарю вас, однако я все же думаю, что ваш ответ требует более глубокого объяснения и уточнения, поскольку мой вопрос направлен на выяснение логики и структуры функциональности DAWG и TRIE. Ваш дальнейший вклад будет очень полезным и будет оценен по достоинству.
Фил

Если вы не используете объекты со слотами, пространство имен вашего экземпляра в любом случае будет словарями.
Безумный физик

4

Если вы хотите, чтобы TRIE был реализован как класс Python, вот что я написал после прочтения о них:

class Trie:

    def __init__(self):
        self.__final = False
        self.__nodes = {}

    def __repr__(self):
        return 'Trie<len={}, final={}>'.format(len(self), self.__final)

    def __getstate__(self):
        return self.__final, self.__nodes

    def __setstate__(self, state):
        self.__final, self.__nodes = state

    def __len__(self):
        return len(self.__nodes)

    def __bool__(self):
        return self.__final

    def __contains__(self, array):
        try:
            return self[array]
        except KeyError:
            return False

    def __iter__(self):
        yield self
        for node in self.__nodes.values():
            yield from node

    def __getitem__(self, array):
        return self.__get(array, False)

    def create(self, array):
        self.__get(array, True).__final = True

    def read(self):
        yield from self.__read([])

    def update(self, array):
        self[array].__final = True

    def delete(self, array):
        self[array].__final = False

    def prune(self):
        for key, value in tuple(self.__nodes.items()):
            if not value.prune():
                del self.__nodes[key]
        if not len(self):
            self.delete([])
        return self

    def __get(self, array, create):
        if array:
            head, *tail = array
            if create and head not in self.__nodes:
                self.__nodes[head] = Trie()
            return self.__nodes[head].__get(tail, create)
        return self

    def __read(self, name):
        if self.__final:
            yield name
        for key, value in self.__nodes.items():
            yield from value.__read(name + [key])

2
Спасибо @NoctisSkytower. Это здорово для начала, но я как бы отказался от Python и TRIES или DAWG из-за чрезвычайно высокого потребления памяти Python в этих сценариях.
Фил

3
Вот для чего нужен ____slots____. Это уменьшает объем памяти, используемой классом, когда у вас много его экземпляров.
dstromberg

4
from collections import defaultdict

Определите Trie:

_trie = lambda: defaultdict(_trie)

Создать Trie:

trie = _trie()
for s in ["cat", "bat", "rat", "cam"]:
    curr = trie
    for c in s:
        curr = curr[c]
    curr.setdefault("_end")

Уважать:

def word_exist(trie, word):
    curr = trie
    for w in word:
        if w not in curr:
            return False
        curr = curr[w]
    return '_end' in curr

Тест:

print(word_exist(trie, 'cam'))

1
внимание: это возвращается Trueтолько для всего слова, но не для префикса, для изменения префикса return '_end' in currнаreturn True
Shrikant Shete 01

3

В этой версии используется рекурсия

import pprint
from collections import deque

pp = pprint.PrettyPrinter(indent=4)

inp = raw_input("Enter a sentence to show as trie\n")
words = inp.split(" ")
trie = {}


def trie_recursion(trie_ds, word):
    try:
        letter = word.popleft()
        out = trie_recursion(trie_ds.get(letter, {}), word)
    except IndexError:
        # End of the word
        return {}

    # Dont update if letter already present
    if not trie_ds.has_key(letter):
        trie_ds[letter] = out

    return trie_ds

for word in words:
    # Go through each word
    trie = trie_recursion(trie, deque(word))

pprint.pprint(trie)

Вывод:

Coool👾 <algos>🚸  python trie.py
Enter a sentence to show as trie
foo bar baz fun
{
  'b': {
    'a': {
      'r': {},
      'z': {}
    }
  },
  'f': {
    'o': {
      'o': {}
    },
    'u': {
      'n': {}
    }
  }
}

0
class Trie:
    head = {}

    def add(self,word):

        cur = self.head
        for ch in word:
            if ch not in cur:
                cur[ch] = {}
            cur = cur[ch]
        cur['*'] = True

    def search(self,word):
        cur = self.head
        for ch in word:
            if ch not in cur:
                return False
            cur = cur[ch]

        if '*' in cur:
            return True
        else:
            return False
    def printf(self):
        print (self.head)

dictionary = Trie()
dictionary.add("hi")
#dictionary.add("hello")
#dictionary.add("eye")
#dictionary.add("hey")


print(dictionary.search("hi"))
print(dictionary.search("hello"))
print(dictionary.search("hel"))
print(dictionary.search("he"))
dictionary.printf()

Вне

True
False
False
False
{'h': {'i': {'*': True}}}

0

Класс Python для Trie


Структура данных Trie может использоваться для хранения данных, O(L)где L - длина строки, поэтому для вставки N строк временная сложность будет заключаться в O(NL)том, что строку можно искать вO(L) только для удаления.

Можно клонировать с https://github.com/Parikshit22/pytrie.git

class Node:
    def __init__(self):
        self.children = [None]*26
        self.isend = False
        
class trie:
    def __init__(self,):
        self.__root = Node()
        
    def __len__(self,):
        return len(self.search_byprefix(''))
    
    def __str__(self):
        ll =  self.search_byprefix('')
        string = ''
        for i in ll:
            string+=i
            string+='\n'
        return string
        
    def chartoint(self,character):
        return ord(character)-ord('a')
    
    def remove(self,string):
        ptr = self.__root
        length = len(string)
        for idx in range(length):
            i = self.chartoint(string[idx])
            if ptr.children[i] is not None:
                ptr = ptr.children[i]
            else:
                raise ValueError("Keyword doesn't exist in trie")
        if ptr.isend is not True:
            raise ValueError("Keyword doesn't exist in trie")
        ptr.isend = False
        return
    
    def insert(self,string):
        ptr = self.__root
        length = len(string)
        for idx in range(length):
            i = self.chartoint(string[idx])
            if ptr.children[i] is not None:
                ptr = ptr.children[i]
            else:
                ptr.children[i] = Node()
                ptr = ptr.children[i]
        ptr.isend = True
        
    def search(self,string):
        ptr = self.__root
        length = len(string)
        for idx in range(length):
            i = self.chartoint(string[idx])
            if ptr.children[i] is not None:
                ptr = ptr.children[i]
            else:
                return False
        if ptr.isend is not True:
            return False
        return True
    
    def __getall(self,ptr,key,key_list):
        if ptr is None:
            key_list.append(key)
            return
        if ptr.isend==True:
            key_list.append(key)
        for i in range(26):
            if ptr.children[i]  is not None:
                self.__getall(ptr.children[i],key+chr(ord('a')+i),key_list)
        
    def search_byprefix(self,key):
        ptr = self.__root
        key_list = []
        length = len(key)
        for idx in range(length):
            i = self.chartoint(key[idx])
            if ptr.children[i] is not None:
                ptr = ptr.children[i]
            else:
                return None
        
        self.__getall(ptr,key,key_list)
        return key_list
        

t = trie()
t.insert("shubham")
t.insert("shubhi")
t.insert("minhaj")
t.insert("parikshit")
t.insert("pari")
t.insert("shubh")
t.insert("minakshi")
print(t.search("minhaj"))
print(t.search("shubhk"))
print(t.search_byprefix('m'))
print(len(t))
print(t.remove("minhaj"))
print(t)

Код Oputpt

Правда
Ложь
[ 'Minakshi', 'Минхадж']
7
Minakshi
minhajsir
Pari
Парикшит
Shubh
Shubham
shubhi

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.