我从C ++标准2003(第23.2.1.3章)中了解到deque::insert()
的复杂性如下:
在最坏的情况下,将单个元素插入到双端队列中需要时间在从插入点到双端队列开始的距离的最小值以及从插入点到双端队列结束的距离的线性。
我总是将stl deque的实现理解为内存块的集合。因此,插入仅影响与插入位置相同的存储块中的元素。我的问题是,标准是什么意思是“从插入点到双端队列开始的距离的最小值和从插入点到双端队列结束的距离的线性”?
我的理解是因为C ++标准没有强制实施deque的某种实现。对于最坏的情况,复杂性通常是一般的。但是,在编译器的实际实现中,它与内存块中的元素数量呈线性关系,这可能因元素大小的不同而不同。
另一个猜测可能是,因为insert()
将使所有迭代器无效,deque需要更新所有迭代器。因此它是线性的。
答案 0 :(得分:8)
std :: deque通常(总是?)实现为内存块的集合,但它通常不会插入一个全新的块,只是为了在你插入一个新的元素中间的集合。因此,它将查找插入点是否更接近开头或结尾,并对现有元素进行随机播放,以便为现有块中的新元素腾出空间。它只会在集合的开头或结尾添加一大块新内存。
答案 1 :(得分:3)
我认为通过图表可以更好地服务......让我们玩ASCII艺术!
deque通常是一组内存块,但前后内存块都已满。这是必要的,因为deque是RandomAccessContainer,并且要获得对任何容器的O(1)访问权限,您不能拥有无限数量的容器来读取大小:
bool size() const { return first.size + (buckets.size()- 2) * SIZE + last.size; }
T& operator[](size_t i) {
if (i < first.size) { return first[SIZE - i]; }
size_t const correctedIndex = i - first.size;
return buckets[correctedIndex / SIZE][correctedIndex % SIZE];
}
由于乘法/除法,这些操作是O(1)!
在我的例子中,我假设当一个内存块包含8个元素时它已满。在实践中,没有人说大小应该是固定的,只是所有内桶应具有相同的尺寸。
// Deque
0: ++
1: ++++++++
2: ++++++++
3: ++++++++
4: +++++
现在说我们想在索引13处插入它。它位于标记为2的桶中。我们可以考虑几种策略:
但是这两种策略会违反所有“内部”桶具有相同数量元素的不变量。
因此,在我们的案例中,我们不得不将元素拖曳到开头或结尾(以较便宜的价格为准):
// Deque
0: +++
1: ++++++++
2: +O++++++
3: ++++++++
4: +++++
注意存储桶0的增长情况。
这种洗牌意味着,在最坏的情况下,你将移动一半元素:O(N / 2)。
deque
在开头或结尾都有O(1)插入,因为这只是在正确的位置添加元素或者(如果存储桶已满)创建新存储桶的问题。
根据B+ Trees,还有其他容器在随机索引处具有更好的插入/擦除行为。在索引的B +树中,您可以而不是“键”(或并行)在内部维护某个位置之前的元素计数。有效地执行此操作有各种技术。最后,您将获得一个容器:
您可以检查Python中的blist
模块,该模块实现了由此类结构支持的类似Python列表的元素。
答案 2 :(得分:2)
你的推测是...... 99.9%是真的。 一切都取决于实际的实施。标准规定的是两个实现者的最低要求(如果不符合规范则不能声称是标准的)和用户(如果编写与实现无关的代码,则不能期望“更好的性能”)。
规范背后的想法是一个未初始化的内存块(一个==一个),其中元素分布在中心周围......直到它们有空间。 插入中间意味着移位。在前端或末端插入意味着只需构建到位。 (当没有空间时,重新分配完成) 修改后,索引和迭代器不可信任,因为我们不能假设已经移位了什么以及在哪个方向上。
更高效的实现不使用单个块,而是使用多个块来重新分配“移位”问题并从底层系统分配恒定大小的内存(从而限制重新分配和分段)。 如果你的目标是其中一个,你可以期待更好的表现,否则你最好不要假设任何结构优化。
答案 3 :(得分:1)
插入元素数量的线性(复制构造)。另外,根据特定的库实现,额外的线性时间可以达到位置和双端队列之一之间的元素数量。 Reference...