146

我对尝试和 DAWG(直接非循环词图)很感兴趣,并且我已经阅读了很多关于它们的内容,但我不明白输出 trie 或 DAWG 文件应该是什么样子。

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

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

我也很感激DAWGtrie的输出应该是什么。

我不想看到带有相互链接的气泡的图形表示,我想知道一旦一组单词变成尝试或 DAWG 后的输出对象。

4

15 回答 15

196

Unwind本质上是正确的,有许多不同的方式来实现 trie;对于大型、可扩展的 trie,嵌套字典可能会变得很麻烦——或者至少空间效率低下。但是由于您才刚刚开始,我认为这是最简单的方法;你可以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也更新了字典。)

接下来,一个测试单词是否在 trie 中的函数:

>>> 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 距离的知识才能正确处理。

于 2012-06-13T13:56:08.223 回答
30

看看这个:

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

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

MARISA-trie 中的字符串数据可能比标准 Python dict 中的内存少 50 到 100 倍;原始查找速度相当;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 Trys,它是一个 Python 包装器,围绕着 marisa trie 的 C++ 实现。“Marisa”是 Matching Algorithm with Recursively Implemented StorAge 的首字母缩写词。marisa 尝试的优点是存储机制确实缩小了您需要的内存量。Python 插件的作者声称尺寸减少了 50-100 倍——我们的经验是相似的。

marisa trie 包的优点在于底层的 trie 结构可以写入磁盘,然后通过内存映射对象读入。使用内存映射的 marisa trie,现在我们的所有要求都得到了满足。我们服务器的内存使用量急剧下降了大约 40%,而且我们的性能与使用 Python 的字典实现时相比没有变化。

还有一些纯 python 实现,但除非您在受限平台上,否则您希望使用上面的 C++ 支持的实现以获得最佳性能:

于 2012-10-16T11:22:37.137 回答
28

以下是实现 Trie 的 python 包的列表:

于 2014-01-23T08:36:11.513 回答
23

修改自senderle's 方法(上图)。我发现 Pythondefaultdict非常适合创建 trie 或前缀树。

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')
于 2015-05-08T13:01:17.277 回答
14

没有“应该”;由你决定。不同的实现将具有不同的性能特征,需要不同的时间来实现、理解和正确处理。在我看来,这对于整个软件开发来说是典型的。

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

于 2012-06-13T12:59:48.277 回答
10
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'))
于 2018-03-19T07:32:29.750 回答
7

这是使用 TrieNode 类的完整代码。还实现了 auto_complete 方法以返回带有前缀的匹配单词。

由于我们使用字典来存储孩子,所以不需要将char转换为整数,反之亦然,也不需要提前分配数组内存。

class TrieNode:
    def __init__(self):
        #Dict: Key = letter, Item = TrieNode
        self.children = {}
        self.end = False
class Trie:
    def __init__(self):
        self.root = TrieNode()

    def build_trie(self,words):       
        for word in words:
            self.insert(word)

    def insert(self,word):
        node = self.root
        for char in word:
            if char not in node.children:
              node.children[char] = TrieNode()
            node = node.children[char]
        node.end = True
    def search(self, word):
        node = self.root
        for char in word:
            if char in node.children:
                node = node.children[char]
            else:
                return False
            
        return node.end

    def _walk_trie(self, node, word, word_list):

        if node.children:   
            for char in node.children:        
                word_new = word + char
                if node.children[char].end:       
                # if node.end: 
                    word_list.append( word_new)
                    # word_list.append( word)
                self._walk_trie(node.children[char],  word_new  , word_list)

    def auto_complete(self, partial_word):
        node = self.root

        word_list = [ ]
        #find the node for last char of word
        for char in  partial_word:
           if char in node.children:
              node = node.children[char]
           else:
                # partial_word not found return 
                return word_list
         
        if node.end:
             word_list.append(partial_word)

        #  word_list will be created in this method for suggestions that start with partial_word
        self._walk_trie(node, partial_word, word_list)
        return word_list

创建一个 Trie

t = Trie()
words = ['hi', 'hieght', 'rat', 'ram', 'rattle', 'hill']
t.build_trie(words)

搜索单词

words = ['hi', 'hello']
for word in  words:
    print(word, t.search(word))

hi True
hel False

使用前缀搜索单词

partial_word = 'ra'
t.auto_complete(partial_word)

['rat', 'rattle', 'ram']
于 2021-03-22T07:00:23.077 回答
6

使用 defaultdict 和 reduce 函数。

创建特里

from functools import reduce
from collections import defaultdict
T = lambda : defaultdict(T)
trie = T()
reduce(dict.__getitem__,'how',trie)['isEnd'] = True

特里:

defaultdict(<function __main__.<lambda>()>,
            {'h': defaultdict(<function __main__.<lambda>()>,
                         {'o': defaultdict(<function __main__.<lambda>()>,
                                      {'w': defaultdict(<function __main__.<lambda>()>,
                                                   {'isEnd': True})})})})

在特里搜索:

curr = trie
for w in 'how':
    if w in curr:
        curr = curr[w]
    else:
        print("Not Found")
        break
if curr['isEnd']:
    print('Found')
于 2020-10-11T11:44:25.263 回答
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])
于 2013-07-12T16:38:13.717 回答
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': {}
    }
  }
}
于 2016-06-13T12:24:32.990 回答
1

Trie 的 Python 类


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

于 2020-07-25T06:35:07.387 回答
1

这很像以前的答案,但更容易阅读:

def make_trie(words):
    trie = {}
    for word in words:
        head = trie
        for char in word:
            if char not in head:
                head[char] = {}
            head = head[char]
        head["_end_"] = "_end_"
    return trie
于 2020-08-18T09:07:51.963 回答
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}}}
于 2019-07-24T15:27:51.940 回答
0

带前缀搜索

这是@senderle 的答案,稍作修改以接受前缀搜索(不仅是全字匹配):

_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

def in_trie(trie, word):
    current_dict = trie
    for letter in word:
        if _end in current_dict:
            return True
        if letter not in current_dict:
            return False
        current_dict = current_dict[letter]
        
t = make_trie(['hello', 'hi', 'foo', 'bar'])
print(in_trie(t, 'hello world')) 
# True
于 2021-09-07T11:19:30.500 回答
0
class TrieNode:
    def __init__(self):
        self.keys = {}
        self.end = False

class Trie:
    def __init__(self):
        self.root = TrieNode()
    def insert(self, word: str, node=None) -> None:
        if node == None:
            node = self.root
        # insertion is a recursive operation
        # this is base case to exit the recursion
        if len(word) == 0:
            node.end = True
            return
        # if this key does not exist create a new node
        elif word[0] not in node.keys:
            node.keys[word[0]] = TrieNode()
            self.insert(word[1:], node.keys[word[0]])
        # that means key exists
        else:
            self.insert(word[1:], node.keys[word[0]])
    def search(self, word: str, node=None) -> bool:
        if node == None:
            node = self.root
        # this is positive base case to exit the recursion
        if len(word) == 0 and node.end == True:
            return True
        elif len(word) == 0:
            return False
        elif word[0] not in node.keys:
            return False
        else:
            return self.search(word[1:], node.keys[word[0]])
    def startsWith(self, prefix: str, node=None) -> bool:
        if node == None:
            node = self.root
        if len(prefix) == 0:
            return True
        elif prefix[0] not in node.keys:
            return False
        else:
            return self.startsWith(prefix[1:], node.keys[prefix[0]])
于 2021-10-16T01:08:19.700 回答