Come creare un trie in Python


125

Sono interessato ai tentativi e ai DAWG (grafo diretto di parole acicliche) e ho letto molto su di loro ma non capisco come dovrebbe apparire il trie di output o il file DAWG.

  • Un trie dovrebbe essere un oggetto di dizionari annidati? Dove ogni lettera è divisa in lettere e così via?
  • Una ricerca eseguita su un tale dizionario sarebbe veloce se ci sono 100k o 500k voci?
  • Come implementare blocchi di parole costituiti da più di una parola separata da -o da uno spazio?
  • Come collegare il prefisso o il suffisso di una parola a un'altra parte della struttura? (per DAWG)

Voglio capire la migliore struttura di output per capire come crearne e utilizzarne una.

Apprezzerei anche quello che dovrebbe essere l' output di un DAWG insieme a trie .

Non voglio vedere rappresentazioni grafiche con bolle collegate tra loro, voglio conoscere l'oggetto di output una volta che un insieme di parole viene trasformato in tentativi o DAWG.


5
Leggi kmike.ru/python-data-structures per un'indagine su strutture di dati esotiche in Python
Colonel Panic

Risposte:


161

Unwind è essenzialmente corretto sul fatto che ci sono molti modi diversi per implementare un trie; e per un trie grande e scalabile, i dizionari annidati potrebbero diventare ingombranti - o almeno inefficienti di spazio. Ma dato che hai appena iniziato, penso che sia l'approccio più semplice; potresti programmare un semplice triein poche righe. Innanzitutto, una funzione per costruire il 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 non hai familiarità con setdefault, cerca semplicemente una chiave nel dizionario (qui, lettero _end). Se la chiave è presente, restituisce il valore associato; in caso contrario, assegna un valore predefinito a quella chiave e restituisce il valore ( {}o _end). (È come se una versione di getquesto aggiorni anche il dizionario.)

Successivamente, una funzione per verificare se la parola è nel 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

Lascio a voi l'inserimento e la rimozione come esercizio.

Ovviamente, il suggerimento di Unwind non sarebbe molto più difficile. Potrebbe esserci un leggero svantaggio di velocità in quanto la ricerca del sottonodo corretto richiederebbe una ricerca lineare. Ma la ricerca sarebbe limitata al numero di caratteri possibili - 27 se includiamo _end. Inoltre, non c'è nulla da guadagnare creando un enorme elenco di nodi e accedendovi per indice come suggerisce; potresti anche semplicemente annidare gli elenchi.

Infine, aggiungerò che la creazione di un grafico di parole acicliche dirette (DAWG) sarebbe un po 'più complessa, perché devi rilevare le situazioni in cui la tua parola corrente condivide un suffisso con un'altra parola nella struttura. In effetti, questo può diventare piuttosto complesso, a seconda di come vuoi strutturare il DAWG! Potrebbe essere necessario imparare alcune cose sulla distanza di Levenshtein per farlo bene.


1
Ecco fatto il cambiamento. Attaccherei dict.setdefault()(è sottoutilizzato e non abbastanza noto), in parte perché aiuta a prevenire bug troppo facili da creare con undefaultdict (dove non si otterrebbe un KeyErrorper chiavi inesistenti sull'indicizzazione). L'unica cosa che ora lo renderebbe utilizzabile per il codice di produzione è usare _end = object():-)
Martijn Pieters

@MartijnPieters hmmm, ho scelto espressamente di non usare l'oggetto, ma non ricordo perché. Forse perché sarebbe difficile da interpretare se visto nella demo? Immagino di poter creare un oggetto finale con una
riproduzione

27

Dai un'occhiata a questo:

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

Strutture Trie statiche efficienti in termini di memoria per Python (2.xe 3.x).

I dati delle stringhe in un trie MARISA possono richiedere fino a 50x-100x meno memoria rispetto a un dict Python standard; la velocità di ricerca grezza è paragonabile; trie fornisce anche metodi avanzati veloci come la ricerca del prefisso.

Basato sulla libreria C ++ marisa-trie.

Ecco un post sul blog di un'azienda che utilizza con successo marisa trie:
https://www.repustate.com/blog/sharing-large-data-structure-across-processes-python/

In Repustate, molti dei nostri modelli di dati che utilizziamo nella nostra analisi del testo possono essere rappresentati come semplici coppie chiave-valore o dizionari in gergo Python. Nel nostro caso particolare, i nostri dizionari sono enormi, poche centinaia di MB ciascuno, e devono essere consultati costantemente. Infatti, per una data richiesta HTTP, è possibile accedere a 4 o 5 modelli, ognuno dei quali esegue 20-30 ricerche. Quindi il problema che dobbiamo affrontare è come manteniamo le cose veloci per il client e il più leggere possibile per il server.

...

