Cara membuat trie dengan Python

125

Saya tertarik dengan percobaan dan DAWGs (grafik kata asiklik langsung) dan saya telah banyak membaca tentang mereka tetapi saya tidak mengerti seperti apa seharusnya tampilan trie keluaran atau file DAWG.

  • Haruskah trie menjadi objek kamus bertingkat? Dimana setiap huruf dibagi menjadi huruf dan seterusnya?
  • Apakah pencarian yang dilakukan pada kamus seperti itu akan cepat jika ada 100k atau 500k entri?
  • Bagaimana menerapkan blok kata yang terdiri dari lebih dari satu kata yang dipisahkan dengan -spasi?
  • Bagaimana cara menghubungkan prefiks atau sufiks sebuah kata ke bagian lain dalam struktur? (untuk DAWG)

Saya ingin memahami struktur keluaran terbaik untuk mengetahui cara membuat dan menggunakannya.

Saya juga akan menghargai apa yang seharusnya menjadi output dari DAWG bersama dengan trie .

Saya tidak ingin melihat representasi grafis dengan gelembung yang terhubung satu sama lain, saya ingin mengetahui objek keluaran setelah sekumpulan kata diubah menjadi percobaan atau DAWG.

Phil
sumber
5
Baca kmike.ru/python-data-structures untuk survei struktur data eksotis dengan Python
Colonel Panic

Jawaban:

161

Bersantai pada dasarnya benar bahwa ada banyak cara berbeda untuk menerapkan trie; dan untuk percobaan yang besar dan dapat diskalakan, kamus bertingkat mungkin menjadi rumit - atau setidaknya tidak efisien ruang. Tetapi karena Anda baru saja memulai, saya pikir itu pendekatan yang paling mudah; Anda bisa membuat kode sederhana triehanya dalam beberapa baris. Pertama, fungsi untuk membangun 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_'}}}}

Jika Anda tidak terbiasa setdefault, itu hanya mencari kunci dalam kamus (di sini, letteratau _end). Jika kunci ada, ia mengembalikan nilai terkait; jika tidak, itu memberikan nilai default ke kunci itu dan mengembalikan nilai ( {}atau _end). (Ini seperti versi getyang juga memperbarui kamus.)

Selanjutnya, fungsi untuk menguji apakah kata tersebut ada di 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

Saya akan menyerahkan penyisipan dan pemindahan kepada Anda sebagai latihan.

Tentu saja, saran Unwind tidak akan jauh lebih sulit. Mungkin ada sedikit kerugian kecepatan dalam menemukan sub-node yang benar akan membutuhkan pencarian linier. Tetapi pencarian akan dibatasi pada jumlah karakter yang memungkinkan - 27 jika kita memasukkannya _end. Juga, tidak ada yang bisa diperoleh dengan membuat daftar node yang besar dan mengaksesnya dengan indeks seperti yang dia sarankan; Anda mungkin juga hanya menyusun daftar.

Terakhir, saya akan menambahkan bahwa membuat grafik kata asiklik terarah (DAWG) akan sedikit lebih rumit, karena Anda harus mendeteksi situasi di mana kata Anda saat ini berbagi sufiks dengan kata lain dalam struktur. Nyatanya, ini bisa menjadi agak rumit, bergantung pada bagaimana Anda ingin menyusun DAWG! Anda mungkin harus mempelajari beberapa hal tentang jarak Levenshtein untuk melakukannya dengan benar.

pengirim
sumber
1
Di sana, perubahan dilakukan. Saya akan tetap menggunakan dict.setdefault()(ini kurang dimanfaatkan dan hampir tidak cukup terkenal), sebagian karena membantu mencegah bug yang terlalu mudah dibuat dengan defaultdict(di mana Anda tidak akan mendapatkan KeyErrorkunci yang tidak ada untuk pengindeksan). Satu-satunya hal sekarang yang membuatnya dapat digunakan untuk kode produksi adalah menggunakan _end = object():-)
Martijn Pieters
@MartijnPieters hmmm, saya secara khusus memilih untuk tidak menggunakan objek, tetapi saya tidak ingat mengapa. Mungkin karena akan sulit diinterpretasikan jika dilihat di demo? Saya kira saya bisa membuat objek akhir dengan repr kustom
pengirim
27

Lihatlah ini:

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

Struktur Trie hemat memori statis untuk Python (2.x dan 3.x).

Data string dalam MARISA-trie mungkin membutuhkan hingga 50x-100x lebih sedikit memori daripada di Python standar diktekan; kecepatan pencarian mentah sebanding; trie juga menyediakan metode lanjutan cepat seperti pencarian awalan.

Berdasarkan pustaka C ++ marisa-trie.

Berikut adalah entri blog dari perusahaan yang berhasil menggunakan marisa trie:
https://www.repustate.com/blog/sharing-large-data-structure-across-processes-python/

Di Repustate, sebagian besar model data yang kami gunakan dalam analisis teks dapat direpresentasikan sebagai pasangan nilai kunci sederhana, atau kamus dalam bahasa Python. Dalam kasus khusus kami, kamus kami sangat besar, masing-masing beberapa ratus MB, dan mereka perlu diakses terus-menerus. Faktanya, untuk permintaan HTTP tertentu, 4 atau 5 model dapat diakses, masing-masing melakukan 20-30 pencarian. Jadi masalah yang kita hadapi adalah bagaimana kita menjaga agar semuanya cepat untuk klien dan seringan mungkin untuk server.

