Como criar um teste em Python

124

Estou interessado em tentativas e DAWGs (gráfico de palavras acíclicas diretas) e tenho lido muito sobre eles, mas não entendo como deve ser a trie de saída ou o arquivo DAWG.

  • Um trie deve ser um objeto de dicionários aninhados? Onde cada letra é dividida em letras e assim por diante?
  • Uma pesquisa realizada nesse dicionário seria rápida se houver entradas de 100k ou 500k?
  • Como implementar blocos de palavras que consistem em mais de uma palavra separada por -ou espaço?
  • Como vincular prefixo ou sufixo de uma palavra a outra parte da estrutura? (para DAWG)

Quero entender a melhor estrutura de saída para descobrir como criar e usar uma.

Eu também apreciaria qual deveria ser a saída de um DAWG junto com trie .

Não quero ver representações gráficas com bolhas ligadas entre si, quero conhecer o objeto de saída assim que um conjunto de palavras for transformado em tentativa ou DAWG.

Phil
fonte
5
Leia kmike.ru/python-data-structures para uma pesquisa sobre estruturas de dados exóticas em Python
Coronel Panic

Respostas:

161

Desenrolar é essencialmente correto que existem muitas maneiras diferentes de implementar uma tentativa; e, para uma tentativa grande e escalável, os dicionários aninhados podem se tornar pesados ​​- ou pelo menos ineficientes em termos de espaço. Mas como você está apenas começando, acho que é a abordagem mais fácil; você pode codificar um simples trieem apenas algumas linhas. Primeiro, uma função para construir o 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_'}}}}

Se você não estiver familiarizado setdefault, basta procurar uma chave no dicionário (aqui letterou _end). Se a chave estiver presente, ela retornará o valor associado; caso contrário, atribui um valor padrão a essa chave e retorna o valor ( {}ou _end). (É como uma versão getque também atualiza o dicionário.)

Em seguida, uma função para testar se a palavra está no seguinte:

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

Deixarei inserção e remoção para você como um exercício.

Obviamente, a sugestão de Unwind não seria muito mais difícil. Pode haver uma ligeira desvantagem de velocidade, pois encontrar o subnó correto exigiria uma pesquisa linear. Mas a pesquisa seria limitada ao número de caracteres possíveis - 27, se incluirmos _end. Além disso, não há nada a ganhar criando uma lista enorme de nós e acessando-os por índice, como ele sugere; você pode apenas aninhar as listas.

Por fim, acrescentarei que a criação de um gráfico de palavras acíclicas direcionadas (DAWG) seria um pouco mais complexa, porque você precisa detectar situações nas quais sua palavra atual compartilha um sufixo com outra palavra na estrutura. De fato, isso pode ficar bastante complexo, dependendo de como você deseja estruturar o DAWG! Talvez você precise aprender algumas coisas sobre a distância de Levenshtein para acertar.

remetente
fonte
1
Lá, mudanças feitas. Eu ficaria com dict.setdefault()(é subutilizado e não é bem conhecido o suficiente), em parte porque ajuda a evitar bugs que são fáceis de criar com um defaultdict(onde você não obteria KeyErrorchaves inexistentes na indexação). A única coisa agora que iria torná-lo utilizável para o código de produção está usando _end = object():-)
Martijn Pieters
@MartijnPieters hmmm, eu escolhi especificamente não usar objeto, mas não me lembro por quê. Talvez porque seria difícil de interpretar quando visto na demo? Acho que eu poderia fazer um objeto final com um repr costume
senderle
27

Veja isso:

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

Estruturas Trie com eficiência de memória estática para Python (2.xe 3.x).

Os dados de sequência em um teste MARISA podem levar até 50x-100x menos memória do que em um ditado Python padrão; a velocidade de pesquisa bruta é comparável; O trie também fornece métodos avançados e rápidos, como a pesquisa de prefixos.

Baseado na biblioteca marisa-trie C ++.

Aqui está uma postagem de blog de uma empresa que usa marisa trie com sucesso:
https://www.repustate.com/blog/sharing-large-data-structure-across-processes-python/

No Repustate, muitos de nossos modelos de dados que usamos em nossa análise de texto podem ser representados como pares simples de valores-chave ou dicionários no jargão Python. No nosso caso particular, nossos dicionários são enormes, com algumas centenas de MB cada, e precisam ser acessados ​​constantemente. De fato, para uma determinada solicitação HTTP, 4 ou 5 modelos podem ser acessados, cada um fazendo 20 a 30 pesquisas. Portanto, o problema que enfrentamos é como mantemos as coisas rápidas para o cliente e o mais leve possível para o servidor.

...

