파이썬에서 트라이를 만드는 방법


125

시도와 DAWG (직접 비순환 단어 그래프)에 관심이 있고 이에 대해 많이 읽었지만 출력 trie 또는 DAWG 파일이 어떻게 생겼는지 이해하지 못합니다.

  • 트라이는 중첩 된 사전의 객체 여야합니까? 각 문자가 문자 등으로 나누어지는 곳은 어디입니까?
  • 100k 또는 500k 항목이있는 경우 이러한 사전에서 수행되는 조회가 빠를까요?
  • -또는 공백으로 구분 된 둘 이상의 단어로 구성된 단어 블록을 구현하는 방법은 무엇입니까?
  • 단어의 접두사 또는 접미사를 구조의 다른 부분에 연결하는 방법은 무엇입니까? (DAWG 용)

생성 및 사용 방법을 파악하기 위해 최상의 출력 구조 를 이해하고 싶습니다 .

또한 trie 와 함께 DAWG출력이 무엇인지 감사하겠습니다 .

서로 연결된 거품이있는 그래픽 표현을보고 싶지 않습니다. 일단 단어 집합이 시도 또는 DAWG로 바뀌면 출력 개체를 알고 싶습니다.


5
읽기 kmike.ru/python-data-structures 파이썬에서 이국적인 데이터 구조의 조사를 위해
대령 패닉

답변:


161

Unwind 는 trie를 구현하는 여러 가지 방법이 있다는 점에서 본질적으로 정확합니다. 확장 가능한 대규모 트라이의 경우 중첩 된 사전이 번거 롭거나 최소한 공간 비효율적 일 수 있습니다. 하지만 이제 막 시작했기 때문에 이것이 가장 쉬운 접근 방법이라고 생각합니다. 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의 제안은 그다지 어렵지 않을 것입니다. 올바른 하위 노드를 찾는 데 선형 검색이 필요하다는 점에서 약간의 속도 단점이있을 수 있습니다. 그러나 검색은 가능한 문자 수로 제한됩니다 _end. 또한 그가 제안한대로 방대한 노드 목록을 만들고 인덱스로 액세스하여 얻을 수있는 것은 없습니다. 목록을 중첩하는 것이 좋습니다.

마지막으로, 현재 단어가 구조에서 다른 단어와 접미사를 공유하는 상황을 감지해야하기 때문에 DAWG (directed acyclic word graph)를 만드는 것이 조금 더 복잡 할 것이라고 덧붙일 것입니다. 사실, 이것은 DAWG를 어떻게 구성하고 싶은지에 따라 다소 복잡해질 수 있습니다! Levenshtein 거리 에 대해 제대로 알아 보려면 몇 가지를 배워야 할 수도 있습니다 .


1
거기에 변화가 생겼습니다. 나는 스틱 것 dict.setdefault(), 부분적으로 너무 쉽게로 만들 수있는 버그를 방지 할 수 있기 때문에 (이 충분히 활용하고 거의 충분히 잘 알려진 아니에요) defaultdict(당신이 얻을하지 않는 게 좋을 경우 KeyError인덱스에 존재하지 않는 키에 대한)를. 이제 프로덕션 코드에 사용할 수있게 만드는 유일한 방법은 다음을 사용하는 것입니다. _end = object():-)
Martijn Pieters

@MartijnPieters hmmm, 저는 특별히 객체를 사용하지 않기로 선택했지만 그 이유를 기억할 수 없습니다. 데모에서 볼 때 해석하기 어렵 기 때문일까요? 사용자 지정 repr로 최종 개체를 만들 수있을 것 같습니다
senderle

27

이것 좀보세요 :

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

Python (2.x 및 3.x)을위한 정적 메모리 효율적인 Trie 구조.

MARISA-trie의 문자열 데이터는 표준 Python dict보다 최대 50x-100x 적은 메모리를 차지할 수 있습니다. 원시 조회 속도는 비슷합니다. trie는 또한 접두사 검색과 같은 빠른 고급 방법을 제공합니다.

marisa-trie C ++ 라이브러리를 기반으로합니다.

다음은 marisa trie를 성공적으로 사용하는 회사의 블로그 게시물입니다.
https://www.repustate.com/blog/sharing-large-data-structure-across-processes-python/

