【问题标题】:How is set() implemented?set() 是如何实现的?
【发布时间】:2011-04-26 08:18:09
【问题描述】:

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

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

【问题讨论】:

  • 这个媒体article展示了纯python实现的样子。

标签: python data-structures set cpython


【解决方案1】:

根据this thread

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

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

如果您愿意,您甚至可以浏览CPython source code for set,根据Achim Domma,它最初主要是从dict 实现的剪切和粘贴。

注意:如今,setdict 的实现已经存在显着的差异,因此各种用例中的精确行为(例如任意顺序与插入顺序)和性能有所不同;它们仍然是根据哈希表实现的,因此平均大小写查找和插入仍然是 O(1),但 set 不再只是“dict,而是带有虚拟/省略键”。

【讨论】:

  • IIRC,原来的set实现实际上 dict带有虚拟值,后来得到了优化。
  • 大 O 不是最坏的情况吗?如果你能找到一个时间为 O(n) 的实例,那么它就是 O(n)。我现在对所有这些教程都一无所知。
  • 不,平均情况是 O(1),但最坏情况是 O(N) 进行哈希表查找。
  • @ClaudiuCreanga 这是一条旧评论,但只是为了澄清:大 O 表示法告诉您事物增长率的上限,但您可以为平均案例性能的增长设定上限,您可以分别设定最坏情况性能增长的上限。
  • 我编辑澄清setdict 不再以通用代码的方式使用太多。只是为了详细说明,dict 现在是插入排序的,并进行了优化以最大限度地减少过度分配(假设小的 dicts 在构建后不会增长太多/根本不增长),支持从一组固定的键重复查找,并假设很少有dicts 删除键,当它用于实现属性时,所有这些都会提高性能(对于所有用户定义的类和大多数用户定义的类实例); set 过度分配更多并更有效地处理项目删除,优化重复数据删除和实时更新。
【解决方案2】:

当人们说集合具有 O(1) 成员资格检查时,他们指的是 平均 情况。在 最坏 情况下(当所有散列值发生冲突时)成员资格检查是 O(n)。请参阅Python wiki on time complexity

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

Wikipedia 文章进一步说,对于 average 情况,并假设一个简单的统一散列函数,时间复杂度为 O(1/(1-k/n)),其中 k/n 可以由一个常数限定c<1.

Big-O 仅指 n → ∞ 的渐近行为。 由于 k/n 可以由常数限制,c独立于 n,

O(1/(1-k/n)) 不大于O(1/(1-c)),相当于O(constant) = O(1)

因此,假设统一的简单散列,平均,Python 集的成员资格检查是O(1)

【讨论】:

    【解决方案3】:

    我认为这是一个常见错误,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)?

    【讨论】:

    • 但是他们确实需要固定的时间来查找项目:python -m timeit -s "s = set(range(10))" "5 in s" 10000000 个循环,最好的 3:0.0642 微秒每个循环 python -m timeit -s "s = set(range(10000000))" "5 in s" 10000000 个循环,最好的 3:每个循环 0.0634 usec ... 这是最大的集合t 抛出 MemoryErrors
    • @THC4k 你所证明的只是查找 X 是在恒定时间内完成的,但这并不意味着查找 X+Y 的时间将花费相同的时间,即 O( 1) 是关于。
    • @intuited:确实如此,但上面的测试运行并不能证明您可以在查找“485398”的同时查找“5”,或者其他可能的数字在可怕的碰撞空间中。这不是关于同时在不同大小的哈希中查找相同的元素(实际上,这根本不是必需的),而是关于您是否可以在当前表中以相同的时间访问每个条目 -哈希表基本上不可能完成的事情,因为通常总会有冲突。
    • 换句话说,查找的时间取决于存储值的数量,因为这会增加冲突的可能性。
    • @intuited:不,这是不正确的。当存储值的数量增加时,Python 会自动增加哈希表的大小,并且碰撞率大致保持不变。假设一个均匀分布的 O(1) 哈希算法,那么哈希表查找是 amortized O(1)。您可能想观看视频演示“The Mighty Dictionary”python.mirocommunity.org/video/1591/…
    【解决方案4】:

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

    【讨论】:

    • 这个答案将受益于 C syntax highlighting。注释的 Python 语法高亮看起来真的很糟糕。
    • 关于评论“这给我们留下了线性探测和开放寻址的混合体”,线性探测不是开放寻址中的一种冲突解决方案,如en.wikipedia.org/wiki/Open_addressing 中所述吗?因此,线性探测是开放寻址的子类型,注释没有意义。
    【解决方案5】:

    为了更加强调set'sdict's 之间的区别,这里是setobject.c 评论部分的摘录,它阐明了set 与dicts 的主要区别。

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

    来源github

    【讨论】:

      【解决方案6】:

      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]

      【讨论】:

      • 真的很难看懂一堆文字:(
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-01-18
      • 1970-01-01
      • 2018-05-20
      • 2011-02-07
      相关资源
      最近更新 更多