Як створити трійку в Python


125

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

  • Чи має бути триє об’єктом вкладених словників? Де кожна літера ділиться на букви тощо?
  • Чи буде пошук у такому словнику швидким, якщо є 100 к або 500 к записів?
  • Як реалізувати слова-блоки, що складаються з декількох слів, розділених -або пробілів?
  • Як пов’язати префікс або суфікс слова з іншою частиною структури? (для DAWG)

Я хочу зрозуміти найкращу структуру виводу , щоб зрозуміти, як її створити та використовувати.

Я також би вдячний, який повинен бути вихід DAWG разом з trie .

Я не хочу бачити графічні зображення з бульбашками, пов'язаними один з одним, я хочу знати вихідний об'єкт, як тільки набір слів перетворюється на спроби або DAWG.


5
Читайте kmike.ru/python-data-structures для опитування екзотичних структур даних в Python
полковник Паніка

Відповіді:


161

Розмотувати по суті правильно, що існує багато різних способів реалізації трійки; а для великої масштабованої трійки вкладені словники можуть стати громіздкими - або, принаймні, неефективними. Але оскільки ви тільки починаєте роботу, я думаю, що це найпростіший підхід; ви можете зашифрувати просте 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

Я вставлю і видаляю вас як вправу.

Звичайно, пропозиція розмотати не буде набагато складніше. Можливо, є невеликий недолік швидкості в тому, що для знаходження правильного підвузла потрібен буде лінійний пошук. Але пошук буде обмежений кількістю можливих символів - 27, якщо включити _end. Крім того, нічого не можна отримати, створивши масивний список вузлів та отримати доступ до них за індексом, як він пропонує; Ви також можете просто вкласти списки.

Насамкінець додам, що створення спрямованого ациклічного графіка слів (DAWG) було б трохи складніше, тому що ви повинні виявити ситуації, в яких ваше поточне слово поділяє суфікс з іншим словом у структурі. Насправді це може бути досить складним, залежно від того, як ви хочете структурувати DAWG! Можливо, вам доведеться дізнатись деякі відомості про відстань Левенштейна, щоб виправити це правильно.


1
Там зміни зроблені. Я б дотримувався dict.setdefault()(він недостатньо використовується і майже не досить відомий), частково тому, що це допомагає запобігти помилкам, які занадто легко створювати за допомогою defaultdict(де ви не отримали б KeyErrorдля неіснуючих ключів при індексації). Єдине, що зробило б це корисним для виробничого коду, це використання _end = object():-)
Martijn Pieters

@MartijnPieters hmmm, я спеціально вирішив не використовувати об'єкт, але не можу пригадати, чому. Можливо, тому, що було б важко інтерпретувати, коли його бачили в демонстрації? Я думаю, я міг би зробити кінцевий об'єкт за допомогою користувацької
repr

27

Погляньте на це:

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

Статичні структури Trie, ефективні для пам'яті для Python (2.x і 3.x).

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

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

Ось запис у блозі від компанії, яка успішно використовує marisa trie:
https://www.repustate.com/blog/sharing-large-data-structure-across-process-python/

У Repustate більшість наших моделей даних, які ми використовуємо в нашому аналізі тексту, можуть бути представлені як прості пари ключових значень або словники в Python lingo. У нашому конкретному випадку наші словники є масовими, по кілька сотень МБ кожен, і до них потрібно постійно користуватися. Насправді для даного запиту HTTP можна отримати доступ до 4 або 5 моделей, кожна з яких здійснює 20-30 пошукових запитів. Тому проблема, з якою ми стикаємося, полягає в тому, як ми зберігаємо речі швидко для клієнта, а також якнайбільше світла для сервера.

...

Я знайшов цей пакет, намагається marisa, який є обгорткою Python навколо C ++ реалізації маріса трие. «Маріса» - це абревіатура для відповідності алгоритму рекурсивно реалізованому StorAge. Що стосується спроб Маріса, це те, що механізм зберігання дійсно скорочується, скільки пам'яті вам потрібно. Автор плагіна Python стверджував, що розмір зменшився на 50-100X - наш досвід схожий.

Що найкраще в пакеті marisa trie - це те, що основну структуру трие можна записати на диск, а потім прочитати через об’єкт, відображений у пам'яті. З картографічною пам’яттю Марісі Тріє всі наші вимоги виконані. Використання пам'яті нашого сервера різко знизилося, приблизно на 40%, і наша продуктивність не змінилася в порівнянні з тим, коли ми використовували реалізацію словника Python.

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


остання фіксація відбулася у квітні 2018 року, остання велика
Борис

25

Ось список пакетів python, які реалізують Trie:

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

18

Змінено senderleметодом '(вище). Я виявив, що Python's 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 використовують типовий параметр лише для першого символу. Відпочиваючі символи все ще використовують звичайний малюнок. Було б краще використовувати вкладений вирок за замовчуванням.
lionelmessi

3
Насправді код, схоже, не "використовує" за замовчуванням для першого символу, оскільки він не встановлює default_factory і все ще використовує set_default.
studgeek

12

Немає "слід"; тобі вирішувати. Різні впровадження матимуть різні характеристики продуктивності, потребуватимуть різного часу, щоб реалізувати, зрозуміти та отримати правильний результат. На мою думку, це характерно для розробки програмного забезпечення в цілому.

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


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

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': {}
    }
  }
}

3
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

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

Клас Пітона для Три


Структура даних 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)

Вихід коду

Правда
Брехня
[ 'Minakshi', 'Мінхадж']
7
Minakshi
minhajsir
Pari
Парікшит
Shubh
Shubham
shubhi

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.