在C ++中对向量使用列表的意义是什么?


32

我已经运行了3个涉及C ++列表和向量的不同实验。

事实证明,即使在中间涉及很多插入操作,带有向量的操作也更加有效。

因此,出现了一个问题:在哪种情况下列表比矢量有意义?

如果向量在大多数情况下似乎更有效率,并考虑其成员的相似程度,那么列表还有哪些优势?

  1. 生成N个整数并将其放入容器中,以便容器保持排序状态。通过逐个读取元素并在第一个较大的元素之前插入新的元素,已天真地执行了插入操作。
    与向量相比,有了清单,尺寸增加时,时间就会流逝。

  2. 在容器的末尾插入N个整数。
    对于列表和向量,时间增加了相同的数量级,尽管向量快3倍。

  3. 在容器中插入N个整数。
    启动计时器。
    使用list.sort来排序容器,使用std :: sort来对容器进行排序。停止计时器。
    同样,时间以相同的数量级增加,但使用向量平均要快5倍。

我可能会继续进行测试,并找出几个清单可以证明更好的例子。

但是你们阅读此消息的共同经验可能会提供更有成效的答案。

您可能遇到过这样的情况,其中列表更易于使用或执行得更好?


2
您应该看看何时在数组/数组列表上使用链表?如果您还没有
Karthik T

1
这是关于该主题的另一个很好的资源:stackoverflow.com/a/2209564/8360同样,我听说的大多数C ++指南默认情况下都使用vector,仅在有特定原因的情况下列出。
Zachary Yates

谢谢。但是,我不同意最喜欢的答案中的大部分内容。我的实验使大多数这些先入为主的想法无效了。这个人没有做过任何测试,并运用了在书本或学校里教授的广泛理论。
Marek Stanley,

1
list如果要删除很多元素,A 可能会更好。我不相信a vector会在整个向量被删除之前将内存返回给系统。还请记住,测试#1并不是仅测试插入时间。这是结合搜索和插入的测试。找到list缓慢插入的地方。实际插入将比矢量更快。

3
非常典型地以(运行时)性能,性能和仅性能来描述此问题。这似乎是很多程序员的盲点-他们专注于这一方面,而忘了还有很多其他方面往往更为重要。
布朗

Answers:


34

简短的答案是,案件似乎很少而且相去甚远。虽然可能有一些。

一种情况是,当您需要存储少量大型对象时-尤其是大型对象,以至于无法为其中的一些额外对象分配空间。基本上没有办法阻止向量或双端队列为额外的对象分配空间-这是它们的定义方式(即,他们必须分配额外的空间以满足其复杂性要求)。如果您完全无法分配额外的空间,则an std::list可能是满足您需求的唯一标准容器。

另一个可能是何时/如果您将迭代器长时间存储到列表中的“有趣”点,并且当您进行插入和/或删除时,您(几乎)总是从某个位置进行迭代您已经有一个迭代器,因此无需遍历列表即可进行插入或删除操作。显然,如果您使用多个地点,则同样适用,但仍计划将迭代器存储到您可能使用的每个位置,因此您可以最大程度地操纵可直接到达的地点,并且很少浏览列表以获取去那些景点。

对于第一个示例,请考虑使用Web浏览器。它可能会保留一个Tab对象的链接列表,每个选项卡对象都表示在浏览器中打开的选项卡上。每个标签页可能有几十兆字节的数据(更多的数据,尤其是在涉及视频之类的情况下)。您打开的标签页的典型数量可能很容易少于十二个,而100个可能接近上限。

对于第二个示例,请考虑一个文字处理程序,该文字处理程序将文本存储为章节的链接列表,每个章节可能包含(例如)段落的链接列表。当用户进行编辑时,他们通常会找到要编辑的特定位置,然后在该位置(或无论如何在该段落内)进行大量工作。是的,他们会一次又一次地从一个段落移到另一个段落,但是在大多数情况下,这将是他们已经工作的地方附近的一段。

偶尔(例如全局搜索和替换之类的东西)最终会遍历所有列表中的所有项目,但这并不常见,即使这样做,您也可能会做足够的工作来搜索其中的一个项目。在列表中,遍历列表的时间几乎是无关紧要的。

