链表VS动态数组用于使用矢量类实现一个堆(Linked list vs dynamic array

2019-08-03 21:49发布

我读了关于执行堆栈的两种不同的方式:链表和动态数组。 在一个动态数组链表的主要优点是,链表没有被调整而动态数组必须调整如果有太多的元素被插入,因此浪费的时间和内存很多。

这让我想如果这是对C ++真(因为有插入每当新的元素它会自动调整大小的矢量类)?

Answer 1:

这是很难比较两个,因为他们的内存使用的模式有很大的不同。

矢量大小调整

根据需要的载体动态调整大小本身。 它通过分配存储器新的块,从旧的块移动(或复制)数据到新的块,释放旧的。 在一般情况下,新的块是1.5倍旧的大小(流行的看法相反,2倍似乎是在实践中相当不寻常)。 这意味着在短时间内重新分配的同时,它需要的内存等于约2.5倍之多,你实际上存储数据。 中剩下的时间里,“块”这是在使用中是最低的RDS 2/3满的最大完全充满。 如果所有的大小同样有可能,我们可以期望它平均约5/6 的部份完整。 从另一个角度看,我们可以预计大约1/6,或空间的约17%被“浪费”在任何给定的时间。

当我们做一个常数因子这样的调整(而不是,例如,一直在增加块的具体尺寸,比如以4KB增量增长),我们得到了什么叫做分期常量时间增加。 换句话说,如在阵列的增长,调整大小发生指数地较少。 时代的数组中的项已被复制的平均数量趋于一个常数(通常在3,而是取决于你使用的生长因子)。

链表分配

使用链表,情况是相当不同的。 我们从来没有看到调整,所以我们看不到多余的时间或内存使用一段插入。 与此同时,我们看到基本上是用来所有的时间额外的时间和内存。 特别地,在该链接的表的每个节点需要包含一个指针到下一个节点。 根据相比,指针的大小在节点中的数据的大小,这可能会导致显著的开销。 例如,假设你需要一堆int秒。 在一个典型的情况下int是大小的指针一样,那将意味着50%的开销-所有的时间。 这是越来越普遍的指针比一个 int ; 两倍的尺寸是相当普遍的(64位指针,32位int)。 在这种情况下,你有〜67%的开销 - 即明显不够,被存储在每个节点投入两倍的空间指针作为数据。

不幸的是,这是冰山的往往只是冰山一角。 在一个典型的链表,每个节点动态单独分配。 至少如果你存储小数据项(如int )分配给一个节点可以是(通常是)在存储器甚至比实际请求量。 所以 - 你问12个字节的内存来容纳一个int和指针 - 但记忆你得到的块很可能被调高至16或32个字节来代替。 现在你看的至少75%的开销,很可能〜88%。

至于速度推移,情况颇为相似:分配和动态释放内存往往是相当缓慢。 堆管理器通常有空闲内存块,并有花时间通过他们搜索找到最适合你要求大小的块。 然后,它(通常)具有与块分割成两片,一个以满足您的分配,和另一个余下的存储器它可以使用,以满足其他的分配。 同样,当你空闲的内存,它通常可以追溯到空闲块和检查同一目录是否有内存已经不含邻的块,所以它可以将两个重新走到一起。

分配和管理的内存块的地段是昂贵的。

高速缓存使用

最后,近期的处理器,我们碰到的另一个重要因素:高速缓存使用。 在载体的情况下,我们拥有所有的数据紧挨着对方。 那么,这是在使用向量的部分结束后,我们有一些空的内存。 这导致了优异的高速缓存的使用 - 我们正在使用的数据缓存获取; 我们不使用中的数据是所有的缓存很小或没有影响。

随着一个链表,指针(并在每个节点可能的开销)是在我们的名单分发。 也就是说,每一块数据,我们关心了,就在旁边,指针的开销,而空的空间分配给我们不使用的节点。 总之,高速缓存的有效尺寸降低了大约为列表中的每个节点的总开销相同的因素-也就是说,我们可能很容易看到的只有1/8缓存的存储我们关心的日期, 7/8 部份专门存储的指针和/或纯的垃圾。

摘要

当你有相对较少的节点,每一个都是单独相当大的链表可以很好地工作。 如果(这是一个堆栈更典型的),你所面对的是相对大量的项目,每一个都是单独相当小,你可能会看到在时间或内存使用储蓄少得多 。 恰恰相反,对于这种情况,一个链表更可能从根本上浪费的时间和内存很大。



Answer 2:

是的,你说的是对C ++如此。 出于这个原因,内部的默认容器std::stack ,这是C ++的标准栈类,既不是矢量,也不是一个链表,但一个双端队列(一个deque )。 这有一个向量的几乎所有的优点,但它会调整要好得多。

基本上, std::deque是内部各种各样的阵列的链接列表 。 这样,当它需要调整,它只是增加了一个阵列。



Answer 3:

首先,链表和动态数组之间的性能权衡很多比这更微妙。

在C ++中的载体类是,通过要求,作为“动态数组”实现,即它必须具有用于插入元件到它的摊销恒定成本。 如何做到这一点通常是通过几何方式增加了阵列的“能力”,也就是你双倍的容量,只要你用完了(或接近用完)。 最终,这意味着重新分配操作(分配内存的新小盘和复制当前内容到它)只会在少数场合发生。 在实践中,这意味着,对于重新分配的开销只会显示在性能图表作为对数间隔的小高峰。 这是什么意思有“摊销常数”成本,因为一旦你忽略了这些小尖峰,插入操作的成本基本上是恒定的(和琐碎的,在这种情况下)。

在链表实现,你没有重新分配的开销,但是,你有分配上的FreeStore(动态内存)每个新元素的开销。 所以,开销是更经常一点(未掺加,有时可需要),但也可以是比使用动态阵列,特别是如果元件是相当便宜复制(体积小,和简单对象)更显著。 在我看来,链表只推荐用于复制(或移动)真正昂贵的对象。 但在一天结束的时候,这是你需要在任何情况下,测试的东西。

最后,重要的是要指出的参考那个地方是经常的,使元素的大量使用和遍历任何应用程序的决定因素。 当使用动态阵列,所述元件在存储器中的一个挤在一起的其他和做一个中序遍历是非常有效的后作为CPU可以抢先缓存提前读取/写入操作的存储器。 在香草链表实现,从一个元素到下一个跳跃通常涉及完全不同的存储位置,有效地禁用这个“预提取”的行为进行相当不稳定的跳跃。 所以,除非列出的各个元素都非常大,对他们的操作通常很长时间来执行,使用链表时缺乏预取的将是占主导地位的性能问题。

正如您可以猜到,我很少用链表( std::list ),由于有利的应用数量都寥寥可数。 很多时候,对于大型和昂贵的对模仿对象,它往往是最好简单地使用指针的矢量(你会得到基本相同的性能优势(和缺点)作为一个链表,但较少的内存使用(用于连接指针),你会得到随机访问的能力,如果你需要的话)。

我能想到的,其中一个链表战胜动态数组(或分段动态数组一样的主箱std::deque )是当你需要(在任一端不)频繁插入元素在中间。 然而,这种情况下,当你保持一个排序(或排序,以某种方式)设定元素,在这种情况下,你可以使用一个树形结构来存储元素通常出现(例如,二叉搜索树(BST)),不是一个链表。 和经常,这些树存储使用动态阵列内的半连续的内存布局(例如,广度优先布局)它们的节点(元素)或分段动态阵列(例如,高速缓存不经意动态数组)。



Answer 4:

是的,这是真正的C++或任何其他语言。 动态阵列是一个概念 。 是C ++有事实vector不改变的理论。 在矢量C++实际上做内部的调整使这项任务不是开发商的责任。 实际成本时使用不会奇迹般地消失vector ,它只是卸载到标准库的实现。



Answer 5:

std::vector使用动态阵列实现,而std::list被作为链接列表来实现。 有取舍使用这两种数据结构。 选择最适合您需要的一个。

  • 正如你所指出的,动态数组可以利用的时间大量增加一个项目,如果它得到充分,因为它有扩张自身。 但是,更快的访问,因为它的所有成员在内存组合在一起。 这种紧密的分组通常也使得更多的缓存友好。

  • 链表不需要不断调整,但他们穿越需要更长的CPU必须在内存中跳来跳去。



Answer 6:

这让我想如果这是对C ++确实因为有每当有新元素插入的自动调整向量类。

是的,它仍持有,因为vector大小调整是一个潜在的昂贵的操作。 在内部,如果达到了向量预分配的大小,并尝试添加新的元素,新的分配发生和旧的数据移动到新的存储位置。



Answer 7:

从C ++的文档 :

矢量::的push_back - 在末尾添加元素

在向量的末尾添加新的元素,其目前的最后一个元素之后。 val的内容被复制(或移动)到新元素。

这有效地通过一个,如果仅 - 和如 - 新的向量大小超过了电流矢量容量引起所分配的存储空间的自动重新分配增加了容器的尺寸。



Answer 8:

http://channel9.msdn.com/Events/GoingNative/GoingNative-2012/Keynote-Bjarne-Stroustrup-Cpp11-Style跳转至44:40。 你应该更喜欢std::vector尽可能通过std::list ,如视频介绍,由Bjarne自己。 由于std::vector存储了所有它的元素彼此相邻,在内存中,而这将有在内存中进行缓存的优势,因为。 这是用于添加和删除元素从真正std::vector ,也搜索。 他指出, std::list比慢50-100X std::vector

如果你真的想要一个堆栈,你应该使用std::stack ,而不是把你自己的。



文章来源: Linked list vs dynamic array for implementing a stack using vector class