【问题标题】:Autocomplete Style Prefix Lookup自动完成样式前缀查找
【发布时间】:2023-07-06 09:03:01
【问题描述】:

举个具体的例子:

  • 您有一份美国每个名字的列表。
  • 您希望在 GUI 中自动建议完成。

显而易见的事情是使用基数树来获取给定前缀的名称列表。但是,这没有考虑频率信息。因此,我想要最常见的 5 个名称,而不是仅将前 5 个结果作为第一个词汇结果:

例如对于前缀dan

 (5913, 'Daniel')
 (889, 'Danny')
 (820, 'Dana')
 (272, 'Dan')
 (60, 'Dane')

有没有我错过的特里树算法?当然,我认为理想的实现(如果存在的话)是在 python 中。

更新:总体上对 Paddy3113 的提议感到满意,尽管我会说当我向它提供 2.6GB 文件时它完全爆炸了,这是我正在减少的文件之一。查看详细信息,输出给出了一些见解:

samz;Samzetta|Samzara|Samzie
samza;Samzara
samzar;Samzara
samzara;Samzara
samze;Samzetta
samzet;Samzetta
samzett;Samzetta
samzetta;Samzetta
samzi;Samzie
samzie;Samzie

# Format - PREFIX;"|".join(CHOICES).

我们在赏金方面还有几天的时间,所以我仍在寻找杀手级解决方案。因为这不仅与减少有关,还与事物的查找有关。

【问题讨论】:

  • 你有相关频率的样本名单吗?
  • 如果它用 2.5G 文件轰炸是因为 python 存储了很多额外的信息并且内存无法容纳它。我通过在“c”中实现 trie 类解决了同样的问题(trie-spellchecker),然后将其设为“.so”并将其链接到 python 并使用它..

标签: python algorithm trie prefix-tree


【解决方案1】:

您基本上可以扩充一个 trie 实现,以按流行度顺序而不是字母顺序存储它的子节点,也就是说,您还必须在 trie 的每个节点中存储流行度。

【讨论】:

  • 我已经考虑过了,但想知道它是否在某个地方更正式。
【解决方案2】:

这里有一个关于如何做到这一点的想法:

构造一个字符串 trie 并在树中的每个节点存储一个整数。此节点指示使用该节点的名称数。因此,当将该名称插入到 trie 中时,您将递增该名称的所有节点。

然后,您可以通过贪婪地选择具有最高值的名称来确定最高名称。

形式上它与任何字符串 trie 构造算法相同,但增加了递增整数的步骤。

