繁体   English   中英

如何在 python 中实现 trie 的删除 function?

[英]How to implement the remove function of a trie in python?

我已经阅读了 python 中的 trie 的以下实现: https://stackoverflow.com/a/11016430/2225221

并尝试为其设置删除功能。 基本上,即使一开始我也遇到了问题:如果你想从树中删除一个词,它可以有子“词”,或者它可以是另一个词的“子词”。

如果你用“del dict[key]”删除,你也删除了上面提到的这两种词。 谁能帮助我,如何正确删除所选单词(让我们假设它在特里)

基本上,要从trie中删除一个单词(在链接的答案中已实现),您只需删除其_end标记,例如:

def remove_word(trie, word):
    current_dict = trie
    for letter in word:
        current_dict = current_dict.get(letter, None)
        if current_dict is None:
            # the trie doesn't contain this word.
            break
    else:
        del current_dict[_end]

但是请注意,这不能确保trie的最小大小。 删除单词后,左侧的树状结构中可能有不再被任何单词使用的分支。 这不会影响数据结构的正确性,仅表示该Trie可能消耗比绝对必要更多的内存。 您可以通过从叶节点向后迭代并删除分支,直到找到具有多个子节点的分支,来改善这一点。

编辑:这是一个想法,您可以如何实现删除功能,该功能还剔除所有不必要的分支。 可能有一种更有效的方法,但这可能会让您入门:

def remove_word2(trie, word):
    current_dict = trie
    path = [current_dict]
    for letter in word:
        current_dict = current_dict.get(letter, None)
        path.append(current_dict)
        if current_dict is None:
            # the trie doesn't contain this word.
            break
    else:
        if not path[-1].get(_end, None):
            # the trie doesn't contain this word (but a prefix of it).
            return
        deleted_branches = []
        for current_dict, letter in zip(reversed(path[:-1]), reversed(word)):
            if len(current_dict[letter]) <= 1:
                deleted_branches.append((current_dict, letter))
            else:
                break
        if len(deleted_branches) > 0:
            del deleted_branches[-1][0][deleted_branches[-1][1]]
        del path[-1][_end]

本质上,它首先找到将要删除的单词的“路径”,然后向后迭代以找到可以删除的节点。 然后,它删除可以删除的路径的根(这也会隐式删除_end节点)。

我认为最好以递归方式进行,代码如下:

def remove(self, word):
    self.delete(self.tries, word, 0)

def delete(self, dicts, word, i):
    if i == len(word):
        if 'end' in dicts:
            del dicts['end']
            if len(dicts) == 0:
                return True
            else:
                return False
        else:
            return False
    else:
        if word[i] in dicts and self.delete(dicts[word[i]], word, i + 1):
            if len(dicts[word[i]]) == 0:
                del dicts[word[i]]
                return True
            else:
                return False

        else:
            return False

处理此类结构的一种方法是通过递归 在这种情况下,关于递归的好处在于,它压缩到了Trie的底部,然后将返回的值通过分支传递回去。

下面的功能就是这样做的。 万一输入的单词是另一个单词的前缀,它将转到叶子并删除_end值。 然后,它传递一个布尔值( boo ),该值指示current_dict仍在外围分支中。 一旦我们达到了当前dict有多个孩子的地步,我们将删除相应的分支并将boo设置为False以便每个剩余的递归都将无效。

def trie_trim(term, trie=SYNONYMS, prev=0):
    # checks that we haven't hit the end of the word
    if term:
        first, rest = term[0], term[1:]
        current_length = len(trie)
        next_length, boo = trie_trim(rest, trie=trie[first], prev=current_length)

        # this statement avoids trimming excessively if the input is a prefix because
        # if the word is a prefix, the first returned value will be greater than 1
        if boo and next_length > 1:
            boo = False

        # this statement checks for the first occurrence of the current dict having more than one child
        # or it checks that we've hit the bottom without trimming anything
        elif boo and (current_length > 1 or not prev):
            del trie[first]
            boo = False

        return current_length, boo

    # when we do hit the end of the word, delete _end
    else:
        del trie[_end]
        return len(trie) + 1, True
def remove_a_word_util(self, word, idx, node):
    if len(word) == idx:
        node.is_end_of_word = False
        return bool(node.children)

    ch = word[idx]
    if ch not in node.children:
        return True

    flag = self.remove_a_word_util(word, idx+1, node.children[ch])
    if flag:
        return True

    node.children.pop(ch)
    return bool(node.children) or node.is_end_of_word

有点长,但我希望这有助于回答您的问题:

class Trie:
    WORD_END = "$"
    
    def __init__(self):
        self.trie = {}

    def insert(self, word):
        cur = self.trie
        for char in word:
            if char not in cur:
                cur[char] = {}
            cur = cur[char]
        cur[Trie.WORD_END] = word

    def delete(self, word):
        def _delete(word, cur_trie, i=0):
            if i == len(word):
                if Trie.WORD_END not in cur_trie:
                    raise ValueError("'%s' is not registered in the trie..." %word)
                cur_trie.pop(Trie.WORD_END)
                if len(cur_trie) > 0:
                    return False
                return True
            if word[i] not in cur_trie:
                raise ValueError("'%s' is not registered in the trie..." %word)
            cont = _delete(word, cur_trie[word[i]], i+1)
            if cont:
                cur_trie.pop(word[i])
                if Trie.WORD_END in cur_trie:
                    return False
                return True
            return False
        _delete(word, self.trie)