Ho trovato questo pacchetto, marisa try, che è un wrapper Python attorno a un'implementazione C ++ di un trie marisa. "Marisa" è l'acronimo di Matching Algorithm with Recursively Implemented StorAge. La cosa fantastica dei tentativi di marisa è che il meccanismo di archiviazione riduce davvero la quantità di memoria necessaria. L'autore del plugin Python ha affermato che le dimensioni sono ridotte di 50-100 volte: la nostra esperienza è simile.

La cosa fantastica del pacchetto trie marisa è che la struttura del trie sottostante può essere scritta su disco e quindi letta tramite un oggetto mappato in memoria. Con un trie Marisa mappato a memoria, tutti i nostri requisiti sono ora soddisfatti. L'utilizzo della memoria del nostro server è diminuito drasticamente, di circa il 40%, e le nostre prestazioni sono rimaste invariate rispetto a quando abbiamo utilizzato l'implementazione del dizionario di Python.

Ci sono anche un paio di implementazioni pure-python, anche se, a meno che tu non sia su una piattaforma con restrizioni, vorresti usare l'implementazione supportata da C ++ sopra per le migliori prestazioni:


l'ultimo commit è stato nell'aprile 2018, l'ultimo commit importante è stato come il 2017
Boris

25

Ecco un elenco di pacchetti Python che implementano Trie:

  • marisa-trie : un'implementazione basata su C ++.
  • python-trie - una semplice implementazione di Python puro.
  • PyTrie : un'implementazione di Python puro più avanzata.
  • pygtrie : una pura implementazione di Python di Google.
  • datrie - un'implementazione di trie a doppio array basata su libdatrie .

18

Modificato dal senderlemetodo di (sopra). Ho scoperto che Python defaultdictè l'ideale per creare un trie o un albero di prefissi.

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

La mia comprensione della complessità dello spazio è O (n * m). Alcuni hanno discusso qui. stackoverflow.com/questions/2718816/...
dapangmao

5
@dapangmao u stai usando defaultdict solo per il primo carattere solo. I caratteri di riposo usano ancora il normale dict. Sarebbe meglio usare defaultdict annidato.
lionelmessi

3
In realtà, il codice non sembra "usare" il defaultdict per il primo carattere poiché non imposta default_factory e usa ancora set_default.
studgeek

12

Non c'è "dovrebbe"; tocca a voi. Varie implementazioni avranno caratteristiche di prestazioni diverse, impiegheranno tempi diversi per implementare, comprendere e ottenere il risultato corretto. Questo è tipico per lo sviluppo del software nel suo complesso, secondo me.

Probabilmente proverei prima ad avere un elenco globale di tutti i nodi trie finora creati e a rappresentare i puntatori figlio in ogni nodo come un elenco di indici nell'elenco globale. Avere un dizionario solo per rappresentare il bambino che collega mi sembra troppo pesante.


2
ancora una volta, grazie, tuttavia penso ancora che la tua risposta abbia bisogno di spiegazioni e chiarimenti un po 'più approfonditi poiché la mia domanda è finalizzata a capire la logica e la struttura delle funzionalità di DAWG e TRIE. Il tuo ulteriore contributo sarà molto utile e apprezzato.
Phil

A meno che non utilizzi oggetti con slot, il tuo spazio dei nomi dell'istanza sarà comunque dizionari.
Mad Physicist,

4

Se vuoi implementare un TRIE come classe Python, ecco qualcosa che ho scritto dopo aver letto su di loro:

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
Grazie @NoctisSkytower. Questo è fantastico all'inizio, ma ho rinunciato a Python e TRIES o DAWG a causa del consumo di memoria estremamente elevato di Python in questi casi.
Phil

3
Ecco a cosa serve ____slots____. Riduce la quantità di memoria utilizzata da una classe, quando ne hai molte istanze.
dstromberg

3

Questa versione utilizza la ricorsione

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)

Produzione:

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

Definisci Trie:

_trie = lambda: defaultdict(_trie)

Crea Trie:

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

Consultare:

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

Test:

print(word_exist(trie, 'cam'))

1
attenzione: restituisce Truesolo una parola intera, ma non il prefisso, perché il prefisso cambia return '_end' in currinreturn 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()

Su

True
False
False
False
{'h': {'i': {'*': True}}}

0

Classe Python per Trie


Trie Data Structure può essere utilizzato per memorizzare i dati in O(L)cui L è la lunghezza della stringa quindi per inserire N stringhe la complessità temporale sarebbe O(NL)la stringa in cui è possibile cercareO(L) solo per l'eliminazione.

Può essere clonato da 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)

Codice Oputpt

Vero
Falso
['minakshi', 'minhaj']
7
minakshi
minhajsir
pari
parikshit
shubh
shubham
shubhi

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.