【讨论】:

    【解决方案3】:

    在没有任何关于调整的想法的情况下,我首先假设我有一个名称及其频率列表,然后构造一个字典,将前缀映射到具有该前缀的一组名称,然后将每个集合转换为仅包含顶部的列表5个名字频率。

    使用从here 得到的男孩名字的列表来创建一个text file,其中每行是一个整数出现频率,一些空格,然后是这样的名字:

    8427    OLIVER 
    7031    JACK 
    6862    HARRY 
    5478    ALFIE 
    5410    CHARLIE 
    5307    THOMAS 
    5256    WILLIAM 
    5217    JOSHUA 
    4542    GEORGE 
    4351    JAMES 
    4330    DANIEL 
    4308    JACOB 
    ...
    

    以下代码构造字典:

    from collections import defaultdict
    
    MAX_SUGGEST = 5
    
    def gen_autosuggest(name_freq_file_name):
        with open(name_freq_file_name) as f:
            name2freq = {}
            for nf in f:
                freq, name = nf.split()
                if name not in name2freq:
                    name2freq[name] = int(freq)
        pre2suggest = defaultdict(list)
        for name, freq in sorted(name2freq.items(), key=lambda x: -x[1]):
            # in decreasing order of popularity
            for i, _ in enumerate(name, 1):
                prefix = name[:i]
                pre2suggest[prefix].append((name, name2freq[name]))
        # set max suggestions
        return {pre:namefs[:MAX_SUGGEST]
                for pre, namefs in pre2suggest.items()}
    
    if __name__ == '__main__':
        pre2suggest = gen_autosuggest('2010boysnames_popularity_engwales2.txt')
    

    如果你给字典你的前缀,那么它会返回你的建议(在这种情况下连同它们的频率,但如果需要,可以丢弃这些:

    >>> len(pre2suggest)
    15303
    >>> pre2suggest['OL']
    [('OLIVER', 8427), ('OLLIE', 1130), ('OLLY', 556), ('OLIVIER', 175), ('OLIWIER', 103)]
    >>> pre2suggest['OLI']
    [('OLIVER', 8427), ('OLIVIER', 175), ('OLIWIER', 103), ('OLI', 23), ('OLIVER-JAMES', 16)]
    >>> 
    

    别试了 :-)

    时间杀手

    如果运行需要很长时间,那么您可以预先计算 dict 并将其保存到文件中,然后在需要时使用 pickle 模块加载预先计算的值:

    >>> import pickle
    >>> 
    >>> savename = 'pre2suggest.pcl'
    >>> with open(savename, 'wb') as f:
        pickle.dump(pre2suggest, f)
    
    
    >>> # restore it
    >>> with open(savename, 'rb') as f:
        p2s = pickle.load(f)
    
    
    >>> p2s == pre2suggest
    True
    >>> 
    

    【讨论】:

    • 我查看了来自 koblas 的 FirstLetterS - 它是在我提交后发布的。我认为通过将文件格式从 name 然后 frq 交换为 freq 然后 name 然后将名称大写然后我上面示例的内容将与他的数据一起使用。
    • 它确实有效。虽然在真正的 1.3G 数据集上启动需要 3 分钟以上。我们能做得更好吗?因为我需要同时加载一个 3G 数据集、1.3G 数据集和一个 1G 数据集。
    • 已编辑为使用更像来自 koblas 的名称列表,并添加了指向我的输入数据的链接
    • 已编辑以添加有关酸洗的信息以节省时间(如果名称列表在调用之间相当静态)。如果名称列表有所不同,但您知道已更改的名称频率(基本上更小),那么我将腌制 name2freq 并阅读更改列表。
    • 已经做了泡菜位...转移到 sqlite 以减少启动时间。仍然希望比预先计算的字典更好。
    【解决方案4】:

    是的,我们可以使用 trie。 trie 节点最常见的名称是 (1) 该 trie 节点的名称或 (2) 该 trie 节点的子节点的最常见名称。这里有一些 Python 代码可供使用。

    from collections import defaultdict
    
    
    class trie:
        __slots__ = ('children', 'freq', 'name', 'top5')
    
        def __init__(self):
            self.children = defaultdict(trie)
            self.freq = 0
            self.name = None
            self.top5 = []
    
        def __getitem__(self, suffix):
            node = self
            for letter in suffix:
                node = node.children[letter]
            return node
    
        def computetop5(self):
            candidates = []
            for letter, child in self.children.items():
                child.computetop5()
                candidates.extend(child.top5)
            if self.name is not None:
                candidates.append((self.freq, self.name))
            candidates.sort(reverse=True)
            self.top5 = candidates[:5]
    
        def insert(self, freq, name):
            node = self[name]
            node.freq += freq
            node.name = name
    
    
    root = trie()
    with open('letter_s.txt') as f:
        for line in f:
            freq, name = line.split(None, 1)
            root.insert(int(freq.strip()), name.strip())
    root.computetop5()
    print(root['St'].top5)
    

    【讨论】:

    • 这非常优雅......而且,它的运行速度比 dict 版本快约 2.5 倍。值得认真考虑。
    • 我还没有写,但是如果你想在一个节点更新频率(或插入一个新名称),你可以在该节点及其祖先上重新运行一个非递归版本的 computetop5 ,应该很快。
    • 我的测试实现还将 top5 转换为按需计算的属性 - 这有助于预计算。
    【解决方案5】:

    如果您想要快速查找,唯一真正的解决方案是预先计算任何给定前缀的答案。这在数据不变的情况下很好,但您需要一种方法来保持较短的加载时间。

    我建议使用 DBM 来存储预先计算的字典。这基本上是一个字典,其中的内容存储在磁盘上,并在您引用项目时进行查找。有关详细信息,请参阅http://docs.python.org/library/anydbm.html。唯一的缺点是值必须是字符串,因此您需要存储前 5 个条目的逗号分隔列表,并在查找时将其拆分。

    这将比 pickle 具有更快的启动时间,因为不需要加载数据库。它也比使用 sqlite 简单得多。

    【讨论】: