STL中的向量与列表


238

我在有效STL中注意到

向量是默认情况下应使用的序列类型。

什么意思 似乎无视效率vector无能为力。

有人可以给我提供vector一个不可行的方案,但list必须使用的方案吗?


6
尽管这不是您要的,但值得指出的是,默认为vector也意味着您可以轻松地与较早的代码,C库或非模板库进行交互,因为vector是指针的“传统”动态数组的薄包装器和大小。

18
Bjarne Strostrup实际上进行了测试,他生成了随机数,然后将它们分别添加到列表和向量中。进行插入,以便始终对列表/向量进行排序。即使这通常是“列表域”,矢量也会以较大的幅度超过列表。原因是内存访问速度慢,并且缓存对顺序数据的处理效果更好。可以在“ GoingNative 2012”的主题演讲中找到所有内容
躲过


1
如果您想看到@evading提到的Bjarne Stroustrup的主题演讲,我在这里找到了:youtu.be/OB-bdWKwXsU?
t=2672

Answers:


98

您要重复插入许多项目(序列末尾除外)的情况。

查看每种不同类型的容器的复杂性保证:

标准容器的复杂性保证是什么?


2
在末尾插入元素也很重要,因为这可能导致内存分配和元素复制成本。而且,在向量的开头插入elenet几乎是不可能的,list具有push_front
Notinlist 2010年

9
不可以,在向量末尾插入元素将按固定时间摊销。内存分配仅偶尔执行,您可以预先分配向量以防止使用它。当然,如果您必须保证恒定的固定时间插入,我想这仍然是一个问题。
布赖恩2010年

16
@Notinlist-下面的“不可能完成的事情”对您有用吗?v.insert(v.begin(),i)
Manuel 2010年

5
@Notinlist-我同意你的观点,只是我不希望OP认为界面不存在,以防有人想用(表演)脚开枪。
曼努埃尔

16
Bjarne Strostrup实际上进行了测试,他生成了随机数,然后将它们分别添加到列表和向量中。进行插入,以便始终对列表/向量进行排序。即使这通常是“列表域”,矢量也会以较大的幅度超过列表。原因是内存访问速度慢,并且缓存对顺序数据的处理效果更好。在“ GoingNative 2012”的主题演讲中,所有内容都可以使用
躲过了

409

向量:

  • 连续内存。
  • 为将来的元素预分配空间,因此除了元素本身所需的空间以外,还需要额外的空间。
  • 每个元素仅需要元素类型本身的空间(无需额外的指针)。
  • 您可以在添加元素时随时为整个向量重新分配内存。
  • 最后的插入是固定的,摊销的时间,但其他位置的插入则代价昂贵的O(n)。
  • 向量末尾的擦除是恒定时间,但其余时间为O(n)。
  • 您可以随机访问其元素。
  • 如果在向量中添加元素或从向量中删除元素,则迭代器无效。
  • 如果需要元素数组,则可以轻松获得基础数组。

清单:

  • 非连续内存。
  • 没有预分配的内存。列表本身的内存开销是恒定的。
  • 每个元素都需要用于存放该元素的节点的额外空间,包括指向列表中下一个和上一个元素的指针。
  • 不必仅仅因为添加了元素就为整个列表重新分配了内存。
  • 无论插入和删除在列表中的何处发生,它们都是廉价的。
  • 将列表与拼接结合起来很便宜。
  • 您不能随机访问元素,因此获取列表中的特定元素可能会很昂贵。
  • 即使在列表中添加或删除元素,迭代器仍然有效。
  • 如果您需要一个元素数组,则由于没有基础数组,因此必须创建一个新元素并将其全部添加到其中。

通常,当您不在乎所使用的顺序容器类型时,可以使用vector,但是如果要在容器中除末尾以外的任何位置进行多次插入或删除操作,您将需要使用清单。或者,如果您需要随机访问,那么您将需要向量,而不是列表。除此之外,在某些情况下自然会根据您的应用程序需要一个或另一个,但是总的来说,这些都是很好的准则。


2
同样,从免费商店进行分配也不是免费的。:)在向量中添加新项目会执行O(log n)个免费商店分配,但是您可以调用reserve()将其减少为O(1)。将新项目添加到列表(即不进行拼接)将执行O(n)个免费商店分配。
bk1e 2010年

7
另一个考虑因素是,list当您擦除元素时会释放内存,但vector不会。vector减小大小时,A 不会减少其容量,除非您使用swap()技巧。
bk1e 2010年

@ bk1e:我真的很想知道您在reserve()和swap()上的把戏:)
Dzung Nguyen 2010年

2
@nXqd:如果需要在向量中添加N个元素,请调用v.reserve(v.size()+ N),以便仅执行一次免费存储分配。swap()技巧在这里:stackoverflow.com/questions/253157/how-to-down-size-stdvector
bk1e 2010年

1
@simplename否。它说的是正确的。向量为向量中当前的元素分配超出空间的额外空间;然后使用多余的容量来增长向量,以便将其增长摊销O(1)。
乔纳森·M·戴维斯

35