Encontrei este pacote, marisa tenta, que é um wrapper Python em torno de uma implementação em C ++ de um marisa trie. "Marisa" é um acrônimo para Matching Algorithm with Recursively Implemented StorAge. O que é ótimo nas tentativas da marisa é que o mecanismo de armazenamento reduz realmente a quantidade de memória necessária. O autor do plugin Python afirmou uma redução de tamanho de 50 a 100X - nossa experiência é semelhante.

O que é ótimo no pacote marisa trie é que a estrutura subjacente da trie pode ser gravada no disco e depois lida através de um objeto mapeado na memória. Com uma marisa trie mapeada em memória, todos os nossos requisitos foram atendidos. O uso de memória do servidor caiu drasticamente, em cerca de 40%, e nosso desempenho permaneceu inalterado quando usamos a implementação de dicionário do Python.

Existem também algumas implementações de python puro, embora, a menos que você esteja em uma plataforma restrita, você queira usar a implementação suportada em C ++ acima para obter melhor desempenho:

Anentrópico
fonte
a última confirmação foi em abril de 2018, a última confirmação principal ocorreu em 2017
Boris
25

Aqui está uma lista de pacotes python que implementam o Trie:

  • marisa-trie - uma implementação baseada em C ++.
  • python-trie - uma simples implementação pura em python.
  • PyTrie - uma implementação python pura mais avançada.
  • pygtrie - uma implementação python pura do Google.
  • datrie - uma implementação de dupla matriz baseada em libdatrie .
Tzach
fonte
17

Modificado a partir senderledo método (acima). Eu descobri que o Python defaultdicté ideal para criar uma árvore trie ou prefixo.

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')
dapangmao
fonte
Meu entendimento da complexidade do espaço é O (n * m). Alguns discutem aqui. stackoverflow.com/questions/2718816/…
dapangmao
5
@dapangmao Você está usando o defaultdict apenas para o primeiro caractere. Os caracteres de descanso ainda usam o ditado normal. Seria melhor usar o defaultdict aninhado.
Lionel Messi
3
Na verdade, o código não parece "usar" o padrão default para o primeiro caractere, pois não define o default_factory e ainda está usando o set_default.
Studgeek #
12

Não há "deveria"; você decide. Várias implementações terão características de desempenho diferentes, levarão várias quantidades de tempo para implementar, entender e acertar. Isso é típico para o desenvolvimento de software como um todo, na minha opinião.

Eu provavelmente tentaria primeiro criar uma lista global de todos os nós até agora criados e representar os ponteiros filhos em cada nó como uma lista de índices na lista global. Ter um dicionário apenas para representar a ligação entre crianças parece muito pesado para mim.

descontrair
fonte
2
mais uma vez, obrigado. No entanto, ainda acho que sua resposta precisa de explicações e esclarecimentos um pouco mais profundos, pois minha pergunta tem como objetivo descobrir a lógica e a estrutura da funcionalidade dos DAWGs e TRIEs. Sua contribuição adicional será muito útil e apreciada.
Phil
A menos que você use objetos com slots, o espaço para nome da sua instância será dicionários de qualquer maneira.
Físico Louco
4

Se você deseja que um TRIE seja implementado como uma classe Python, aqui está algo que escrevi depois de ler sobre eles:

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])
Noctis Skytower
fonte
2
Obrigado @NoctisSkytower. Isso é ótimo para começar, mas eu meio que desisti de Python e TRIES ou DAWGs devido ao consumo extremamente alto de memória de Python nesses cenários.
22413 Phil
3
É para isso que serve ____slots____. Reduz a quantidade de memória usada por uma classe, quando você tem muitas instâncias.
dstromberg 28/09
3

Esta versão está usando recursão

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)

Resultado:

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': {}
    }
  }
}
naren
fonte
3
from collections import defaultdict

Definir Trie:

_trie = lambda: defaultdict(_trie)

Crie Trie:

trie = _trie()
for s in ["cat", "bat", "rat", "cam"]:
    curr = trie
    for c in s:
        curr = curr[c]
    curr.setdefault("_end")

Olho para cima:

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

Teste:

print(word_exist(trie, 'cam'))
DingLi
fonte
1
Cuidado: Este retornos Truesomente para uma palavra inteira, mas não para prefixo, para a mudança de prefixo return '_end' in currareturn 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()

Fora

True
False
False
False
{'h': {'i': {'*': True}}}
Nons
fonte
0

Classe Python para Trie


A estrutura de dados Trie pode ser usada para armazenar dados em O(L)que L é o comprimento da string, portanto, para inserir N strings, a complexidade do tempo seria que O(NL)a string pudesse ser pesquisada da O(L)mesma maneira que a exclusão.

Pode ser clonado em 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)

Code Oputpt

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

Parikshit Agarwal
fonte