Python字典键值. "In"操作的时间复杂度

84

我有一个好奇的问题,主要是想满足自己对这个主题的好奇心。

我正在编写一些使用SQlite数据库后端的大型Python程序,并将来会处理大量记录,因此我需要尽可能地进行优化。

对于一些函数,我正在搜索字典中的键。我一直在使用“in”关键字进行原型设计,并计划稍后返回并优化这些搜索,因为我知道“in”关键字通常是O(n)(因为这只是将Python迭代整个列表并比较每个元素)。 但是,由于Python字典基本上只是一个哈希映射,那么Python解释器是否足够聪明以解释:

if(key in dict.keys()):
    ...code...

致:

if(dict[key] != None):
    ...code...

本质上,这是相同的操作,但是顶部的时间复杂度为O(n),底部的时间复杂度为O(1)。

对我来说,在我的代码中使用底部版本很容易,但我只是好奇想问一下。


我认为先做最容易的,然后再进行性能分析。 - jh314
3
实际上,底部的代码不起作用。您必须执行类似于try:dict [key];except KeyError:pass;else:#...code ...的操作。 - Travis DePrato
@TravisGD 这是一个很好的观点,我忘记了那个。 - tknickman
6
附带说明:不要在无必要的情况下将if条件用括号括起来。熟练阅读和编写Python代码的人会认为括号代表某些意义,如元组、生成器表达式或者覆盖运算符优先级,他们会不得不停下来读两遍以确保您的括号实际上并没有表示任何东西。 简化翻译:不必要地给if语句加括号会让人感到困惑,因为读写Python的有经验的人会认为括号代表了一些含义,这可能导致需要反复阅读才能理解代码的真正含义。 - abarnert
2
另外一点需要注意:不要把字典命名为 dict,因为这会隐藏同名类型和构造函数,而你在后面可能还会用到它们。 - abarnert
显示剩余2条评论
5个回答

159

首先,对于任何字典 d,在调用 d.keys() 和使用 key in d 进行判断时,返回的结果是相同的。

在字典或者字典对象通过调用 keys() 返回的 dict_keys 对象上执行 in 操作(在 3.x 版本中),时间复杂度不是 O(N),而是 O(1)。

并没有真正的“优化”操作;在哈希表上实现 __contains__ 的明显方式就是使用哈希,正如实现 __getitem__ 的明显方式一样。


你可能会问这是怎么保证的。

事实上并没有保证。在 Mapping Types 中,dict 被定义为基于 collections.abc.Mapping 实现的哈希表。虽然也可以创建一个哈希表实现的 Mapping,但是提供 O(N) 搜索需要额外的工作,那么为什么要这样做呢?

如果你真的需要验证这个结论,你可以测试每个你关心的实现(使用分析器或者使用自定义的 __hash____eq__ 记录函数调用次数,等等),或者阅读源代码。


在 2.x 版本中,你不应该调用 keys,因为这将生成一个键的列表而不是 KeysView。你可以使用 iterkeys,但是它可能会生成一个迭代器或其他不是 O(1) 的东西。所以,只需将字典本身作为序列使用。

即使在 3.x 版本中,也没有必要调用 keys。遍历字典、检查其 __contains__,并一般地把它当作序列来对待,总是等价于对其键做同样的事情,那么为什么还要麻烦呢?(当然,构建简单的 KeyView,并通过它进行访问,会增加几个纳秒的运行时间和一些程序代码。)

(虽然使用序列操作在 2.x 中的 d.keys()/d.iterkeys()d 等价于性能问题,但它们在每个 CPython、Jython、IronPython 和 PyPy 实现中都是等价的,但似乎没有像在 3.x 中那样声明。并且这也无关紧要;只需使用 key in d 即可。)


顺带一提,这个式子:

if(dict[key] != None):

如果在字典中没有找到 key,这样写会抛出 KeyError 而不是返回 None。因此这种方法行不通。

同时,你永远不应该使用 == 或者 != 来检查 None;而应该始终使用 is

