实现用于字典的帕特里夏树

11 投票
2 回答
8639 浏览
提问于 2025-04-15 20:11

我正在尝试实现一个叫做Patricia Trie的数据结构,主要用来存储一个大的单词字典,以便快速查找(包括前缀搜索)。我已经了解了一些相关的概念,但还是不太明白怎么去实现它。我想知道(用Java或Python代码)如何实现这个Trie,特别是节点的部分(或者我应该用递归来实现吗)。我看到有人用一个包含26个子节点的数组来实现,每个子节点都设置为null/None。有没有更好的方法(比如把字母当作二进制位处理),如果有的话,应该怎么实现呢?

2 个回答

3

这里有一个使用更符合Python风格的递归实现:

def matching_prefix_index(word1, word2):
    max_len = min(len(word1),len(word2))
    for i in range(max_len):
        if word2[i] != word1[i]:
            return i
    return max_len

class PatriciaTrie(object):
    def __init__(self):
        self._storage = {}
        self._complete_prefix_flag = False

    def _find_storage_key(self, word):
        for key in self._storage:
            prefix_index = matching_prefix_index(key, word)
            if prefix_index > 0:
                return (key, prefix_index)
        return (None, None)

    def add(self, word):
        if word == '':
            self._complete_prefix_flag = True
            return True

        key, prefix_index = self._find_storage_key(word)
        if key is not None:
            if prefix_index == len(key):
                return self._storage[key].add(word[len(key):])
            else:
                new_tree = PatriciaTrie()
                new_tree._storage[key[prefix_index:]] = self._storage.pop(key)
                self._storage[key[0:prefix_index]] = new_tree
                return new_tree.add(word[prefix_index:])
        else:
            self._storage[word] = PatriciaTrie()
            self._storage[word].add('')
            return True

    def remove(self, word):
        if word == '':
            self._complete_prefix_flag = False
            return True

        key, prefix_index = self._find_storage_key(word)
        if key is None or prefix_index != len(key):
            return False

        subword = word[prefix_index:]
        subtrie = self._storage[key]
        if subtrie.remove(subword):
            if (not subtrie._complete_prefix_flag) and len(subtrie._storage) == 0:
                self._storage.pop(key)
            return True
        else:
            return False

    def __contains__(self, word):
        if word == '':
            return self._complete_prefix_flag

        key, prefix_index = self._find_storage_key(word)
        if key is None or prefix_index != len(key):
            return False
        else:
            return (word[prefix_index:] in self._storage[key])

    def has_prefix(self, word):
        if word == '':
            return True

        key, prefix_index = self._find_storage_key(word)
        if key is None:
            return False
        elif len(key) > len(word):
            return (prefix_index == len(word))
        elif len(key) != prefix_index:
            return False
        else:
            return self._storage[key].has_prefix(word[prefix_index:])
13

之前有人问过关于Patricia树的问题,我当时想过用Python来实现一个,但这次我决定真的试试看(是的,这可能有点过于复杂,但我觉得这是个不错的小项目)。我做的可能不是一个纯粹的Patricia树实现,但我更喜欢我自己的方式。其他语言中的Patricia树通常用列表来存储子节点,然后检查每个子节点是否匹配,但我觉得这样效率不高,所以我用了字典。下面是我大致的设置方式:

我从根节点开始。根节点就是一个字典。这个字典的键是所有单个字符(单词的第一个字母),每个键对应一个分支。与每个键对应的值是一个列表,列表的第一个项目是一个字符串,表示与这个分支匹配的其余部分,第二个项目是一个字典,指向这个节点的进一步分支。这个字典也有单个字符的键,代表单词其余部分的第一个字母,处理过程就这样继续往下进行。

还有一点我应该提到的是,如果某个节点有分支,但它本身也是树中的一个单词,那么这个节点的字典中会有一个''键,指向一个包含列表['',{}]的节点。

这里有个小例子,展示了单词是如何存储的(根节点是变量_d):