请注意,在一般情况下,这是有可能符合第一条标准,以及-一个章节包含了相当少量的段落,每一个可能是相当大的(相对于在指针的大小至少节点等)。同样,您的章节数量相对较少,每个章节可能约为几千字节。

就是说,我必须承认这两个示例可能都是人为设计的,尽管链表对于这两个示例都可以很好地工作,但在任何情况下都可能不会提供巨大的优势。例如,在两种情况下,为某些(空)网页/选项卡或某些空章节在向量中分配额外的空间都不是真正的问题。


4
+1,但是:使用指针时第一种情况消失了,应该始终将其与大对象一起使用。链接列表也不适合第二个示例;这么短,所有操作拥有数组。
amara 2013年

2
大对象的情况根本不起作用。使用std::vector指针的效率将比所有那些链接列表节点对象的效率更高。
Winston Ewert

链接列表有许多用途-只是它们不像动态数组那样常见。LRU缓存是链接列表的一种常见用法。
查尔斯·索尔维亚

另外,a std::vector<std::unique_ptr<T>>可能是一个很好的选择。
Deduplicator

24

根据Bjarne Stroustrup本人的看法,向量应始终是数据序列的默认集合。如果要优化元素的插入和删除,则可以选择列表,但通常不这样做。列表的代价是慢速遍历和内存使用。

他在本演讲中谈到了这一点

在大约0:44,他通常谈论向量与列表。

紧凑性很重要。向量比列表更紧凑。可预测的使用模式至关重要。使用向量,您必须推翻很多元素,但是缓存确实非常擅长于此。...列表没有随机访问权限。但是,当遍历列表时,您将继续进行随机访问。这里有一个节点,它到达内存中的那个节点。因此,您实际上是在随机访问内存,并且正在使高速缓存未命中最大化,这与您想要的完全相反。

在大约1:08,他被问到了这个问题。

我们应该看到,我们需要一系列元素。C ++中元素的默认序列是向量。现在,因为它既紧凑又高效。实现,映射到硬件很重要。现在,如果您要针对插入和删除进行优化-您说,“嗯,我不需要序列的默认版本。我想要专门的,这是一个列表。如果这样做,您应该足够了解,“我接受一些成本和一些问题,例如缓慢的遍历和更多的内存使用”。


1
您介意简短地写您链接至 “大约0:44和1:08” 的演示文稿中的内容吗?
gnat 2013年

2
@gnat-当然。我试图分别引用有意义的内容,但确实需要幻灯片的上下文。
皮特

11

我通常使用列表的唯一地方是我需要删除元素并且不使迭代器无效的地方。std::vector使插入和擦除时的所有迭代器失效。std::list保证插入或删除后对现有元素的迭代器仍然有效。


4

除了已经提供的其他答案之外,列表还具有矢量中不存在的某些功能(因为它们会非常昂贵)。拼接和合并操作最为重要。如果您经常有一堆需要追加或合并在一起的列表,那么列表可能是一个不错的选择。

但是,如果您不需要执行这些操作,则可能不需要。


3

缺少链接列表固有的高速缓存/页面友好性,往往使它们几乎完全被许多C ++开发人员所忽略,并且以这种默认形式具有充分的理由。

链表仍然很棒

然而,链表在固定分配器的支持下可能很棒,该分配器可以使链表恢复其固有的空间位置。

他们擅长的地方是我们可以将一个列表分为两个列表,例如,只需存储一个新的指针并处理一个或两个指针即可。我们可以仅通过指针操作就可以在恒定的时间内将节点从一个列表移动到另一个列表,而空列表可以仅具有单个head指针的内存开销。

简单网格加速器

作为实际示例,请考虑2D视觉模拟。它有一个滚动屏幕,其中的地图跨越了400x400(160,000个网格),用于加速诸如帧之间移动的数百万个粒子之间的碰撞检测之类的操作(在此我们避免使用四叉树,因为四叉树实际上往往在此水平下表现较差)动态数据)。一大堆粒子在每个帧中不断移动,这意味着它们从驻留在一个栅格单元中不断移动到另一个栅格单元。

在这种情况下,如果每个粒子是一个单链列表节点,则每个网格单元都可以从仅head指向的指针开始nullptr。当新粒子诞生时,我们通过将其设置为head指向该粒子节点的指针,将其放置在它所驻留的网格单元中。当粒子从一个网格单元移动到下一个网格单元时,我们只需操纵指针。

