deque 和 list STL 容器有啥区别?

Posted

技术标签:

【中文标题】deque 和 list STL 容器有啥区别?【英文标题】:What's the difference between deque and list STL containers?deque 和 list STL 容器有什么区别? 【发布时间】:2009-09-16 23:23:56 【问题描述】:

两者有什么区别?我的意思是方法都是一样的。因此,对于用户而言,它们的工作方式相同。

对吗??

【问题讨论】:

我对迭代性能很感兴趣。从开始到结束的迭代速度更快? 【参考方案1】:

让我列出不同之处:

Deque 使用 动态数组,提供随机 访问,和几乎一样 接口作为向量。 List 将其元素作为 双向链表 提供随机访问
Deque 提供快速插入和删除 结束和开始。插入和删除元素 中间比较慢,因为 所有元素,直到两者中的任何一个 末端可能会移动以腾出空间或 填补空白。 在List中,在每个位置插入和删除元素都很快, 包括两端。
Deque:任何元素的插入或删除 除了在开头或结尾 使所有指针、引用无效, 和引用元素的迭代器 双端队列的。 列表:插入和删除元素 不会使指针、引用无效, 和其他元素的迭代器。

复杂性

             Insert/erase at the beginning       in middle        at the end

Deque:       Amortized constant                  Linear           Amortized constant
List:        Constant                            Constant         Constant

【讨论】:

@aJ:constantamortized constant有什么区别? 长期的操作与描述的一样。但是,单个操作可能需要比指定时间更长的时间。例如:将一个元素插入到当前容量为 10 且大小已经为 9 的向量中,如果容量为 10 且大小也为 10,则时间是线性的。这是因为它必须将所有元素分配并复制到新内存中. @aJ:双端队列如何提供随机访问?还有这个结构是怎么实现的? @Lazer - 当您有提前分配内存的对象时,会发生摊销常量。例如,当您定义向量或双端队列时,它将在内存中分配一些位置(比如说 1024)。但是现在,如果你的向量或双端队列增长并且你向它添加越来越多的元素,(因此它达到 1024 个元素),它必须做一些事情。它必须创建一个更大的容器(可能是 2048 个单元的容器)+ 它还必须将所有内容复制到那个更大的容器中。因此,它以恒定的时间运行,直到它到达某个点(您必须等待),直到它复制了所有内容【参考方案2】:

来自deque的(过时但仍然非常有用的)SGI STL摘要:

双端队列很像向量:和向量一样,它是一个序列,支持随机访问元素,在序列末尾以恒定时间插入和移除元素,以及在序列中线性时间插入和移除元素中间。

deque 与 vector 的主要不同之处在于,deque 还支持在序列开头进行恒定时间的元素插入和删除。此外,deque 没有任何类似于 vector 的 capacity() 和 reserve() 的成员函数,并且不提供与这些成员函数相关的迭代器有效性的任何保证。

这是来自同一站点的list 的摘要:

列表是一个双向链表。也就是说,它是一个既支持向前遍历又支持向后遍历的序列,以及(摊销的)常数时间在开头或结尾或中间插入和移除元素。列表具有重要的属性,即插入和拼接不会使列表元素的迭代器无效,即使删除也会使指向被删除元素的迭代器无效。迭代器的顺序可能会改变(也就是说,list::iterator 在列表操作之后可能有不同的前任或后继者),但迭代器本身不会失效或指向不同的元素,除非失效或突变是明确的。

总的来说,容器可能具有共享例程,但这些例程的时间保证因容器而异。在考虑将这些容器中的哪一个用于某项任务时,这一点非常重要:考虑到如何该容器将最常使用(例如,更多用于搜索而不是插入/删除)有很长的路要走引导您到正确的容器。

【讨论】:

std::list 也有 'splice' 方法可以让你将两个列表合并在一起 其实时间保证是list的最重要的特性。 list 最重要的特性是您可以添加和删除元素,而不会使您的迭代器无效!在(几乎?)每个其他 STL 容器中,每个编辑操作都会使您的所有迭代器无效 - 因此要“删除匹配项”,您需要在一个操作中累积匹配项,然后在另一个操作中删除它们。在列表中,您可以遍历它,根据需要删除和添加,而无需重新计算迭代器。 这些也是抽象的差异,所以请根据您的情况衡量现实! list 和 deque 都有 O(1) 的插入/删除,但不要忘记这意味着 k * O(1),并且 k 对于 list 和 deque 有不同的值。在我的例子中,将对象添加到列表比双端队列花费的时间长十倍,因为列表需要更多的调用 new/delete。这显然会根据您拥有的 STL 实现而有所不同。【参考方案3】:

std::list 基本上是一个双向链表。

另一方面,std::deque 的实现更像std::vector。它具有恒定的索引访问时间,以及在开头和结尾的插入和删除,这提供了与列表截然不同的性能特征。

【讨论】:

【参考方案4】:

另一个重要的保证是每个不同的容器在内存中存储数据的方式:

向量是一个连续的内存块。 双端队列是一组链接的内存块,其中每个内存块中存储多个元素。 列表是分散在内存中的一组元素,即:每个内存“块”仅存储一个元素。

请注意,deque 旨在尝试平衡向量和列表的优点,而不存在各自的缺点。它是内存有限平台(例如微控制器)中特别有趣的容器。

内存存储策略经常被忽视,然而,它往往是为某个应用选择最合适的容器的最重要原因之一。

【讨论】:

【参考方案5】:

没有。 deque 只支持前后的 O(1) 插入和删除。例如,它可以在带有环绕的向量中实现。由于它还保证 O(1) 随机访问,因此您可以确定它没有使用(仅)双向链表。

【讨论】:

【参考方案6】:

我为 C++ 班的学生制作了插图。 这是(松散地)基于(我对)GCC STL实现中的实现( https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/stl_deque.h 和 https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/stl_list.h)

双端队列

集合中的元素存储在内存块中。每个块的元素数量取决于元素的大小:元素越大,每个块越少。潜在的希望是,如果无论元素的类型如何,块的大小都相似,这在大多数情况下都会对分配器有所帮助。

您有一个列出内存块的数组(在 GCC 实现中称为映射)。所有内存块都已满,除了第一个可能在开头有空间和最后一个可能在结尾有空间。地图本身是从中心向外填充的。与std::vector 相反,这就是如何在恒定时间内完成两端的插入。与std:::vector 类似,可以在恒定时间内进行随机访问,但需要两次间接而不是一次。与std::vector 类似,与std::list 相反,在中间删除或插入元素的成本很高,因为您必须重新组织大部分数据结构。

双向链表

双向链表可能更常见。每个元素都存储在自己的内存块中,独立于其他元素分配。在每个块中,都有元素的值和两个指针:一个指向前一个元素,一个指向下一个元素。它使在列表中的任何位置插入元素变得非常容易,甚至可以将元素的子链从一个列表移动到另一个列表(称为 splicing 的操作):您只需更新指针插入点的开始和结束。缺点是要通过索引找到一个元素,您必须遍历指针链,因此随机访问在列表中的元素数量上具有线性成本。

【讨论】:

【参考方案7】:

dequelist 之间的显着差异

对于deque

并排存放的物品;

针对从两侧(正面、背面)添加数据进行了优化;

按数字(整数)索引的元素。

可以被迭代器甚至元素的索引浏览。

访问数据的时间更快。

对于list

项目“随机”存储在内存中;

只能被迭代器浏览;

针对中间的插入和移除进行了优化。

由于空间局部性非常差,对数据的时间访问速度较慢,迭代速度较慢。

很好地处理大型元素

您还可以查看以下Link,它比较了两个 STL 容器(使用 std::vector)之间的性能

希望我分享了一些有用的信息。

【讨论】:

【参考方案8】:

其他人已经很好地解释了性能差异。我只是想补充一点,类似甚至相同的接口在面向对象编程中很常见——这是编写面向对象软件的一般方法的一部分。你绝不应该仅仅因为两个类实现相同的接口就假设它们以相同的方式工作,就像你不应该假设一匹马像狗一样工作,因为它们都实现了attack()和make_noise()。

【讨论】:

【参考方案9】:

这是一个使用列表的概念验证代码,无序映射提供 O(1) 查找和 O(1) 精确的 LRU 维护。需要(非擦除)迭代器才能在擦除操作中存活。计划在 O(1) 任意大的软件管理缓存中使用 GPU 内存上的 CPU 指针。向 Linux O(1) 调度程序致敬(LRU 每个处理器的运行队列)。 unordered_map 通过哈希表进行恒定时间访问。

#include <iostream> 
#include <list> 
#include <unordered_map>  
using namespace std; 

struct MapEntry 
  list<uint64_t>::iterator LRU_entry;
  uint64_t CpuPtr;
;
typedef unordered_map<uint64_t,MapEntry> Table;
typedef list<uint64_t> FIFO;
FIFO  LRU;        // LRU list at a given priority 
Table DeviceBuffer; // Table of device buffers

void Print(void)
  for (FIFO::iterator l = LRU.begin(); l != LRU.end(); l++) 
    std::cout<< "LRU    entry "<< *l << "   :    " ;
    std::cout<< "Buffer entry "<< DeviceBuffer[*l].CpuPtr <<endl;
    

int main() 
 

  LRU.push_back(0);
  LRU.push_back(1);
  LRU.push_back(2);
  LRU.push_back(3);
  LRU.push_back(4);

  for (FIFO::iterator i = LRU.begin(); i != LRU.end(); i++) 
    MapEntry ME =  i, *i; 
    DeviceBuffer[*i] = ME;
  

  std::cout<< "************ Initial set of CpuPtrs" <<endl;
  Print();

  
    // Suppose evict an entry - find it via "key - memory address uin64_t" and remove from 
    // cache "tag" table AND LRU list with O(1) operations
    uint64_t key=2;
    LRU.erase(DeviceBuffer[2].LRU_entry);
    DeviceBuffer.erase(2);
  

  std::cout<< "************ Remove item 2 " <<endl;
  Print();

   
    // Insert a new allocation in both tag table, and LRU ordering wiith O(1) operations
    uint64_t key=9;
    LRU.push_front(key); 
    MapEntry ME =  LRU.begin(), key ;
    DeviceBuffer[key]=ME;
  

  std::cout<< "************ Add item 9  " <<endl;
  Print();

  std::cout << "Victim "<<LRU.back()<<endl;
 

【讨论】:

您是否在正确的位置发布了此内容?这没有回答问题。

以上是关于deque 和 list STL 容器有啥区别?的主要内容,如果未能解决你的问题,请参考以下文章

STL中vector,list,deque和map的区别

stl容器区别: vector list deque set map及底层实现

C++ STL 之 deque

STL中vector,list,deque和map的区别

C++,list,vector,deque有啥区别

STL中vector,list,deque和map的区别