Pythonでトライを作成する方法


124

私はトライとDAWG(直接非循環ワードグラフ)に興味があり、それらについてたくさん読んでいますが、出力トライまたはDAWGファイルがどのように見えるべきか理解できません。

  • トライはネストされた辞書のオブジェクトである必要がありますか?各文字が文字などに分割される場所はどこですか?
  • 10万件または50万件のエントリがある場合、そのような辞書で実行される検索は高速でしょうか?
  • -またはスペースで区切られた複数の単語で構成される単語ブロックを実装するにはどうすればよいですか?
  • 単語のプレフィックスまたはサフィックスを構造内の別の部分にリンクする方法は?(DAWG用)

作成方法と使用方法を理解するために、最適な出力構造を理解したいと思います。

また、トライと一緒にDAWG出力がどうあるべきかを評価します。

バブルが相互にリンクされたグラフィック表現を表示したくありません。一連の単語が試行またはDAWGに変換された後の出力オブジェクトを知りたいです。


5
Pythonのエキゾチックなデータ構造の調査についてはkmike.ru/python-data-structuresをお読みください
パニック大佐

回答:


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

挿入と取り外しは演習としてお任せします。

もちろん、Unwindの提案はそれほど難しくありません。正しいサブノードを見つけるには線形検索が必要になるという点で、速度がわずかに不利になる場合があります。しかし、検索は可能な文字数に限定されます-含めると27文字になります_end。また、ノードの大規模なリストを作成して、インデックスでアクセスすることで得られるものは何もありません。リストをネストするだけでもよいでしょう。

最後に、現在の単語が構造内の別の単語と接尾辞を共有する状況を検出する必要があるため、有向非巡回単語グラフ(DAWG)の作成は少し複雑になることを付け加えます。実際、これは、DAWGをどのように構成するかによって、かなり複雑になる可能性があります。あなたはそれを正しくするためにレーベンシュタイン 距離についていくつかのことを学ぶ必要があるかもしれません。


1
そこで、変更が行われました。私はdict.setdefault()(十分に活用されておらず、十分に知られていない)に固執します。これは、defaultdictKeyErrorインデックス化で存在しないキーに対してが得られない)で作成するのが簡単すぎるバグを防ぐのに役立つためです。本番用コードで使用できるようになる唯一のことは、次のようにすることです_end = object():-)
Martijn Pieters

@MartijnPietersうーん、具体的にはオブジェクトを使用しないことにしましたが、その理由を思い出せません。おそらく、デモで見たときに解釈するのが難しいためでしょうか?カスタムreprを使用して最終オブジェクトを作成できると思います
sendle '23

27

これを見てください:

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

Python(2.xおよび3.x)の静的メモリ効率の高いTrie構造。

MARISA-trieの文字列データは、標準のPython dictの場合よりも最大50倍から100倍少ないメモリを使用する可能性があります。生の検索速度は同等です。トライはまた、プレフィックス検索のような高速で高度な方法を提供します。

marisa-trie C ++ライブラリに基づいています。

マリサトライをうまく使用している会社からのブログ投稿は次のとおりです。https://www.repustate.com/blog/sharing-large-data-structure-across-processes-python/

Repustateでは、テキスト分析で使用するデータモデルの多くは、単純なキーと値のペア、またはPython専門用語の辞書として表すことができます。私たちの特定のケースでは、私たちの辞書は巨大で、それぞれ数百MBであり、それらは常にアクセスする必要があります。実際、特定のHTTPリクエストに対して、4つまたは5つのモデルがアクセスされ、それぞれが20〜30のルックアップを実行します。したがって、私たちが直面している問題は、クライアントにとっては高速で、サーバーにとっては可能な限り軽量であるようにする方法です。

...

私はこのパッケージを見つけました、これはmarisaトライのC ++実装のPythonラッパーです。「Marisa」は、再帰的に実装されたStorAgeを使用したマッチングアルゴリズムの頭字語です。marisaの優れた点は、ストレージメカニズムによって、必要なメモリの量が実際に減少することです。Pythonプラグインの作成者は、サイズが50〜100倍に減少したと主張しています–私たちの経験も同様です。

marisaトライパッケージの優れている点は、基になるトライ構造がディスクに書き込まれ、メモリマップされたオブジェクトを介して読み取られることです。メモリマップされたマリサトライで、すべての要件が満たされました。サーバーのメモリ使用量は劇的に減少し、約40%減少しました。また、Pythonのディクショナリ実装を使用したときと比べてパフォーマンスは変わりませんでした。

いくつかの純粋なPythonの実装もありますが、制限されたプラットフォームを使用しているのでない限り、最高のパフォーマンスを得るために上記のC ++ベースの実装を使用する必要があります。


最後のコミットは2018年4月で、最後の主要なコミットは2017
Boris


16

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を使用しています。残りの文字はまだ通常の辞書を使用します。ネストされたdefaultdictを使用することをお勧めします。
lionelmessi

3
実際、コードはdefault_factoryを設定せずにset_defaultをまだ使用しているため、最初の文字のdefaultdictを「使用」しているようには見えません。
studgeek 2017

11

「すべき」というものはありません。それはあなた次第です。さまざまな実装にはさまざまなパフォーマンス特性があり、さまざまな時間をかけて実装し、理解し、正しく理解します。私の意見では、これはソフトウェア開発全体としては典型的です。

私はおそらく最初に、これまでに作成されたすべてのトライノードのグローバルリストを作成し、各ノードの子ポインターをグローバルリストへのインデックスのリストとして表現することを試みます。子のリンクを表すためだけの辞書を持つことは、私には重すぎると感じます。


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をあきらめました。
Phil

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)

コード出力

True
False
['minakshi'、 'minhaj']
7
minakshi
minhajsir
pari
parikshit
shubh
shubham
shubhi

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.