你可以用 try 来做到这一点,或者更简单地使用 if dict.get(key, None) is not None。但是,没有任何理由这样做。还有一种情况需要考虑,即 None 是一个完全有效的项。在这种情况下,你需要像这样做: sentinel = object(); if dict.get(key, sentinel) is not sentinel:


因此,正确的写法是:

if key in d:

更普遍地说,这并不是真的:

我知道 "in" 关键字通常是O(n)(因为它只是将 Python 迭代整个列表并比较每个元素)

in 运算符像大多数其他运算符一样,只是调用了一个 __contains__ 方法(或者对于 C/Java/.NET/RPython 内置的等效方法)。list 通过迭代列表并比较每个元素来实现它;dict 通过哈希值查找来实现它;blist.blist 通过遍历 B+树 来实现它;等等。所以,它可能是 O(n)、O(1)、O(log n),或者完全不同。


这正是我所想的,这个有文档记录吗?我不确定,因为我认为dict.keys()可能只返回一个列表。这会使“in”成为O(n)。 - tknickman
1
总的来说,Python不会记录其函数的性能特征。(部分原因是因为您始终可以做一些荒谬的事情,比如定义一个依赖于元素数量的哈希函数。)所以,这就是您得到的全部内容。但是,它记录字典是哈希表的事实,相当强烈地暗示了“key in d”、“d[key]”和“d.get(key)”都将是O(1)。 - abarnert
4
平均情况下的时间复杂度为O(1),最坏情况下的时间复杂度为O(n)。 - Steven Rumbalski
2
@AshwiniChaudhary:它们在语义上保证是相等的。在Python 3.x中,它们在性能方面也是相等的。在Python 2.x中,keys显然会慢一些。我已经编辑了答案以提供更多细节。但真正的问题是,从来没有理由使用key in d.keys(),所以你不必记住细节。 - abarnert
如果key in dict不可用,我会尝试第一个备选方案,即使用*dict.has_key()*而不是自己编写的异常分支。 - guidot

18

在Python 2中,dict.keys() 首先创建整个键列表,因此它是一个 O(N) 操作,而 key in dict 是一个 O(1) 操作。

if(dict[key] != None) 如果在字典中找不到键会引发 KeyError ,因此它与第一个代码不等价。

Python 2 结果:

>>> dic = dict.fromkeys(range(10**5))
>>> %timeit 10000 in dic
1000000 loops, best of 3: 170 ns per loop
>>> %timeit 10000 in dic.keys()
100 loops, best of 3: 4.98 ms per loop
>>> %timeit 10000 in dic.iterkeys()
1000 loops, best of 3: 402 us per loop
>>> %timeit 10000 in dic.viewkeys()
1000000 loops, best of 3: 457 ns per loop

在Python 3中,dict.keys()返回一个视图对象,比Python 2的keys()要快得多,但仍比简单的正常key in dict慢:

Python 3结果:

>>> dic = dict.fromkeys(range(10**5))
>>> %timeit 10000 in dic
1000000 loops, best of 3: 295 ns per loop
>>> %timeit 10000 in dic.keys()
1000000 loops, best of 3: 475 ns per loop

只需使用:

if key in dict:
   #code

这是特定于2.x版本的。(另外,请注意在CPython 2.7.3或PyPy 2.0b1中,iterkeys可能比keys快得多 - Python 2.x允许iterkeys成为比iter(d.keys())更智能的东西,并且它们实际上确实有一些优势。但它仍然远不及直接使用d快。在我的电脑上,它是94ns vs. 338us vs. 2.03ms。) - abarnert

11

正确的做法是这样的

if key in dict:
    do stuff

在Python中,对于字典和集合,in操作符的时间复杂度是O(1)。


1
字典的in运算符平均时间复杂度为O(1)。有关其他dict()方法的时间复杂度的详细信息,请访问此 link

3
虽然该链接可能回答了问题,但最好在这里包含回答的关键部分并提供链接作为参考。仅有链接的答案可能会在链接页面发生变化时失效。 - Kenzo_Gilead

-1

试试这个 - 不会引发任何异常,时间复杂度为O(1)

if myDict.get(key, None) is not None:
    # key is present

你的语法有误。你应该使用括号而不是方括号。 - chatax

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接