如果您不需要经常插入元素,那么向量将更加有效。它具有比列表更好的CPU缓存位置。换句话说,访问一个元素可能使下一个元素存在于缓存中,并且无需读取慢速RAM即可对其进行检索。


32

这里的大多数答案都遗漏了一个重要的细节:为什么?

您想在容器中保留什么?

如果它是的集合int,则std::list在每种情况下都将丢失,无论是否可以重新分配,都只能从前面删除,以此类推。列表的遍历速度较慢,每次插入都需要您与分配器进行交互。准备一个list<int>跳动的例子是非常困难的vector<int>。即使这样deque<int>也可能更好或更接近,而不是仅仅依靠列表的使用,这将增加内存开销。

但是,如果您要处理的是大块丑陋的数据-其中很少-您不想在插入时进行总体分配,并且由于重新分配而进行的复制将是一场灾难-那么,也许可以list<UglyBlob>vector<UglyBlob>

不过,如果您切换到vector<UglyBlob*>甚至vector<shared_ptr<UglyBlob> >再次,则-列表会落后。

因此,访问模式,目标元素数量等仍然会影响比较,但是在我看来-元素大小-复制成本等。


1
一个更反射,通过读迈尔斯“有效STL”时我有:的特殊的属性list<T>这样的可能性,以spliceO(1) 。如果需要恒定时间的拼接,列表也可能是选择的结构;)
Tomasz Gandor

+1 -它甚至没有成为一个UglyBlob-即使只有几柱部件很容易被复制昂贵的对象,因此,重新分配成本。另外:不要忽略空间开销,vector几十个大小的保持对象的指数增长可能会导致(如果不能reserve提前)。
马丁·巴

至于vector<smart_ptr<Large>>vs。list<Large>我想说的是,如果您需要随机访问元素,这vector是有道理的。如果您不需要随机访问,则list看起来更简单,并且应具有相同的性能。
马丁·巴

19

std :: list的一种特殊功能是拼接(将部分或整个列表链接或移动到另一个列表中)。

或者,如果您的内容复制非常昂贵。在这种情况下,例如,用列表对集合进行排序可能会更便宜。

还要注意,如果集合很小(并且内容复制并不特别昂贵),即使您在任何地方插入和删除向量,向量也可能仍然胜过列表。列表会分别分配每个节点,这可能比移动一些简单的对象要贵得多。

我认为没有非常严格的规定。这取决于您最想对容器执行什么操作,以及容器的大小和所包含的类型。向量通常胜过一个列表,因为它将其内容分配为单个连续的块(它基本上是一个动态分配的数组,在大多数情况下,数组是保存一堆东西的最有效方法)。


