【问题标题】:When do you prefer using std::list<T> instead of std::vector<T>?您何时更喜欢使用 std::list<T> 而不是 std::vector<T>?
【发布时间】:2011-06-30 17:29:30
【问题描述】:

我自己从未使用过std::list&lt;T&gt;。我想知道当我们已经拥有std::vector&lt;T&gt; 时人们何时使用它,就像具有连续内存的数组一样。当我们需要顺序容器时,std::vector 似乎是一个完美的选择!

所以我的问题是

  • 你究竟是什么时候更喜欢std::list而不是std::vector?为什么?
  • 你什么时候更喜欢std::vector而不是std::list?为什么?

如果有性能方面的考虑,请也列出它们并附上详细的解释/信息。

如果可能,还请引用一些参考资料,以支持您的回答。

【问题讨论】:

标签: c++ stl containers


【解决方案1】:

所以我的问题是:你到底什么时候更喜欢std::list 而不是std::vector

当我需要在性能敏感区域中使用顺序容器时,profiling 显示 std::list 更快。

到目前为止,这从未发生在我身上。

(当我必须存储非常大的对象并在中间进行大量插入/删除时,我可能会先尝试std::list。但是,在实践中,我从未遇到过这样的用例。 )

【讨论】:

  • +1 "当分析显示std::list 很快" 这是迄今为止最好的答案。数学和 BigO 表示法:分析您的代码并了解机器的工作原理,而不仅仅是算法/数据结构的理论属性
  • 我完全同意。通常应该从地图或矢量开始,然后分析列表是否更适合该特定用例。我认为答案永远不会是因为地图或矢量在所有情况下都更好。
【解决方案2】:

列表更适合在中间任意位置插入或删除,向量更适合在末尾插入。

向量也更适合访问元素。

这是它们实现方式的产物。

所以,如果一个集合变化很小(与访问相比)或者变化集中在最后,我会使用向量。

如果更改的数量很大(与访问相比)并且它们不在末尾,我会使用列表。

例如,在程序启动时读入一个集合并且几乎不更改它(或者如果更改只是添加到末尾),这将是一个很好的向量候选者。

另一方面,一个特别受欢迎且反复无常的摇滚明星的电话簿应用程序,我会寻找一个列表。实际上我正在寻找一个数据库连接,但这是我能在短时间内想出的最好的例子:-)

至于参考资料,最新的 C++0x 草案(在此答案时)部分说明(23.3.4,列表):

列表是一个序列容器,它支持双向迭代器,并允许在序列中的任何位置进行恒定时间的插入和擦除操作,并自动处理存储管理。与向量和双端队列不同,不支持对列表元素的快速随机访问。

第 23.3.5 节(关于向量):

向量是支持随机访问迭代器的序列容器。此外,它还支持(摊销)恒定时间的最后插入和擦除操作;在中间插入和擦除需要线性时间。

【讨论】:

  • 理论上没问题。然而,在实践中,std::vector 通常更快,即使理论上 std::list 应该表现更好。
  • @sbi:看到这令人困惑。这让我觉得listvector几乎是一样的。仅仅因为这个微小的差异,我们就有了两个不同的容器。
  • @Nawaz: std::vector 在实践中更快是当前处理器设计的产物。 STL 是在 90 年代初期/中期提出的,当时可能并不那么明显。此外,两者都是任何容器库都应提供的传统数据结构。最后,总有大对象的用例。我从未遇到过这并不意味着它不存在。
  • 我完全不同意“向量更适合在末尾插入”[请参阅下面的答案](stackoverflow.com/a/22975070/321013)
【解决方案3】:

std::liststd::vector 之间进行选择时需要考虑一些权衡。

另外,std::list 与连续内存无关,如果您无法承受迭代器失效,或者如果您需要在开始/中间/结束中插入分摊的常数时间,它会非常有用。

