【问题标题】:How to create a trie in Python如何在 Python 中创建一个 trie
【发布时间】:2012-06-16 10:29:00
【问题描述】:

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

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

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

我也很欣赏 DAWG 的输出 以及 trie

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

【问题讨论】:

标签: python trie dawg


【解决方案1】:

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

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

【讨论】:

  • 再次感谢您,但是我仍然认为您的回答需要更深入的解释和澄清,因为我的问题旨在弄清楚 DAWG 和 TRIE 功能的逻辑和结构。您的进一步输入将非常有用和感激。
  • 除非你使用带槽的对象,否则你的实例命名空间将是字典。
【解决方案2】:

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 的建议不会难很多。找到正确的子节点可能需要线性搜索,这可能会带来轻微的速度劣势。但是搜索将限制在可能的字符数——如果我们包括_end,则为 27 个。此外,正如他所建议的那样,通过创建大量节点列表并按索引访问它们并没有什么好处;你也可以只嵌套列表。

最后,我要补充一点,创建有向无环词图 (DAWG) 会稍微复杂一些,因为您必须检测当前词与结构中的另一个词共享后缀的情况。事实上,这可能会变得相当复杂,具体取决于您希望如何构建 DAWG!您可能需要了解一些关于 Levenshtein distance 的知识才能正确使用。

【讨论】:

  • 那里,改变了。我会坚持使用dict.setdefault()(它没有得到充分利用,而且还不够知名),部分原因是它有助于防止使用defaultdict 太容易创建的错误(你不会得到KeyError索引上不存在的键)。现在唯一可以让它用于生产代码的是使用_end = object() :-)
  • @MartijnPieters 嗯,我特意选择不使用对象,但我不记得为什么了。也许是因为在演示中看到它时很难解释?我想我可以用自定义 repr 制作一个最终对象
  • 抱歉,这不是真正的 Trie 实现,因为它不提供前缀搜索
  • @PrithivirajDamodaran,我会说“trie”是数据结构的名称。 “前缀搜索”是可以与 trie 一起使用的算法的名称。我还没有在这里实现前缀搜索,但这并不能阻止数据结构成为一个 trie。
  • @PrithivirajDamodaran 查看我的答案以在此处进行前缀搜索:stackoverflow.com/a/69087281/1422096
【解决方案3】:

看看这个:

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

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

MARISA-trie 中的字符串数据可能占用的内存比 在标准的 Python 字典中;原始查找速度相当;特里 还提供快速的高级方法,如前缀搜索。

基于 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++ 支持的实现以获得最佳性能:

【讨论】:

  • 最后一次提交是在 2018 年 4 月,最后一次重大提交是在 2017 年
  • marisa-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])

【讨论】:

  • 谢谢@NoctisSkytower。这很好,但由于在这些情况下 Python 的内存消耗极高,我有点放弃了 Python 和 TRIES 或 DAWG。
  • 这就是____slots____ 的用途。当你有很多实例时,它会减少一个类使用的内存量。
【解决方案5】:

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

【讨论】:

    【解决方案6】:

    修改自senderle的方法(上图)。我发现 Python 的 defaultdict 非常适合创建 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')
    

    【讨论】:

    • 我对空间复杂度的理解是O(n*m)。有些人在这里讨论。 stackoverflow.com/questions/2718816/…
    • @dapangmao 你只对第一个字符使用 defaultdict。其余字符仍使用普通字典。使用嵌套的 defaultdict 会更好。
    • 实际上,代码似乎也没有“使用”第一个字符的 defaultdict,因为它没有设置 default_factory 并且仍在使用 set_default。
    【解决方案7】:

    这个版本使用递归

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

    【讨论】:

      【解决方案8】:
      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'))
      

      【讨论】:

      • 注意:这只会为整个单词返回True,而不是前缀,因为前缀将return '_end' in curr更改为return True
      • @ShrikantShete 如果要进行前缀搜索,还必须在if w not in curr: return False 之前添加if '_end' in curr: return True
      【解决方案9】:
      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}}}
      

      【讨论】:

        【解决方案10】:

        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
        米纳什
        明海西尔
        巴黎
        废话

        舒巴姆

        【讨论】:

          【解决方案11】:

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

          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
          

          【讨论】:

          • 赋值head[char] = {}只是手动实现collections.defaultdictdict.setdefault(letter, {})
          【解决方案12】:

          使用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')
          

          【讨论】:

          • 伟大的实施!非常感谢。
          【解决方案13】:

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

          由于我们使用字典来存储children,所以不需要char和integer之间的转换,也不需要提前分配数组内存。

          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']
          

          【讨论】:

            【解决方案14】:

            带前缀搜索

            这里是@senderle's answer,稍微修改为接受前缀搜索(不仅是全字匹配):

            _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
            

            【讨论】:

              【解决方案15】:
              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]])
              

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2011-09-18
                • 1970-01-01
                • 2011-11-18
                • 2019-09-23
                • 2018-01-03
                • 1970-01-01
                相关资源
                最近更新 更多