如何在Python中创建特里


123

我对尝试和DAWG(直接非循环字图)感兴趣,并且已经阅读了很多有关它们的信息,但我不知道输出trie或DAWG文件应该是什么样。

  • 特里应该是嵌套词典的对象吗?每个字母分为几个字母,依此类推?
  • 如果存在100k或500k条目,对这样的词典执行的查找会很快吗?
  • 如何实现由多个单词组成的单词块,用-空格或空格分隔?
  • 如何将单词的前缀或后缀链接到结构的另一部分?(对于DAWG)

我想了解最佳的输出结构,以便弄清楚如何创建和使用一个结构。

我也希望DAWGtrie一起输出

我不想看到带有彼此链接的气泡的图形表示,我想知道将一组单词转换为尝试或DAWG后的输出对象。


5
阅读kmike.ru/python-data-structures,以了解Python外来数据结构的调查
Panic Panic

Answers:


161

展开实际上是正确的,因为有许多不同的方法可以实现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)。如果存在键,则返回关联的值;否则,返回0。如果不是,它将为该键分配一个默认值并返回值({}_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!您可能需要学习一些有关Levenshtein 距离的知识才能正确使用它。


1
在那里,进行了更改。我坚持使用dict.setdefault()(它未得到充分利用,并且不够知名),部分原因是它有助于防止使用defaultdictKeyError在索引中不存在的键不会得到)创建太容易的错误。现在唯一使它可用于生产代码的东西是使用_end = object():-)
Martijn Pieters

@MartijnPieters hmmm,我特别选择不使用对象,但我不记得为什么。也许是因为在演示中看到它很难解释?我想我可以通过自定义代表来制作最终对象
senderle

27

看看这个:

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

适用于Python的静态内存高效Trie结构(2.x和3.x)。

与标准Python字典相比,MARISA-trie中的字符串数据最多可占用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)尝试了一下,这是一个围绕玛丽莎(Marisa Trie)C ++实现的Python包装器。“ Marisa”是递归实现StorAge的匹配算法的首字母缩写。玛丽莎(Marisa)尝试的最大好处是,存储机制确实缩小了所需的内存量。Python插件的作者声称尺寸减少了50-100倍-我们的经验是相似的。

marisa trie软件包的优点在于,可以将基本的trie结构写入磁盘,然后通过内存映射对象读取。使用内存映射的marisa trie,现在可以满足我们的所有要求。我们服务器的内存使用量急剧下降了约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您仅将defaultdict用于第一个字符。其余字符仍然使用普通字典。使用嵌套的defaultdict会更好。
lionelmessi

3
实际上,由于该代码未设置default_factory且仍在使用set_default,因此该代码似乎并未“使用”第一个字符的defaultdict。
studgeek

11

没有“应该”;由你决定。各种实现将具有不同的性能特征,需要花费大量时间来实现,理解和正确使用。我认为,这对于整个软件开发来说都是典型的。

我可能首先会尝试创建一个到目前为止所有trie节点的全局列表,并将每个节点中的子指针表示为全局列表中的索引列表。对我来说,拥有一本字典来代表孩子的联系感觉太沉重了。


2
再次感谢您,但是我仍然认为您的答案需要更深入的说明和澄清,因为我的问题旨在弄清楚DAWG和TRIEs功能的逻辑和结构。您的进一步意见将非常有用并受到赞赏。
Phil

除非您使用带有插槽的对象,否则您的实例名称空间将始终是字典。
疯狂物理学家

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。
2013年

3
这就是____插槽____的用途。当您有很多实例时,它会减少类使用的内存量。
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 '19

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)

代码输入

正确
错误
['minakshi','minhaj']
7
minakshi
minhajsir
pari
parikshit
shubh
shubham
shubhi

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.