【问题标题】:Is list::size() really O(n)?list::size() 真的是 O(n) 吗?
【发布时间】:2010-09-18 17:43:59
【问题描述】:

最近,我注意到有人提到std::list::size() 具有线性复杂度。
根据somesources,这实际上取决于实现,因为标准没有说明复杂性必须是什么。
评论in this blog entry 说:

其实这要看你是哪个STL 正在使用。微软 Visual Studio V6 将 size() 实现为 {return (_Size); } 而 gcc(至少在版本中 3.3.2 和 4.1.0) 这样做 { return std::distance(begin(), end()); } 这 第一个恒速,第二个 有 o(N) 速度

  1. 所以我的猜测是,对于 VC++ 人群,size() 与 Dinkumware 一样具有恒定的复杂性 自 VC6 以来可能不会改变这一事实。我在吗?
  2. gcc 中的当前情况如何?如果真的是 O(n),为什么 开发者选择这样做?

【问题讨论】:

    标签: c++ list stl complexity-theory big-o


    【解决方案1】:

    我之前不得不查看 gcc 3.4 的 list::size,所以我可以这样说:

    1. 它使用std::distance(head, tail)。
    2. std::distance 有两种实现:对于满足 RandomAccessIterator 的类型,它使用“tail-head”,对于仅满足 InputIterator 的类型,它使用 O(n)算法依赖于“iterator++”,一直计数直到到达给定的尾部。
    3. std::list 不满足 RandomAccessIterator,所以大小为 O(n)。

    至于“为什么”,我只能说std::list 适合需要顺序访问的问题。将大小存储为类变量会在每次插入、删除等操作中引入开销,并且按照 STL 的意图,这种浪费是一个很大的禁忌。如果您确实需要恒定时间的size(),请使用std::deque。

    【讨论】:

      【解决方案2】:

      Pre-C++11 答案

      您是正确的,该标准没有说明 list::size() 的复杂性必须是什么 - 但是,它确实建议它“应该具有恒定的复杂性”(表 65 中的注释 A)。

      Here's an interesting article by Howard Hinnant 这解释了为什么有些人认为 list::size() 应该具有 O(N) 复杂度(基本上是因为他们认为 O(1) list::size() 使得 list::splice() 具有 O(N) 复杂度)以及为什么 O (1) list::size() 是个好主意(在作者看来):

      我认为论文的主要观点是:

      • 在少数情况下维持内部计数,因此list::size() 可以为 O(1) 导致拼接操作变为线性
      • 可能还有更多的情况,有人可能没有意识到可能发生的负面影响,因为他们调用了 O(N) size()(例如他的一个例子,在持有锁时调用了 list::size())。
      • 为了“最少意外”,标准不应允许size() 为O(N),而是应要求任何实现size() 的容器以O(1) 的方式实现它。如果容器不能做到这一点,它根本不应该实现size()。在这种情况下,容器的用户将被告知 size() 不可用,如果他们仍然想要或需要获取容器中元素的数量,他们仍然可以使用 container::distance( begin(), end()) 来获取该值 - 但他们将完全意识到这是一个 O(N) 操作。

      我想我倾向于同意他的大部分推理。但是,我不喜欢他对splice() 重载的提议。必须传入必须等于 distance( first, last) 的 n 才能获得正确的行为,这似乎是难以诊断错误的秘诀。

      我不确定今后应该或可以做什么,因为任何更改都会对现有代码产生重大影响。但就目前而言,我认为现有代码已经受到影响 - 对于应该定义明确的东西,行为可能会因一种实现与另一种实现有很大不同。也许 onebyone 的关于将大小“缓存”并标记为已知/未知的评论可能效果很好 - 你会得到摊销的 O(1) 行为 - 你得到 O(N) 行为的唯一时间是当列表被某些 splice() 操作修改时.这样做的好处是,今天的实现者可以在不改变标准的情况下完成它(除非我遗漏了一些东西)。

      据我所知,C++0x 在这方面没有任何改变。

      【讨论】:

      • 答案是正确的,但是关于列表大小的推理是流动的。你的提案容易出现参数不一致的情况,违反了用户每条信息只提供一次的原则。
      • 应该也可以保持拼接O(1),但将大小标记为“未知”。那么 size() 仍然是 O(N) 最坏情况,但最坏情况在每个“不友好”拼接中最多发生一次。因此,所有操作的性能都严格优于 always-O(N) size()。警告:我还没有考虑清楚。
      • “严格优越” - 实际上这是一个谎言,因为在 splice 中有一些额外的检查来确定你所处的情况,以及所有突变体的大小的算术。告诉你我没有考虑清楚。但复杂性永远不会更糟,有时甚至更好。
      • @PierreBdR - 如果不清楚,我不是论文的作者,我指出它是因为我认为它有一些有趣的观点。我已经编辑了答案以使其更清楚(以及添加更多我自己的想法并结合这些 cmets 的想法)。
      • 最新的 C++0x 草案要求 size() 具有恒定的时间复杂度(在 N3000 中对容器要求进行了更改)。
      【解决方案3】:

      我会去source (archive)。 SGI 的 STL 页面说它允许具有线性复杂度。我相信他们遵循的设计准则是让列表实现尽可能通用,从而在使用列表时提供更大的灵活性。

      【讨论】:

      • SGI 并不完全是“来源”。它基于原始的(HP?)STL,但标准偏离了这一点。 SGI 只是说他们的实现做了什么,而不是标准说它应该做什么。
      • 反正现在链接坏了。
      【解决方案4】:

      如果您正确使用列表,您可能不会注意到任何差异。

      列表适用于您希望在不复制的情况下重新排列的大数据结构,以及您希望在插入后保留有效指针的数据。

      在第一种情况下没有区别,在第二种情况下,我更喜欢旧的(较小的)size() 实现。

      无论如何,std 更多的是关于正确性和标准行为以及“用户友好性”,而不是原始速度。

      【讨论】:

      • 我不清楚如何想知道,在紧要关头,列表中有多少元素,构成不正确使用列表。
      【解决方案5】:

      在 C++11 中,对于 任何 标准容器,.size() 操作必须以“恒定”复杂度 (O(1)) 完成。 (表 96——容器要求)。以前在 C++03 中 .size() 应该 具有恒定的复杂性,但不是必需的(请参阅 Is std::string size() a O(1) operation?)。

      标准的变化由n2923: Specifying the complexity of size() (Revision 1) 介绍。

      不过,libstdc++ 中.size() 的实现在gcc 4.8 之前仍然使用O(N) 算法:

        /**  Returns the number of elements in the %list.  */
        size_type
        size() const _GLIBCXX_NOEXCEPT
        { return std::distance(begin(), end()); }
      

      另请参阅Why is std::list bigger on c++11?,了解为何以这种方式保存的详细信息。

      更新:在 C++11 模式(或更高版本)下使用 gcc 5.0 时,std::list::size() 是 properly O(1)。 p>


      顺便说一句,libc++ 中的.size() 是正确的 O(1):

      _LIBCPP_INLINE_VISIBILITY
      size_type size() const _NOEXCEPT     {return base::__sz();}
      
      ...
      
      __compressed_pair<size_type, __node_allocator> __size_alloc_;
      
      _LIBCPP_INLINE_VISIBILITY
      const size_type& __sz() const _NOEXCEPT
          {return __size_alloc_.first();}
      

      【讨论】:

      • 这应该被接受,不幸的是人们不要看老Q。:)
      【解决方案6】:

      这个错误报告:[C++0x] std::list::size complexity,详细记录了 GCC 4.x 中的实现是线性时间的事实,以及 C++11 向恒定时间的过渡是如何缓慢到来的(在 5.0 中可用) ABI 兼容性问题。

      GCC 4.9 系列的手册页仍然包含以下免责声明:

      仍然支持 C++11 实验性的,并且在未来的版本中可能会以不兼容的方式进行更改。


      此处引用了相同的错误报告:Should std::list::size have constant complexity in C++11?

      【讨论】:

        【解决方案7】:

        我个人不认为拼接为 O(N) 的问题是允许尺寸为 O(N) 的唯一原因。 你不为你不使用的东西付费是一个重要的 C++ 座右铭。在这种情况下,无论您是否检查列表的大小,维护列表大小都需要在每次插入/擦除时额外增加/减少。这是一个很小的固定开销,但仍然需要考虑。

        很少需要检查列表的大小。在不关心总大小的情况下从头到尾迭代是非常普遍的。

        【讨论】:

        • 显然 C++11 委员会不同意你的看法。可惜。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-12-10
        • 1970-01-01
        • 1970-01-01
        • 2010-09-20
        • 1970-01-01
        相关资源
        最近更新 更多