【讨论】:

    【解决方案4】:

    我喜欢std::list 的唯一(少数)次是由于list::splice 成员函数。如果您在列表中或列表之间的子范围内移动,则此操作可能比使用 std::vector 快得多。

    【讨论】:

      【解决方案5】:

      我不需要重复这些基础知识,但我很难学到的是,如果插入性能是相关的并且你有“大”对象,那么你应该真的考虑一个标准: :list, even 如果你只在最后插入。 (嗯,一个列表或者可能是一个智能指针/ptr_vector 的向量。)

      我们有一些用例,我们必须建立先验未知大小的多个小型 std::string 结构的集合,并使用 std::vector 完全杀死插入性能并且添加了不可忽略的内存开销。

      未知计数插入中 std::vector 的问题是:

      • 由于向量总是会过度分配,因此对于典型实现而言,最坏情况下您需要支付 50% 的空间开销(单个字符串向量,其中一个对象具有,例如 24 字节 (MSVC),给定 100,000 个元素,可能有一个空间2MB 的开销。对于具有更多字符串或其他大成员的较大对象,它会成倍增加。)
      • 每次向量必须重新分配时,它都必须复制周围的所有对象,如果你有任何稍微复杂的东西,这并不便宜。显然,移动语义会有所帮助,但如果对象本身很大,重复的副本可能仍然是相关的。如果在构建向量期间多次复制所有对象,平均摊销插入时间(最后)是否在理论上是恒定的并不重要。您可能会注意到这种性能冲击。
      • 对象变大真的很快:只有两个空 std::string 的结构在 MSVC 上已经有 不可移动 48 个字节。

      这不是要抨击 std::vector,我仍然默认使用它——但知道你的数据结构会发生什么!

      【讨论】:

        【解决方案6】:

        除了其他答案, node-base 容器(list/associative 容器)可以提供强大的 例外保证。
        即使容器变异操作(例如插入)抛出一个 例外,所有指向元素的指针/引用/迭代器仍然存在 有效。
        然而,线性内存(分段连续内存单元)容器可以 只提供基本保障。 当插入抛出时,即使插入实际上没有执行, 指针/引用/迭代器可能无效 (虽然容器本身可以安全地破坏)。

        【讨论】:

          【解决方案7】:

          有些人会说你应该,可以说,never ever ever use linked list in your code again。 ;)

          一个问题是向量具有更好的参考局部性,在许多情况下,由此带来的巨大性能提升将超过(算法上)更高效的操作的优势,例如删除并插入到链表中。

          (有关此特定问题的更多讨论,请参阅上面的博客文章,以及基准等。)

          因此,通常 std::vector 会胜过 std::list,即使您正在执行一定数量的操作,列表会更自然(例如,任意元素删除、在任意位置插入或拼接) .

          但请注意,即使您确实执行了很多此类操作,最好将列表“托管”在 std::vector 的连续缓冲区中。

          我在这篇博文中更详细地讨论了这一点,并附有一些图表和代码示例:http://upcoder.com/12/vector-hosted-lists/

          在特定情况下,当您需要删除任意元素,但不需要在任意位置插入或拼接时,完整链表的一个很好的替代方法是将条目标记为“死”并跳过迭代期间的这些死条目。

          【讨论】:

            【解决方案8】:

            在其失效语义和性能特征符合您的要求时使用列表。

            List 可以在 O(1) 的任何地方插入/擦除/拼接,这不会使任何迭代器无效。插入/擦除的向量为 O(n),除了末尾,即使在 size

            【讨论】:

              【解决方案9】:

              std::list 几乎完全是我的首选,但vector 不共享一个属性,那就是知道我指向list 的指针将始终有效。因此,我可以使用它在单个区域中包含我需要的任何资产的所有实例,同时还可以借出对其他对象的引用以供使用。我能想到的最好的例子是一个图像加载器,它只在内存中保留一个像素副本,同时借出指向多个实体的指针,以便它们都可以从中提取。

              所有vector 的访问时间都是 O(1)。虽然这听起来像是一项巨大的资产,但实际上我很少需要访问数据结构中的项目,而不是从“第一个”到“最后一个”。

              【讨论】:

              • +1 是图像列表和访问它们的多个渲染器的好例子。
              • 我会同意,如果不是因为 std::map/set 可以完全做到这一点,并且有 O(log(n)) 访问而不是 O(n)。
              • @MooingDuck mapset 有 O(log(n)) 用于 搜索 元素,然后执行操作(插入/删除)需要额外的成本来维护 红黑树结构。对于从头到尾的完整迭代,它应该与list 相同。此外,mapset 以被排序的元素为前提,但在某些问题中并不总是可取的。例如,如果模拟 UTM,磁带必须具有从任一端扩展的能力,并且节点的排列必须锁定,即使内容可以更改,并且任何更改都是顺序的,并且永远不需要随机访问。
              【解决方案10】:

              std::list 当你需要频繁修改前面或后面以外的其他地方的序列时,你可以使用std::list。与std::list相比,std::vector中此类操作的开销较大。

              【讨论】:

              • std::vector 不可能吗?
              • @Nawaz:这是可能的,但不是在恒定时间内。 sgi.com/tech/stl/Vector.html
              • 除了在现实中,std::vector 即使在那里也通常比std::list 更出色。 (这通常用向量更好的数据局部性来解释,据说它与现代处理器的缓存更好地配合。
              • 对于 std::vector 的开销实际上并没有那么大,除非复制对象非常昂贵。另一方面,std::list 分别分配每个列表节点,这也是一种开销。此外,它使用更多内存来存储指向节点的指针。
              【解决方案11】:

              当您进行大量删除/插入时,您应该使用列表。

              如果元素的总大小变化不大,并且进行了一些交换,则可以使用向量。

              【讨论】:

              • -1 向量即使在大小变化很大的某些情况下也很好。
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2023-02-16
              • 2017-09-01
              • 2018-02-02
              • 1970-01-01
              • 2016-03-31
              • 2019-10-20
              相关资源
              最近更新 更多