1
+1。拼接被忽略了,但是不幸的是,拼接并非如期所愿。:((((如果list :: size是恒定时间,则不能。)

出于这个原因,我非常确定list :: size是(允许)线性的。
UncleBens


@Potatoswatter:该标准含糊不清,因此您不能依赖于“兼容”实施,这使它成为更大的问题。从字面上看,您必须避免使用stdlib来获得可移植且可靠的保证。

@Roger:是的,很不幸。我当前的项目强烈依赖于拼接操作,并且该结构几乎是笔直的C。更不幸的是,在N3000中splice,不同列表之间的顺序被指定为线性复杂度,size并且特别恒定。因此,为了适应那些反复进行的新手,size对于STL或任何“兼容”容器时期,一整类算法都无法解决。
Potatoswatter

13

好吧,我班的学生们似乎无法向我解释什么时候使用向量更有效,但是当他们建议我使用列表时,他们看起来很高兴。

这就是我的理解

列表:每个项目都包含下一个或上一个元素的地址,因此,使用此功能,您可以将这些项目随机化,即使它们没有排序,顺序也不会改变:如果内存碎片化,这将非常有效。但这还有另一个很大的优点:您可以轻松地插入/删除项目,因为您唯一需要做的就是更改一些指针。缺点:要读取随机的单个项目,您必须从一个项目跳到另一个项目,直到找到正确的地址。

向量:使用向量时,内存的组织方式更像常规数组:第n个项目存储在第(n-1)个项目之后和第(n + 1)个项目之前。为什么比列表更好?因为它允许快速随机访问。方法如下:如果您知道向量中某个项目的大小,并且它们在内存中是连续的,则可以轻松地预测第n个项目在哪里;您无需浏览列表中的所有项目即可阅读所需的内容,而使用vector则可以直接阅读,而无需阅读列表。另一方面,修改向量数组或更改值的速度要慢得多。

列表更适合跟踪可在内存中添加/删除的对象。当您要从大量单个项目访问元素时,向量更合适。

我不知道如何优化列表,但是您必须知道,如果要快速读取访问,则应该使用向量,因为STL固定列表的能力非常好,读取访问的速度不会比向量快。


“修改向量数组或更改值的速度要慢得多”-如所阅读的,这似乎与您之前所说的关于向量因其低级和连续的特性而倾向于具有良好性能的说法相矛盾。没有你的意思是重新分配vector造成改变其大小可能会很慢?然后同意,但在reserve()可以使用的情况下,可以避免这些问题。
underscore_d

10

基本上,向量是具有自动内存管理功能的数组。数据在内存中是连续的。尝试在中间插入数据是一项昂贵的操作。

在列表中,数据存储在不相关的存储位置中。在中间插入并不涉及复制某些数据以为新数据腾出空间。

为了更具体地回答您的问题,我将引用此页面

向量通常在时间上是最有效的,可用于访问元素以及从序列末尾添加或删除元素。对于涉及在末端以外的位置插入或删除元素的操作,它们的性能比双端队列和列表差,并且迭代器和引用的一致性比列表差。


9

任何时候您都不能使迭代器无效。


2
但是,在不询问 a的持久引用是否deque足够的情况下,切勿得出关于迭代器的结论。
Potatoswatter

8

当序列中间有很多插入或删除时。例如内存管理器。


因此它们之间的区别只是效率,而不是功能问题。
skydoor 2010年

他们当然都对一系列要素建模。如@dirkgently所述,用法上的差别很小,但是您必须查看“经常完成”操作的复杂性,才能确定选择哪个顺序(@Martin答案)。
AraK 2010年

@skydoor-有一些功能上的区别。例如,只有向量支持随机访问(即可以被索引)。
曼努埃尔

2
@skydoor:效率转化为绩效。性能不佳会破坏功能。毕竟,性能是C ++的优势。
Potatoswatter

4

保留迭代器的有效性是使用列表的原因之一。另一个是当您不希望向量在推送项目时重新分配时。可以通过智能使用reserve()来管理此操作,但是在某些情况下,仅使用列表可能更容易或更可行。


4

要在容器之间移动对象时,可以使用list::splice

例如,图分区算法可以在越来越多的容器中递归地划分恒定数量的对象。对象应初始化一次,并始终保留在内存中的相同位置。通过重新链接重新排列它们比通过重新分配要快得多。

编辑:随着库准备实现C ++ 0x,将子序列拼接到列表中的一般情况随着序列的长度而变得线性复杂。这是因为splice(现在)需要遍历该序列以计算其中的元素数量。(因为列表需要记录其大小。)简单地对列表进行计数和重新链接仍然比任何其他方法都要快,并且拼接整个列表或单个元素是具有恒定复杂性的特殊情况。但是,如果要拼接的序列很长,则可能需要挖掘一个更好的,老式的,不兼容的容器。


2

唯一list必须使用的硬性规则是您需要在其中分配指向容器元素的指针。

与之不同vector,您知道不会重新分配元素的内存。如果可能的话,那么您可能有指向未使用内存的指针,这充其量是一个很大的禁忌,而最糟糕的是一个SEGFAULT

(从技术上讲vector*_ptr也可以使用of ,但是在这种情况下,您正在仿真,list所以这只是语义。)

其他软性规则与将元素插入容器中间的可能的性能问题有关,因此list更可取。


2

简单点-
当您困惑地在C ++中选择容器时,请使用此流程图图像(感谢我):- 在此处输入图像描述

向量-1
.向量基于传染性内存
2.向量是处理小型数据集的方式
3.向量在遍历数据集时执行速度最快
4.向量插入删除在大型数据集上较慢,但对于非常小的数据集则较快


列表-1 .列表基于堆内存
2.列表是处理非常大的数据集的方式
3.列表在遍历小数据集时相对较慢,但在处理大数据集时
则相对较快4.列表插入删除的速度很快庞大的数据集,但较小的数据集速度较慢


1

列表只是stl中的双向链接列表的包装,因此提供了d链接列表可能期望的功能,即O(1)插入和删除。向量是具有传染性的数据序列,其作用类似于动态数组。PS-易于遍历。


0

对于vectorlist,突出的主要区别如下:

向量

  • 向量将其元素存储在连续内存中。因此,可以在向量内部进行随机访问,这意味着访问向量的元素非常快,因为我们可以简单地将基地址与项索引相乘以访问该元素。实际上,为此仅需要O(1)或恒定时间。

  • 由于向量基本上是包装一个数组,因此,每次将一个元素插入向量(动态数组)时,它都必须通过查找新的连续内存块来容纳新元素来调整自身大小,而这是耗时的。

  • 它不消耗额外的内存来存储指向其中其他元素的任何指针。

清单

  • 列表将其元素存储在非连续内存中。因此,列表内的随机访问是不可能的,这意味着要访问其元素,我们必须使用指针并遍历相对于向量而言较慢的列表。这需要O(n)或比O(1)慢的线性时间。

  • 由于列表使用非连续内存,因此将元素插入列表内所需的时间比其向量对应对象的效率高得多,因为可以避免重新分配内存。

  • 它消耗额外的内存来在特定元素之前和之后存储指向该元素的指针。

因此,请牢记这些差异,我们通常会考虑内存,频繁的随机访问插入来确定给定场景中向量与列表的优胜者。


0

列表是双链表,因此很容易插入和删除元素。我们只需要更改几个指针,而在向量中,如果我们想在中间插入一个元素,则其后的每个元素都必须移动一个索引。同样,如果向量的大小已满,则必须首先增加其大小。因此,这是一项昂贵的操作。因此,在这种情况下,无论何时需要更频繁地执行插入和删除操作,都应使用列表。

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.