t = Trie()
t.insert("bar")
t.insert("baraka")
t.insert("barakalar")

t.delete("barak") # raises error as 'barak' is not a valid WORD_END although it is a valid path.
t.delete("bareka") # raises error as 'e' does not exist in the path.
t.delete("baraka") # deletes the WORD_END of 'baraka' without deleting any letter as there is 'barakalar' afterwards.
t.delete("barakalar") # deletes until the previous word (until the first Trie.WORD_END; "$" - by going backwards with recursion) in the same path (until 'baraka').

如果您需要整个 DS:

class TrieNode:
    def __init__(self):
        self.children = {}
        self.wordCounter = 0
        self.prefixCounter = 0

class Trie:
    def __init__(self):
        self.root = TrieNode()

    def insert(self, word: str) -> None:
        node = self.root
        for char in word:
            if char not in node.children:
                node.children[char] = TrieNode()
            
            node.prefixCounter += 1                  
            node = node.children[char] 

        node.wordCounter += 1

    def countWordsEqualTo(self, word: str) -> int:
        node = self.root
        if node.children:
            for char in word:
                node = node.children[char]               
        else:
            return 0
            
        return node.wordCounter
 
    def countWordsStartingWith(self, prefix: str) -> int:
        node = self.root
        if node.children:
            for char in prefix:
                node = node.children[char]               
        else:
            return 0

        return node.prefixCounter

    def erase(self, word: str) -> None:
        node = self.root
        for char in word:
            if node.children:
                node.prefixCounter -= 1
                node = node.children[char]
            else:
                return None

        node.wordCounter -= 1

        if node.wordCounter == 0:
            self.dfsRemove(self.root, word, 0)

    def dfsRemove(self, node: TrieNode, word: str, idx: int) -> None:
        if len(word) == idx:
            node.wordCounter = 0
            return

        char = word[idx]
        if char not in node.children:
            return

        self.dfsRemove(node.children[char], word, idx+1)
        
        node.children.pop(char)
        
            



trie = Trie();
trie.insert("apple");                     #// Inserts "apple".
trie.insert("apple");                     #// Inserts another "apple".
print(trie.countWordsEqualTo("apple"))    #// There are two instances of "apple" so return 2.
print(trie.countWordsStartingWith("app")) #// "app" is a prefix of "apple" so return 2.
trie.erase("apple")                       #// Erases one "apple".
print(trie.countWordsEqualTo("apple"))    #// Now there is only one instance of "apple" so return 1.
print(trie.countWordsStartingWith("app")) #// return 1
trie.erase("apple");                      #// Erases "apple". Now the trie is empty.
print(trie.countWordsEqualTo("apple"))    #// return 0
print(trie.countWordsStartingWith("app")) #// return 0

我认为这个实现是最简洁和最容易理解的。

        def removeWord(word, node=None):
            if not node:
                node = self.root
            if word == "":
                node.isEnd = False
                return

            newnode = node.children[word[0]]
            removeWord(word[1:], newnode)
            if not newnode.isEnd and len(newnode.children) == 0:
                del node.children[word[0]]

虽然一开始使用默认参数node=None有点难以理解,但这是处理标记单词node.isEnd = False同时修剪无关节点的 Trie 删除的最简洁实现。

  • 该方法首先被称为Trie.removeWord("ToBeDeletedWord")
  • 在随后的递归调用中,与相应字母(“T”然后“o”然后“B”然后“e”等)绑定的节点被添加到下一个递归(例如“删除'oBeDeletedWord',节点位于T")。
  • 一旦我们到达具有完整字符串ToBeDeletedWord的结束节点,最后一次递归调用removeWord("", <node d>)
  • 在最后一次递归调用中,我们标记node.isEnd = False 稍后,该节点不再被标记为isEnd并且它没有子节点,因此我们可以调用删除运算符。
  • 一旦最后一次递归调用结束,递归的 rest(例如TobeDeletedWorTobeDeletedWoTobeDeletedW等)将观察到它也不是结束节点并且没有更多子节点。 这些节点也将被删除。

您将不得不阅读此文几次,但此实现简洁、易读且正确。 困难在于递归发生在函数中间,而不是在开始或结束时。

TL;DR

class TrieNode:
    children: dict[str, "TrieNode"]

    def __init__(self) -> None:
        self.children = {}
        self.end = False

    def __contains__(self, char: str) -> bool:
        return char in self.children

    def __getitem__(self, __name: str) -> "TrieNode":
        return self.children[__name]

    def __setitem__(self, __name: str, __value: "TrieNode") -> None:
        self.children[__name] = __value

    def __len__(self):
        return len(self.children)

    def __delitem__(self, __name: str):
        del self.children[__name]


class Trie:
    def __init__(self, words: list[str]) -> None:
        self.root = TrieNode()
        for w in words:
            self.insert(w)

    def insert(self, word: str):
        curr = self.root
        for c in word:
            curr = curr.children.setdefault(c, TrieNode())
        curr.end = True

    def remove(self, word: str):
        def _remove(node: TrieNode, index: int):
            if index >= len(word):
                node.end = False
                if not node.children:
                    return True

            elif word[index] in node:
                if _remove(node[word[index]], index + 1):
                    del node[word[index]]

        _remove(self.root, 0)

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM