【问题标题】:More efficient way to look up dictionary values whose keys start with same prefix查找键以相同前缀开头的字典值的更有效方法
【发布时间】:2015-08-05 19:33:04
【问题描述】:

我有一个字典,它的键来自共享相同前缀的集合,如下所示:

d = { "key1":"valA", "key123":"valB", "key1XY":"valC",
      "key2":"valD", "key2-22":"valE" }

给定一个查询字符串,我需要查找与以该前缀开头的键关联的所有值,例如对于query="key1",我需要得到["valA", "valB", "valC"]

我下面的实现可以工作,但是对于大量查询来说太慢了,因为字典 d 有大约 30,000 个键,并且大多数键的长度超过 20 个字符:

result = [d[s] for s in d.keys() if s.startswith(query)]

有没有更快/更有效的方法来实现这一点?

【问题讨论】:

  • 我不这么认为 - 键是散列的,这意味着 key1 和 key12 的键在查找表中可能具有完全不同的表示形式。
  • @PadraicCunningham 除非是Python3,否则keys 实际上会返回一个迭代器,如果我没记错的话.. 就像Python2 的ikeys
  • 如果你想要效率,那就选择数据库引擎(例如 Mysql),这些引擎旨在尽可能高效地检索数据。
  • 让我们知道哪种方法在您的情况下更快——您可以post your own answer with the comparison

标签: python performance dictionary lookup startswith


【解决方案1】:

您可以避免生成由dict.keys() 生成的中间列表(在python 2.x 中):

result = [d[key] for key in d if key.startswith(query)]

但您很可能希望使用trie 而不是字典,这样您就可以找到与具有公共前缀的键关联的所有值(trie 类似于基于前缀的树)。

Here你可以找到一些不同的尝试实现。

键“A”、“to”、“tea”、“ted”、“ten”、“i”、“in”和“inn”的尝试。 (来源wikipedia)


让我们比较一下不同解决方案的时间安排:

# create a dictionary with 30k entries
d = {str(x):str(x) for x in xrange(1, 30001)}
query = '108'

# dict with keys()
%timeit [d[s] for s in d.keys() if s.startswith(query)]

    100 loops, best of 3: 8.87 ms per loop

# dict without keys()
%timeit [d[s] for s in d if s.startswith(query)]

    100 loops, best of 3: 7.83 ms per loop

# 11.72% improvement

# PyTrie (https://pypi.python.org/pypi/PyTrie/0.2)
import pytrie
pt = pytrie.Trie(d)

%timeit [pt[s] for s in pt.iterkeys(query)]

    1000 loops, best of 3: 320 µs per loop

# 96.36% improvement

# datrie (https://pypi.python.org/pypi/datrie/0.7)
import datrie
dt = datrie.Trie('0123456789')
for key, val in d.iteritems():
    dt[unicode(key)] = val

%timeit [dt[s] for s in dt.keys(unicode(query))]

    10000 loops, best of 3: 162 µs per loop

# 98.17% improvement

【讨论】:

  • 在 dict 方法中,我会坚持推迟值访问,直到确认密钥有效(即,[d[s] for s in d if s.startswith(...)])。
  • @chepner:是的,我认为它可能会更有效,特别是如果像预期的那样没有很多匹配项。
  • 您的比较不包括 trie 构建时间或它需要多少内存。可以添加与SuffixTree.SubstringDict(b'0123456789') 的比较吗?这是comparison for a similar problem
【解决方案2】:

sortedContainers 库有一个 SortedDict 实现,一旦你对 dict 进行了排序,你可以 bisect_left 找到从哪里开始, bisect_right 找到最后一个位置,然后使用 irange 获取范围内的键:

from sortedcontainers import SortedDict
from operator import itemgetter
from itertools import takewhile


d = { "key1":"valA", "key123":"valB", "key1XY":"valC",
  "key2":"valD", "key2-22":"valE","key3":"foo" }

key = "key2"
d = SortedDict(sorted(d.items(), key=itemgetter(0)))
start = d.bisect_left(key)
print([d[key] for key in takewhile(lambda x: x.startswith("key2"), d.irange(d.iloc[start]]))
['valD', 'valE']

一旦你使用 sorteddict 维护一个 sorteddict 会更有效:

In [68]: l = ["key{}".format(randint(1,1000000)) for _ in range(100000)] 
In [69]: l.sort()    
In [70]: d = SortedDict(zip(l,range(100000)))

In [71]: timeit [d[s] for s in d.keys() if s.startswith("key2")]
10 loops, best of 3: 124 ms per loop

In [72]: timeit [d[s] for s in d if s.startswith("key2")]
10 loops, best of 3: 24.6 ms per loop

In [73]: %%timeit
key = "key2"
start = d.bisect_left(key)
l2 =[d[k] for k in takewhile(lambda x: x.startswith("key2"),d.irange(d.iloc[start]))]
   ....: 

100 loops, best of 3: 5.57 ms per loop

【讨论】:

    【解决方案3】:

    你可以使用suffix tree:

    #!/usr/bin/env python2
    from SuffixTree import SubstringDict # $ pip install https://github.com/JDonner/SuffixTree/archive/master.zip
    
    d = { "key1":"valA", "key123":"valB", "key1XY":"valC",
          "key2":"valD", "key2-22":"valE" }
    
    a = '\n' # anchor
    prefixes = SubstringDict()
    for key, value in d.items(): # populated the tree *once*
        prefixes[a + key] = value # assume there is no '\n' in key
    
    for query in ["key1", "key2"]: # perform queries
        print query, prefixes[a + query]
    

    输出

    key1 ['valC', 'valA', 'valB']
    key2 ['valE', 'valD']
    

    【讨论】:

    • 这个特殊的后缀树的作者已经宣布他的项目已经死了,并评论说“Python 中有很多其他的后缀树实现”。麻烦的是,到目前为止我尝试过的那些并没有公开一个方便的类似 dict 的对象。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-02-22
    • 1970-01-01
    • 2014-03-02
    • 2019-07-29
    相关资源
    最近更新 更多