为什么C ++向量中的push_back会被摊销?


23

我正在学习C ++,注意到向量的push_back函数的运行时间是恒定的“摊销”。该文档进一步指出:“如果发生重新分配,则重新分配本身在整个大小上都是线性的。”

这不应该意味着push_back函数是,其中是向量的长度吗?毕竟,我们对最坏情况分析感兴趣,对吧?O(n)n

我想至关重要的是,我不知道形容词“摊销”如何改变运行时间。


对于RAM机器,分配个字节的内存不是操作-它被认为是几乎恒定的时间。nO(n)
usul 2013年

Answers:


24

这里重要的词是“摊销”。摊销分析是一种检查操作序列的分析技术。如果整个序列以时间运行,那么序列中的每个操作都以。这样做的想法是,尽管序列中的一些操作可能会很昂贵,但它们的发生频率不足以降低程序的负担。重要的是要注意,这与某些输入分布或随机分析的平均案例分析不同。摊销分析为算法的性能确定了最坏的情况,而与输入无关。它最常用于分析数据结构,该数据结构在整个程序中具有持久状态。nT(n)T(n)/n

给出的最常见示例之一是使用弹出元素的multipop操作分析堆栈。对多播的幼稚分析会说,在最坏的情况下,多播必须花费时间,因为它可能必须弹出堆栈的所有元素。但是,如果查看一系列操作,您会注意到弹出的次数不能超过推动的次数。因此,在操作的任何序列上,pop 的数量都不能超过,因此即使在单个调用可能花费更多时间的情况下,multipop也会以摊销时间运行。kO(n)nO(n)O(1)

现在,这与C ++向量有什么关系?向量是通过数组实现的,因此要增加向量的大小,您必须重新分配内存并复制整个数组。显然,我们不想经常这样做。因此,如果您执行push_back操作,并且向量需要分配更多空间,它将使大小增加倍。现在,这将占用更多的内存,您可能不会完全使用它,但是接下来的几个push_back操作都将以恒定的时间运行。m

现在,如果我们对push_back操作(在这里找到)进行了摊销分析,我们会发现它以固定的摊销时间运行。假设您有项目,并且乘数为。那么重定位的次数大约是。第个重新分配的成本与成正比,大约等于当前数组的大小。因此,推回的总时间为,因为它是一个几何级数。将其除以运算,就可以得出每个运算都需要nmlogm(n)imini=1logm(n)minmm1nmm1,一个常数。最后,您必须谨慎选择因子。如果它太接近于则对于实际应用而言,此常数将变得太大,但是如果太大(例如2),则您将开始浪费大量内存。理想的增长率因应用程序而异,但我认为某些实现使用。m1m1.5


12

尽管@Marc提供了出色的分析(我认为是这样),但有些人可能希望从稍微不同的角度考虑问题。

一种是考虑一种稍微不同的重新分配方式。考虑到一次只复制一个元素,而不是立即将所有元素从旧存储复制到新存储,即,每次执行push_back时,它都会将新元素添加到新空间,并精确复制一个现有元素从旧空间到新空间的元素。假设增长因子为2,则很显然,当新空间已满时,我们已经完成了将所有元素从旧空间复制到新空间的操作,并且每个push_back都完全是恒定时间。到那时,我们将丢弃旧空间,分配一个新的内存块,该内存块的增益是原来的两倍,然后重复该过程。

很明显,我们可以无限期地继续此操作(或者只要有可用的内存就可以),并且每个push_back都将涉及添加一个新元素并复制一个旧元素。

一个典型的实现仍然具有完全相同的副本数-但不是一次复制一个副本,而是一次复制所有现有元素。一方面,您是对的:这确实意味着,如果您查看各个push_back调用,其中某些调用的速度将比其他调用慢得多。但是,如果我们查看长期平均值,则不管向量的大小如何,每次调用push_back都完成的复制量保持恒定。

尽管这与计算复杂性无关,但我认为值得指出的是为什么这样做是有利的,而不是每个push_back复制一个元素,因此每个push_back的时间保持不变。至少要考虑三个原因。

首先是简单的内存可用性。仅在复制完成后才能将旧内存释放以用于其他用途。如果一次仅复制一项,则旧的内存块将保持更长的分配时间。实际上,您将一直在分配一个旧块和一个新块。如果您决定一个小于2的增长因子(通常需要),那么您将需要一直分配更多的内存。

其次,如果您一次只复制一个旧元素,则对数组进行索引会比较棘手-每个索引操作都需要弄清楚给定索引处的元素当前是在旧内存块中还是在旧内存块中。新的一个。无论如何,这并不是十分复杂,但是对于像索引到数组这样的基本操作而言,几乎所有的减速都可能很重要。

第三,通过一次复制所有内容,您可以更好地利用缓存。一次复制所有内容,在大多数情况下,您可以预期源和目标都在高速缓存中,因此,高速缓存未命中的成本将在适合高速缓存行的元素数量中摊销。如果您一次复制一个元素,那么您复制的每个元素都会很容易出现缓存未命中的情况。这只会改变常数因子,而不会改变复杂性,但是它仍然是相当重要的-对于典型的机器,您很容易期望因子为10到20。

暂时还可能需要考虑另一个方向:如果您要设计一个具有实时需求的系统,那么一次只复制一个元素而不是一次复制一个元素可能很有意义。尽管整体速度可能会(或可能不会)降低,但是您仍然需要为一次执行push_back花费很长时间-假设您有一个实时分配器(当然,许多实时系统根本根本禁止动态分配内存,至少在有实时要求的情况下至少如此)。


2
+1这是一个很好的费曼风格的解释。
恢复莫妮卡2014年
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.