set()如何实现?


151

我见过有人说setpython 中的对象具有O(1)成员资格检查。如何在内部实现它们以允许这样做?它使用哪种数据结构?该实现还有什么其他含义?

这里的每个答案都非常有启发性,但是我只能接受一个答案,因此,我将选择与原始问题最接近的答案。谢谢你的信息!

Answers:


139

根据这个线程

实际上,CPython的集合被实现为类似于带有伪值的字典(键是集合的成员)的字典,并且进行了一些优化,可以利用这种缺乏值的方式

因此,基本上a set使用哈希表作为其基础数据结构。这解释了O(1)成员资格检查,因为在哈希表中查找项目平均而言是O(1)操作。

如果您愿意,甚至可以浏览CPython源代码以获取集合,根据Achim Domma的说法,该代码大部分是实现中的剪切和粘贴dict


18
IIRC,最初的set实现实际上 dict使用伪值,后来进行了优化。
dan04

1
大O不是最坏的情况吗?如果您可以找到一个实例,其中时间为O(n),那么它就是O(n)。.我现在对所有这些教程都不了解。
Claudiu Creanga

4
不,对于哈希表查找,平均情况为O(1),但最差情况为O(N)。
贾斯汀·埃斯蒂尔16/09/22

4
@ClaudiuCreanga这是一个旧评论,但只是为了澄清:big-O表示法告诉您事物增长率的上限,但是您可以将平均案例性能的增长率上限,也可以将最坏情况的增长率分别上限性能。
柯克·博耶

79

当人们说集合具有O(1)成员资格检查时,他们正在谈论平均情况。在最坏的情况下(当所有哈希值冲突时),成员资格检查为O(n)。有关时间复杂性,请参见Python Wiki

维基百科的文章说,最好的情况下为一个哈希表,不调整大小的时间复杂度O(1 + k/n)。由于Python集使用调整大小的哈希表,因此该结果并不直接适用于Python集。

在Wikipedia文章上再说一点,对于一般情况,并假设一个简单的统一哈希函数,时间复杂度为O(1/(1-k/n)),其中k/n可以由常数限制c<1

Big-O仅将渐近行为表示为n→∞。由于k / n可以由常数c <1限制,与n无关

O(1/(1-k/n))不大于O(1/(1-c))等于O(constant)= O(1)

因此,假设统一的简单哈希,平均而言,Python集的成员资格检查为O(1)


14

我认为这是一个常见的错误,set查找(或该问题的哈希表)不是O(1)。
来自维基百科

在最简单的模型中,哈希函数是完全未指定的,并且该表不会调整大小。为了最好地选择散列函数,大小为n且具有开放寻址的表没有冲突,最多可容纳n个元素,一次比较即可成功查找,并且大小为n的具有链接和k个键的表具有最小的最大(0,kn)冲突和O(1 + k / n)比较以查找。对于最差的哈希函数选择,每个插入都会导致冲突,并且哈希表会退化为线性搜索,每个插入都要进行Ω(k)摊销比较,并且最多可以进行k个比较才能成功查找。

相关:Java哈希图真的是O(1)吗?


4
但是它们确实需要花费恒定的时间来查找项目:python -m timeit -s“ s = set(range(10))”“ 5 in s” 10000000循环,最好3:每个循环0.0642 usec <-> python- m timeit -s“ s = set(range(10000000))”“ 5 in s” 10000000循环,最好是3:每个循环
0.0634 usc

2
@ THC4k您所证明的是,查找X是在恒定时间内完成的,但这并不意味着查找X + Y所花费的时间与O(1)所花费的时间相同。
Shay Erlichmen 2010年

3
@intuited:可以,但是上面的测试并不能证明您可以同时查找“ 485398”或其他一些可能在可怕的碰撞空间中查找“ 5”。这不是要在同一时间在大小不同的哈希中查找相同的元素(事实上,这根本不是必需的),而是关于您是否可以在当前表中以相同的时间访问每个条目-哈希表基本上是不可能完成的事情,因为通常总会发生冲突。
Nick Bastin 2010年

3
换句话说,查找时间取决于存储值的数量,因为这会增加发生冲突的可能性。
直觉

3
@intuited:不,那是不正确的。当存储值的数量增加时,Python将自动增加哈希表的大小,并且冲突率大致保持恒定。假设均匀分布的O(1)哈希算法,则哈希表查找将摊销 O(1)。你可能想要观看的视频演示“强大词典” python.mirocommunity.org/video/1591/...
李瑞安

13

我们都可以轻松访问source,前面的评论set_lookkey()说:

/* set object implementation
 Written and maintained by Raymond D. Hettinger <python@rcn.com>
 Derived from Lib/sets.py and Objects/dictobject.c.
 The basic lookup function used by all operations.
 This is based on Algorithm D from Knuth Vol. 3, Sec. 6.4.
 The initial probe index is computed as hash mod the table size.
 Subsequent probe indices are computed as explained in Objects/dictobject.c.
 To improve cache locality, each probe inspects a series of consecutive
 nearby entries before moving on to probes elsewhere in memory.  This leaves
 us with a hybrid of linear probing and open addressing.  The linear probing
 reduces the cost of hash collisions because consecutive memory accesses
 tend to be much cheaper than scattered probes.  After LINEAR_PROBES steps,
 we then use open addressing with the upper bits from the hash value.  This
 helps break-up long chains of collisions.
 All arithmetic on hash should ignore overflow.
 Unlike the dictionary implementation, the lookkey function can return
 NULL if the rich comparison returns an error.
*/


...
#ifndef LINEAR_PROBES
#define LINEAR_PROBES 9
#endif

/* This must be >= 1 */
#define PERTURB_SHIFT 5

static setentry *
set_lookkey(PySetObject *so, PyObject *key, Py_hash_t hash)  
{
...

2
这个答案将受益于C 语法突出显示。注释的Python语法高亮看起来确实很糟糕。
user202729

关于注释“这给我们留下了线性探测和开放寻址的混合”,在en.wikipedia.org/wiki/Open_addressing中描述的线性探测不是开放寻址中的一种冲突解决方案吗?因此,线性探测是开放式寻址的子类型,注释没有意义。
艾伦·

2

为了进一步强调set's和之间的区别dict's,这是setobject.c注释部分的摘录,其中阐明了set与dicts的主要区别。

集合的用例与字典中存在较大差异的字典大相径庭。相反,集合主要是关于成员资格测试,其中事先不知道元素的存在。因此,集合实现需要针对发现和未发现的情况进行优化。

github上的源代码

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.