>>> x = patricia()
>>> x.addWord('abcabc')
>>> x._d
{'a': ['bcabc', {}]}
>>> x.addWord('abcdef')
>>> x._d
{'a': ['bc', {'a': ['bc', {}], 'd': ['ef', {}]}]}
>>> x.addWord('abc')
{'a': ['bc', {'a': ['bc', {}], '': ['', {}], 'd': ['ef', {}]}]}

注意在最后的例子中,字典中添加了一个''键,以表示'abc'也是一个单词,除了'abcdef'和'abcabc'之外。

源代码

class patricia():
    def __init__(self):
        self._data = {}

    def addWord(self, word):
        data = self._data
        i = 0
        while 1:
            try:
                node = data[word[i:i+1]]
            except KeyError:
                if data:
                    data[word[i:i+1]] = [word[i+1:],{}]
                else:
                    if word[i:i+1] == '':
                        return
                    else:
                        if i != 0:
                            data[''] = ['',{}]
                        data[word[i:i+1]] = [word[i+1:],{}]
                return

            i += 1
            if word.startswith(node[0],i):
                if len(word[i:]) == len(node[0]):
                    if node[1]:
                        try:
                            node[1]['']
                        except KeyError:
                            data = node[1]
                            data[''] = ['',{}]
                    return
                else:
                    i += len(node[0])
                    data = node[1]
            else:
                ii = i
                j = 0
                while ii != len(word) and j != len(node[0]) and \
                      word[ii:ii+1] == node[0][j:j+1]:
                    ii += 1
                    j += 1
                tmpdata = {}
                tmpdata[node[0][j:j+1]] = [node[0][j+1:],node[1]]
                tmpdata[word[ii:ii+1]] = [word[ii+1:],{}]
                data[word[i-1:i]] = [node[0][:j],tmpdata]
                return

    def isWord(self,word):
        data = self._data
        i = 0
        while 1:
            try:
                node = data[word[i:i+1]]
            except KeyError:
                return False
            i += 1
            if word.startswith(node[0],i):
                if len(word[i:]) == len(node[0]):
                    if node[1]:
                        try:
                            node[1]['']
                        except KeyError:
                            return False
                    return True
                else:
                    i += len(node[0])
                    data = node[1]
            else:
                return False

    def isPrefix(self,word):
        data = self._data
        i = 0
        wordlen = len(word)
        while 1:
            try:
                node = data[word[i:i+1]]
            except KeyError:
                return False
            i += 1
            if word.startswith(node[0][:wordlen-i],i):
                if wordlen - i > len(node[0]):
                    i += len(node[0])
                    data = node[1]
                else:
                    return True
            else:
                return False

    def removeWord(self,word):
        data = self._data
        i = 0
        while 1:
            try:
                node = data[word[i:i+1]]
            except KeyError:
                print "Word is not in trie."
                return
            i += 1
            if word.startswith(node[0],i):
                if len(word[i:]) == len(node[0]):
                    if node[1]:
                        try:
                            node[1]['']
                            node[1].pop('')
                        except KeyError:
                            print "Word is not in trie."
                        return
                    data.pop(word[i-1:i])
                    return
                else:
                    i += len(node[0])
                    data = node[1]
            else:
                print "Word is not in trie."
                return


    __getitem__ = isWord

你可能注意到最后我把__getitem__设置成了isWord方法。这意味着

x['abc']

会返回'abc'是否在树中。

我觉得也许我应该把这个做成一个模块,提交到PyPI上,但它需要更多的测试,至少还需要一个removeWord方法。如果你发现任何bug,请告诉我,但它似乎运行得相当不错。另外,如果你发现有什么效率上的大改进,我也很想听听。我考虑过在每个分支的底部处理空字典的事情,但我现在先不做。这些空字典可以用与单词相关的数据来替换,以扩展这个实现的用途。

总之,如果你不喜欢我实现的方式,至少希望这能给你一些灵感,让你想出自己想要的实现版本。

撰写回答