วิธีสร้าง Trie ใน Python


125

ฉันสนใจในการลองและ DAWG (กราฟคำแบบ acyclic โดยตรง) และฉันได้อ่านเกี่ยวกับพวกเขามามาก แต่ฉันไม่เข้าใจว่าเอาต์พุต trie หรือไฟล์ DAWG ควรมีลักษณะอย่างไร

  • Trie ควรเป็นวัตถุของพจนานุกรมที่ซ้อนกันหรือไม่? ตัวอักษรแต่ละตัวแบ่งเป็นตัวอักษรและอื่น ๆ ตรงไหน?
  • การค้นหาในพจนานุกรมดังกล่าวจะรวดเร็วหรือไม่หากมีรายการ 100k หรือ 500k
  • จะใช้บล็อกคำที่ประกอบด้วยคำมากกว่าหนึ่งคำที่คั่นด้วย-หรือเว้นวรรคได้อย่างไร
  • วิธีการเชื่อมโยงคำนำหน้าหรือคำต่อท้ายของคำกับส่วนอื่นในโครงสร้าง? (สำหรับ DAWG)

ฉันต้องการเข้าใจโครงสร้างผลลัพธ์ที่ดีที่สุดเพื่อที่จะหาวิธีสร้างและใช้งาน

ฉันยังอยากจะชื่นชมสิ่งที่ควรจะเป็นเอาท์พุทของ DAWGพร้อมกับTrie

ฉันไม่ต้องการเห็นการแสดงภาพกราฟิกด้วยฟองอากาศที่เชื่อมโยงกันฉันต้องการทราบว่าวัตถุที่ส่งออกเมื่อชุดของคำถูกเปลี่ยนเป็นการพยายามหรือ DAWG


5
อ่านkmike.ru/python-data-structuresสำหรับการสำรวจโครงสร้างข้อมูลที่แปลกใหม่ใน Python
Colonel Panic

คำตอบ:


161

การผ่อนคลายนั้นถูกต้องเป็นหลักว่ามีหลายวิธีในการใช้ 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ที่อัปเดตพจนานุกรมด้วย)

ถัดไปฟังก์ชั่นเพื่อทดสอบว่าคำนั้นอยู่ในไตร:

>>> 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เพื่อให้ถูกต้อง


1
มีการเปลี่ยนแปลง ฉันจะยึดติดกับdict.setdefault()(มันใช้งานน้อยเกินไปและไม่เป็นที่รู้จักมากพอ) ส่วนหนึ่งเป็นเพราะมันช่วยป้องกันข้อบกพร่องที่สร้างได้ง่ายเกินไปด้วย a defaultdict(ซึ่งคุณจะไม่ได้รับKeyErrorคีย์ที่ไม่มีอยู่ในการจัดทำดัชนี) สิ่งเดียวที่จะทำให้ใช้งานได้สำหรับรหัสการผลิตคือการใช้_end = object():-)
Martijn Pieters

@MartijnPieters อืมฉันเลือกที่จะไม่ใช้วัตถุโดยเฉพาะ แต่ฉันจำไม่ได้ว่าทำไม อาจเป็นเพราะมันยากที่จะตีความเมื่อเห็นในการสาธิต? ฉันเดาว่าฉันสามารถสร้าง end object ด้วยตัวแทนที่กำหนดเองได้
ส่ง

27

ดูสิ่งนี้:

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

โครงสร้าง Trie ที่มีประสิทธิภาพหน่วยความจำแบบคงที่สำหรับ Python (2.x และ 3.x)

ข้อมูลสตริงใน MARISA-trie อาจใช้หน่วยความจำน้อยกว่าใน Python dict มาตรฐานถึง 50x-100x ความเร็วในการค้นหาข้อมูลดิบเทียบได้ 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 รายการ ดังนั้นปัญหาที่เราเผชิญคือเราจะทำอย่างไรให้สิ่งต่างๆรวดเร็วสำหรับไคลเอนต์และเบาบางที่สุดสำหรับเซิร์ฟเวอร์

...

ฉันพบแพ็คเกจนี้มาริสาพยายามซึ่งเป็น Python wrapper รอบ ๆ การใช้งาน C ++ ของ marisa trie “ Marisa” เป็นคำย่อของ Matching Algorithm กับ StorAge ที่ดำเนินการซ้ำ สิ่งที่ยอดเยี่ยมเกี่ยวกับมาริสาพยายามคือกลไกการจัดเก็บข้อมูลจะลดขนาดหน่วยความจำที่คุณต้องการจริงๆ ผู้เขียนปลั๊กอิน Python อ้างว่ามีขนาดลดลง 50-100X - ประสบการณ์ของเราใกล้เคียงกัน

สิ่งที่ยอดเยี่ยมเกี่ยวกับแพ็คเกจ marisa trie คือโครงสร้าง trie ที่อยู่ข้างใต้สามารถเขียนลงดิสก์แล้วอ่านผ่านวัตถุที่แมปหน่วยความจำ ด้วยความจำที่แมป marisa trie ตอนนี้มีคุณสมบัติตรงตามข้อกำหนดทั้งหมดของเราแล้ว การใช้งานหน่วยความจำของเซิร์ฟเวอร์ของเราลดลงอย่างมากประมาณ 40% และประสิทธิภาพของเราก็ไม่เปลี่ยนแปลงจากเมื่อเราใช้การใช้งานพจนานุกรมของ Python

นอกจากนี้ยังมีการใช้งาน pure-python อีกด้วยแม้ว่าคุณจะอยู่บนแพลตฟอร์มที่ จำกัด คุณต้องการใช้การใช้งาน C ++ ที่ได้รับการสนับสนุนด้านบนเพื่อประสิทธิภาพที่ดีที่สุด:


การกระทำครั้งสุดท้ายคือในเดือนเมษายน 2018 การกระทำที่สำคัญครั้งสุดท้ายคือในปี 2017
บอริส

25

นี่คือรายการแพ็คเกจ python ที่ใช้ Trie:

  • marisa-trie - การใช้งานโดยใช้ C ++
  • python-trie - การใช้งาน python ที่เรียบง่าย
  • PyTrie - การใช้งาน Python บริสุทธิ์ขั้นสูง
  • pygtrie - การใช้งาน python โดย Google
  • datrie - การดำเนินงานอาร์เรย์ Trie คู่อยู่บนพื้นฐานของlibdatrie

18

แก้ไขจาก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

5
@dapangmao คุณใช้ defaultdict เฉพาะสำหรับถ่านแรกเท่านั้น อักขระที่เหลือยังคงใช้คำสั่งปกติ จะดีกว่าถ้าใช้ค่าเริ่มต้นที่ซ้อนกัน
lionelmessi

3
อันที่จริงโค้ดดูเหมือนจะไม่ "ใช้" defaultdict สำหรับอักขระตัวแรกเนื่องจากไม่ได้ตั้งค่า default_factory และยังคงใช้ set_default
studgeek

12

ไม่มี "ควร"; มันขึ้นอยู่กับคุณ. การนำไปใช้งานต่างๆจะมีลักษณะการทำงานที่แตกต่างกันใช้เวลาในการดำเนินการทำความเข้าใจและทำให้ถูกต้อง นี่เป็นเรื่องปกติสำหรับการพัฒนาซอฟต์แวร์โดยรวมในความคิดของฉัน

ก่อนอื่นฉันอาจจะลองสร้างรายการส่วนกลางของโหนด trie ทั้งหมดที่สร้างขึ้นและแสดงตัวชี้ลูกในแต่ละโหนดเป็นรายการดัชนีในรายการส่วนกลาง การมีพจนานุกรมเพียงเพื่อแสดงถึงการเชื่อมโยงของเด็กทำให้รู้สึกหนักเกินไปสำหรับฉัน


2
ขอขอบคุณอีกครั้ง แต่ฉันยังคิดว่าคำตอบของคุณต้องการคำอธิบายและการชี้แจงที่ลึกซึ้งกว่านี้เนื่องจากคำถามของฉันมีเป้าหมายเพื่อหาตรรกะและโครงสร้างของฟังก์ชันการทำงานของ DAWG และ TRIEs ข้อมูลเพิ่มเติมของคุณจะเป็นประโยชน์และชื่นชมมาก
ฟิล

เว้นแต่คุณจะใช้ออบเจ็กต์กับสล็อตเนมสเปซอินสแตนซ์ของคุณจะเป็นพจนานุกรมอยู่ดี
Mad Physicist

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

2
ขอบคุณ @NoctisSkytower นี่เป็นการเริ่มต้นที่ดี แต่ฉันยอมแพ้ Python และ TRIES หรือ DAWGs เนื่องจากการใช้หน่วยความจำ Python ที่สูงมากในสถานการณ์เช่นนี้
ฟิลิป

3
นั่นคือสิ่งที่ ____slots____ มีไว้สำหรับ ช่วยลดจำนวนหน่วยความจำที่ใช้โดยคลาสเมื่อคุณมีอินสแตนซ์จำนวนมาก
dstromberg

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

3
from collections import defaultdict

กำหนด Trie:

_trie = lambda: defaultdict(_trie)

สร้าง 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'))

1
ข้อควรระวัง: สิ่งนี้จะส่งกลับTrueสำหรับทั้งคำเท่านั้น แต่ไม่ใช่สำหรับคำนำหน้าสำหรับการเปลี่ยนคำนำหน้าreturn '_end' in currเป็นreturn 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()

ออก

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

0

Python Class สำหรับ Trie


Trie Data Structure สามารถใช้ในการจัดเก็บข้อมูล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)

รหัส Oputpt

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

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.