vector容器的扩容原理

【常规】

push_back的话,一般来说,都是按两倍来扩容,因为push_back每次都是只插入一个数据

insert的话,因为可以一次插入多个数据,所以要复杂一些。

触发扩容时,如果要插入的数据量比旧容量小,则按两倍扩容;如果要插入的数据量比原来的旧容量还要大,即表示即使按两倍扩容了,依然存不下要插入的数据,此时将会按照旧容量加要插入的数据量来扩容,保证一次扩容就能容下要插入的数据;
即 new_capacity = max ( old_capacity * 2, n + old_capacity )。

【进阶】

其实,那个两倍也不是一定的,C++标准并没有规定vector到底应该按多少来扩容,有一些选择2,有一些选择1.5。那么一般是根据什么来选择这个扩容因子的呢?

确定扩容因子的大小,主要从时间和空间的角度来分析。

从空间上来分析,扩容因子越大,意味着预留空间越大,浪费的空间也越多,所以从空间考虑,扩容因子因越小越好。

从时间上来分析,如果预留空间不足的话,就需要重新开辟一段空间,把原有的数据复制到新空间,如果扩容因子无限大的话,那显然就不再需要额外开辟空间了。所以时间角度看,扩容因子越大越好。

还可以从摊还分析的角度来分析,为了简化分析,就不考虑一次性插入大量数据的情况,只考虑push_back的情况:

假设扩容因子为K,vector最后的长度为N

显然每次常规插入操作需要1的时间,插入n个数据的常规时间为N

扫描二维码关注公众号,回复: 10163436 查看本文章

现在考虑额外的消耗,扩容因子为K,意味着一共需要扩容logkN次(以K为底,N的对数),为简化计算,假设是容量是从1开始,也就是要对下面的等比序列求和

S = 1 + k + k^2 + k^3 + k^4  + …… +  k^(logkN) = (N-1)/(K-1)

显然,也就意味着扩容因子K越大,额外的消耗越小。总消耗为N+(N-1)/(K-1) 。当K大于1时,K越大总消耗越小。

[BYTHEWAY]

顺便可以再求一下push_back的摊还时间复杂度。即为(N+(N-1)/(K-1))/N = (NK-1)/(K-1)

最普遍的情况下,当扩容因子为2时,最好的评价时间复杂度为2N,发生在N等于2的n次幂时,最差为3N,发生在N等于2的n次幂加1时。

【再进阶】

既然从时间上考虑K越大越好,从空间考虑K越小越好,那么有没有一个数为最佳,同时协调好空间与时间呢?

我们从内存上来考虑,vector数据实际存放在堆上,vector的每次扩容需要把旧的释放,再重新开辟一段新的连续内存。

我们来看一下K = 2时的情况。

每次扩容后capacity的情况如下:1,2,4,8,16,32 ……..

当我们释放了4的空间,我们寻找8的新空间,再次扩容,释放8,寻找16。。

仔细分析,第5次扩容时,需要寻找16的新空间,第4次释放了8,第3次释放了4,第2次释放了2,第1次释放了1,所以 1 + 2 + 4 + 8 = 15  < 16,也就意味着,之前释放的空间,永远无法被下一次的扩容利用,这对内存与cache是非常不友好的。

我们再来看一下K = 1.5的情况。

每次扩容之后capacity的情况为:1,2,3,4,6,9,13,19,28 ……

再按刚才的思路分析一遍,1 + 2 >= 3;  2 + 3 + 4 >= 6;  6 + 9 >= 13 …….

所以,当K为1.5时,显然对内存和cache要友好很多,至少从容量上来说,是存在重复利用的可能性的。

因此,我们可以得出结论,当K = 2时,时间上要比 K = 1.5 占优,而空间上比 1.5 稍有劣势。

 

那么,那个最佳的数是多少呢?

继续刚才的分析,我们希望的是,上几次的空间,存在被下一次扩容时利用的可能性。

也就是 X(n-2) + X(n-1) >= X(n),显然我们也希望时间上也要更好,即X(n-2) + X(n-1) = X(n)

即:1,2,3,5,8,13,21,34,55 。。。。

是不是很熟悉。。。是的,这就是我们的斐波那契数列。。。

那么当N趋于无限大时,取极限,最佳的扩容因子也就是那个最美的数,黄金分割率,1.618。

发布了33 篇原创文章 · 获赞 6 · 访问量 553

猜你喜欢

转载自blog.csdn.net/weixin_43519984/article/details/104594417