如何实现set()?

时间:2010-10-16 14:39:01

标签: python data-structures set cpython

我见过有人说python中的set个对象有O(1)成员资格检查。它们如何在内部实施以实现这一目标?它使用什么样的数据结构?该实施还有哪些其他含义?

这里的每个答案都很有启发性,但我只能接受一个答案,所以我会用最接近我原来问题的答案。谢谢大家的信息!

6 个答案:

答案 0 :(得分:114)

根据this thread

  

事实上,CPython的集合实现为字典之类的东西   带有虚拟值(键是集合的成员),有些   利用这种缺乏价值的优化

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

如果您如此倾向,您甚至可以浏览CPython source code for set,根据Achim Domma,{{3}}主要是dict实施中的剪切和粘贴。

答案 1 :(得分:73)

当人们说集合有O(1)成员资格检查时,他们正在谈论平均案例。在最差情况下(当所有散列值冲突时),成员资格检查为O(n)。请参阅Python wiki on time complexity

Wikipedia article表示未调整大小的哈希表的最佳情况时间复杂度为O(1 + k/n)。此结果不直接适用于Python集,因为Python集使用调整大小的哈希表。

维基百科上的文章进一步说明,对于平均值情况,并假设一个简单的统一散列函数,时间复杂度为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)

答案 2 :(得分:13)

我认为这是一个常见错误,set查找(或哈希表)不是O(1)。
from the Wikipedia

  

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

相关:Is a Java hashmap really O(1)?

答案 3 :(得分:13)

我们都可以轻松访问the 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)  
{
...

答案 4 :(得分:2)

为了更加强调set'sdict's之间的差异,这里是setobject.c评论部分的摘录,其中阐明了集合与字母的主要区别。

  

集合的用例与查找的字典有很大不同      密钥更有可能存在。相比之下,集合主要是      关于成员资格测试,其中不知道元素的存在      预先。因此,集合实现需要针对两者进行优化      发现和未发现的案例。

来自github

来源

答案 5 :(得分:1)

python中的设置在内部使用哈希表。让我们首先谈谈哈希表。 假设有一些要存储在哈希表中的元素,并且在哈希表中有31个位置可以存储。令元素为:2.83、8.23、9.38、10.23、25.58、0.42、5.37、28.10、32.14、7.31。当您要使用哈希表时,首先要确定哈希表中将这些元素存储在其中的索引。模数函数是确定这些索引的一种常用方法,因此,让我们说我们一次取一个元素,将其乘以100,然后对31取模。重要的是,对元素的每个此类运算都将产生一个唯一的数字,即除非允许链接,否则哈希表中的条目只能存储一个元素。这样,每个元素将被存储在由模运算获得的索引所控制的位置。现在,如果您要使用该哈希表在实质上存储元素的集合中搜索元素,则您将获得O(1)时间的元素,因为该元素的索引是使用恒定时间的模运算来计算的。 为了说明模运算,我还要写一些代码:

piles = [2.83, 8.23, 9.38, 10.23, 25.58, 0.42, 5.37, 28.10, 32.14, 7.31]

def hash_function(x):
    return int(x*100 % 31)

[hash_function(pile) for pile in piles]

输出:[4、17、8、0、16、11、10、20、21、18]