Repustate에서 텍스트 분석에 사용하는 대부분의 데이터 모델은 간단한 키-값 쌍 또는 Python 용어의 사전으로 표현 될 수 있습니다. 우리의 특별한 경우, 우리 사전은 각각 수백 MB의 방대하고 지속적으로 액세스해야합니다. 실제로 주어진 HTTP 요청에 대해 4 개 또는 5 개의 모델에 액세스 할 수 있으며 각각 20-30 개의 조회를 수행합니다. 따라서 우리가 직면 한 문제는 클라이언트를 위해 일을 빠르게 유지하고 서버를 위해 가능한 한 가볍게 유지하는 방법입니다.

...

marisa trie의 C ++ 구현에 대한 Python 래퍼 인 marisa tries이 패키지를 찾았습니다. "Marisa"는 재귀 적으로 구현 된 StorAge를 사용한 매칭 알고리즘의 약자입니다. marisa 시도의 장점은 저장 메커니즘이 실제로 필요한 메모리 양을 줄인다는 것입니다. Python 플러그인 작성자는 크기가 50-100 배 감소했다고 주장했습니다. 우리의 경험은 비슷합니다.

marisa trie 패키지의 장점은 기본 trie 구조를 디스크에 기록한 다음 메모리 매핑 된 객체를 통해 읽을 수 있다는 것입니다. 메모리 매핑 마리사 트라이를 사용하면 이제 모든 요구 사항이 충족됩니다. 우리 서버의 메모리 사용량은 약 40 %까지 급격히 감소했으며 성능은 Python의 사전 구현을 사용했을 때와 변함이 없었습니다.

또한 몇 가지 순수한 Python 구현이 있지만 제한된 플랫폼에 있지 않는 한 최상의 성능을 위해 위의 C ++ 기반 구현을 사용하고 싶을 것입니다.


마지막 커밋은 2018 년 4 월, 마지막 메이저 커밋은 2017 년과 같았습니다
Boris

25

다음은 Trie를 구현하는 파이썬 패키지 목록입니다.


18

senderle의 방법 (위) 에서 수정되었습니다 . 파이썬 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를 사용하고 있습니다. 나머지 문자는 여전히 일반 사전을 사용합니다. 중첩 된 defaultdict를 사용하는 것이 좋습니다.
lionelmessi

3
실제로 코드는 default_factory를 설정하지 않고 여전히 set_default를 사용하고 있기 때문에 첫 번째 문자에 대해 defaultdict를 "사용"하지 않는 것 같습니다.
studgeek

12

"해야 할 것"은 없습니다. 그것은 당신에게 달려 있습니다. 다양한 구현은 서로 다른 성능 특성을 가지며 구현, 이해 및 올바르게 작동하는 데 다양한 시간이 걸립니다. 제 생각에는 이것은 소프트웨어 개발 전체에서 일반적입니다.

나는 먼저 지금까지 생성 된 모든 트라이 노드의 글로벌 목록을 만들고 각 노드의 자식 포인터를 글로벌 목록에 대한 인덱스 목록으로 나타내려고 할 것입니다. 연결하는 아이를 나타내는 사전을 갖는 것은 나에게 너무 무겁게 느껴집니다.


2
다시 한 번 감사드립니다. 그러나 제 질문은 DAWG 및 TRIE의 기능의 논리와 구조를 파악하기위한 것이기 때문에 귀하의 답변에 좀 더 깊은 설명과 설명이 필요하다고 생각합니다. 귀하의 추가 입력은 매우 유용하고 감사 할 것입니다.
Phil

슬롯이있는 객체를 사용하지 않는 한 인스턴스 네임 스페이스는 어쨌든 사전이됩니다.
Mad Physicist

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의 매우 높은 메모리 소비로 인해 Python 및 TRIES 또는 DAWG를 포기했습니다.

3
그것이 ____slots____의 목적입니다. 인스턴스가 많을 때 클래스에서 사용하는 메모리 양을 줄입니다.
dstromberg 2014 년

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 = lambda: defaultdict(_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 currreturn True
Shrikant Shete

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 용 Python 클래스


Trie Data Structure는 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', 'minhaj']
7
minakshi
minhajsir
pari
parikshit
shubh
shubham
shubhi

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.