...

Saya menemukan paket ini, marisa mencoba, yang merupakan pembungkus Python di sekitar implementasi C ++ dari marisa trie. “Marisa” adalah singkatan dari Matching Algorithm with Recursively Implemented StorAge. Apa yang hebat tentang marisa mencoba adalah mekanisme penyimpanan benar-benar menyusutkan berapa banyak memori yang Anda butuhkan. Penulis plugin Python mengklaim pengurangan ukuran 50-100X - pengalaman kami serupa.

Yang hebat tentang paket marisa trie adalah bahwa struktur trie yang mendasarinya dapat ditulis ke disk dan kemudian dibaca melalui objek yang dipetakan memori. Dengan memori yang dipetakan marisa trie, semua persyaratan kami sekarang terpenuhi. Penggunaan memori server kami turun secara dramatis, sekitar 40%, dan kinerja kami tidak berubah dari saat kami menggunakan implementasi kamus Python.

Ada juga beberapa implementasi pure-python, meskipun kecuali Anda menggunakan platform terbatas, Anda ingin menggunakan implementasi yang didukung C ++ di atas untuk performa terbaik:

Anentropik
sumber
komitmen terakhir pada April 2018, komitmen besar terakhir terjadi pada 2017
Boris
25

Berikut adalah daftar paket python yang menerapkan Trie:

  • marisa-trie - implementasi berbasis C ++.
  • python-trie - implementasi python murni sederhana.
  • PyTrie - implementasi python murni yang lebih canggih.
  • pygtrie - implementasi python murni oleh Google.
  • datrie - implementasi trie array ganda berdasarkan libdatrie .
Tzach
sumber
18

Dimodifikasi dari senderlemetode (di atas). Saya menemukan bahwa Python defaultdictsangat ideal untuk membuat pohon trie atau prefiks.

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
sumber
Pemahaman saya tentang kompleksitas ruang adalah O (n * m). Beberapa berdiskusi di sini. stackoverflow.com/questions/2718816/…
dapangmao
5
@dapangmao u menggunakan defaultdict hanya untuk karakter pertama saja. Karakter istirahat masih menggunakan dict normal. Akan lebih baik jika menggunakan defaultdict bersarang.
lionelmessi
3
Sebenarnya, kode tampaknya tidak "menggunakan" defaultdict untuk karakter pertama karena tidak menyetel default_factory dan masih menggunakan set_default.
studgeek
12

Tidak ada "seharusnya"; terserah kamu. Berbagai implementasi akan memiliki karakteristik kinerja yang berbeda, membutuhkan waktu yang berbeda-beda untuk diterapkan, dipahami, dan dilakukan dengan benar. Ini tipikal untuk pengembangan perangkat lunak secara keseluruhan, menurut saya.

Saya mungkin pertama-tama akan mencoba memiliki daftar global dari semua node trie yang sejauh ini dibuat, dan mewakili anak-pointer di setiap node sebagai daftar indeks ke dalam daftar global. Memiliki kamus hanya untuk mewakili anak yang menghubungkan terasa terlalu berat, bagi saya.

beristirahat
sumber
2
sekali lagi, terima kasih namun saya masih berpikir bahwa jawaban Anda memerlukan penjelasan dan klarifikasi yang lebih dalam karena pertanyaan saya ditujukan untuk mencari tahu logika dan struktur fungsionalitas DAWG dan TRIE. Masukan Anda selanjutnya akan sangat berguna dan dihargai.
Phil
Kecuali Anda menggunakan objek dengan slot, namespace instance Anda akan tetap menjadi kamus.
Fisikawan Gila
4

Jika Anda ingin TRIE diimplementasikan sebagai kelas Python, berikut adalah sesuatu yang saya tulis setelah membaca tentang mereka:

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
sumber
2
Terima kasih @NoctisSkytower. Ini bagus untuk memulai tetapi saya agak menyerah pada Python dan TRIES atau DAWG karena konsumsi memori Python yang sangat tinggi dalam skenario kasus ini.
Phil
3
Untuk itulah ____slots____. Ini mengurangi jumlah memori yang digunakan oleh kelas, ketika Anda memiliki banyak instance.
dstromberg
3

Versi ini menggunakan rekursi

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)

Keluaran:

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
sumber
3
from collections import defaultdict

Tentukan Trie:

_trie = lambda: defaultdict(_trie)

Buat Percobaan:

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

Menengadah:

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

Uji:

print(word_exist(trie, 'cam'))
DingLi
sumber
1
PERHATIAN: ini mengembalikan Truehanya untuk seluruh kata, tetapi tidak untuk awalan, untuk perubahan awalan return '_end' in currmenjadireturn 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()

Di luar

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

Kelas Python untuk Trie


Trie Data Structure dapat digunakan untuk menyimpan data O(L)dimana L adalah panjang string sehingga untuk penyisipan N string kompleksitas waktu akan O(NL)string dapat dicari O(L)hanya dengan cara yang sama untuk penghapusan.

Dapat di-clone dari 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)

Kode Oputpt

Benar
Salah
['minakshi', 'minhaj']
7
minakshi
minhajsir
pari
parikesit
shubh
shubham
shubhi

Parikesit Agarwal
sumber