这比vectors在每个网格单元中存储160,000个并在每个帧的基础上始终从中间推回并擦除的效率要高得多。

std :: list

但是,这是由固定分配器支持的手动滚动式,侵入式,单链接式列表。std::list表示一个双链表,当它为空时,它可能不像单个指针那样紧凑(因供应商的实现而异),再加上以std::allocator表格形式实现自定义分配器,这是一种痛苦。

我必须承认,我从不使用list任何东西。但是链接列表仍然可以很棒!但是,它们并不是很出色,原因是人们经常会尝试使用它们,除非拥有非常高效的固定分配器作为后盾,否则它们就不会那么出色,这种分配器至少可以缓解许多强制性页面错误和缓存丢失。


1
自C ++ 11起,存在一个标准的单链接列表std::forward_list
sharyex

2

您应该考虑容器中元素的大小。

int 元素向量非常快,因为大多数数据都适合CPU缓存(并且SIMD指令可能可以用于数据复制)。

如果元素的大小较大,则测试1和3的结果可能会发生显着变化。

通过非常全面的性能比较

得出关于每种数据结构用法的简单结论:

  • 数字运算:使用std::vectorstd::deque
  • 线性搜索:使用std::vectorstd::deque
  • 随机插入/删除:
    • 小数据量:使用 std::vector
    • 大元素尺寸:使用std::list(除非主要用于搜索)
  • 非平凡的数据类型:std::list除非您特别需要用于搜索的容器,否则请使用。但是对于容器的多次修改,它将非常缓慢。
  • 推到最前面:使用std::dequestd::list

(作为旁注std::deque是一个非常低估的数据结构)。

从方便的角度来看,std::list可以保证迭代器在插入和删除其他元素时永远不会失效。这通常是一个关键方面。



0

简而言之,没有充分的理由使用std::list<>

  • 如果您需要未分类的容器,请std::vector<>遵循规则。
    (通过将元素替换为向量的最后一个元素来删除它们。)

  • 如果需要排序的容器,请std::vector<shared_ptr<>>遵循规则。

  • 如果您需要稀疏索引,请使用std::unordered_map<>规则。

而已。

我发现在一种情况下,我倾向于使用链表:当我已经存在需要以某种方式连接以实现一些附加应用程序逻辑的对象时。但是,在那种情况下,我从不使用std::list<>,而是在对象内部求助于(智能)next指针,尤其是因为大多数用例都生成树而不是线性列表。在某些情况下,结果结构是链表,在其他情况下,它是树或有向无环图。这些指针的主要目的始终是建立逻辑结构,而不是管理对象。我们std::vector<>为此。


-1

您需要显示在第一次测试中如何进行插入。您的第二次和第三次测试,vector将轻松获胜。

列表的一个重要用途是当您必须支持在迭代时删除项目。修改向量后,所有迭代器都(可能)无效。对于列表,只有对已删除元素的迭代器无效。所有其他迭代器仍然有效。

容器的典型使用顺序是矢量,双端队列,然后是列表。容器的选择通常基于push_back选择向量,pop_front选择双端队列,插入选择列表。


3
在迭代过程中删除项目时,通常最好使用一个向量,然后为结果创建一个新向量
amara 2013年

-1

我可以想到的一个因素是,随着向量的增长,随着向量释放其内存并一遍又一遍地分配更大的块,可用内存将变得碎片化。列表不会有问题。

除了以下事实外,大量push_back无保留的s还会在每次调整大小时导致复制,从而使其效率低下。插入中间类似地导致所有元素向右移动,甚至更糟。

我不知道这是否是一个主要问题,但这是我在工作(手机游戏开发)中避免使用向量的原因。


1
不,载体会复制,这很昂贵。但是遍历链接列表(以找出要插入的位置)也很昂贵。关键的确是要衡量
凯特·格雷戈里

@KateGregory除此之外,我的意思是,让我进行相应的编辑
Karthik T

3
是的,但是不管您信不信,您不相信(大多数人都不相信)您没有提到的费用,请遍历链接列表以查找在哪里插入那些副本(特别是如果元素较小(或可移动,因为这样的话))它们是动作)),向量通常(甚至通常)更快。信不信由你。
凯特·格雷戈里
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.