前言
對於 std::vector
的 push_back
函數, cplusplus.com 上的復雜度解釋如下:
Constant (amortized time, reallocation may happen).
常數 (均攤時間, 可能發生重新分配)
它的原理想必大家都知道, 當大小達到容量之后, 為了保證內存的連續性, 就會再開一個新的內存塊, 把之前的復制過去。
每次復制時間復雜度為 \(O(n)\), 直覺上, 每次 push_back
的時間復雜度不太像 \(O(1)\), 但由於只有很少的情況下才會復制, 所以均攤時間確實很快, 但至於為什么是 \(O(1)\), 本篇文章將給出證明。
聚能分析
此代碼打印 push_back
\(1 \times 10 ^ 5\) 個元素時 vector
的容量大小變化:
#include <iostream>
#include <vector>
int main() {
std::vector<int> v;
int last = 0;
for (int i = 1; i <= 1e5; i++) {
v.push_back(1);
if (last != (int)v.capacity()) {
std::cout << v.capacity() << " ";
last = v.capacity();
}
}
}
實際運行時, 會輸出 1 2 4 8 16 32 64 128 256 512 1024 2048 4096 8192 16384 32768 65536 131072
很明顯都是2的次冪, 所以, 每次插入代價
則
共有 \(n\) 次插入操作, 總時間復雜度為 \(O(3n)\), 單次均攤時間復雜度為 \(O(3)\) 。
核算法
考慮每次 push_back
要產生的代價, 可能不一樣, 有的時候只需要付出插入1個元素的1代價, 有的時候卻要付出復制n個元素的n代價, 我們只要在每次插入元素的時候提前付出代價, 為之后可能的復制元素做好准備。 復制元素的時候用之前存儲的代價支付, 就相當於每次插入元素代價相同。
那么每次插入元素都要付出什么代價呢?
- 插入元素本身的1代價, 不做解釋。
- 為將來復制這個元素付出的1代價。 因為移動一個元素要付出1代價, 而如果這1代價在插入的時候就已經支付, 那么移動的時候就不用付出額外代價
- 為已經復制過的元素將來再次復制所付出的代價。如果僅僅為自己的復制付出代價, 那么之前復制過來的元素已經消耗了自己插入時付出的代價, 它們將無法復制, 所以必須要再為它們付出1次代價
此圖畫出了2次復制, 下標為3的元素, 要為自己的插入, 將來自己的復制和將來下標為1的元素的復制付出3次代價。
同理, 下標為4的元素, 要為自己的插入, 將來自己的復制和將來下標為2的元素的復制付出3次代價。
這是第1次復制, 第二次復制的時候, 下標為1, 2, 3, 4的元素又有5, 6, 7, 8為它們付出代價。
所以每次插入元素付出了3次代價, 均攤時間復雜度為\(O(3)\)。
還有需要注意的一點, 如果插入了幾個元素但還沒有復制的時候, 還存儲着一些沒有被消耗的代價, 此時總時間是小於 \(3n\) 的, 只有剛完成一次復制時, 時間才是 \(3n\), 同時, 存儲的代價也為0。所以有: 當前總時間 = 均攤總時間 - 存儲的代價
其他
但是由於STL自帶大常數, 不開啟 \(O2\) 優化的情況下, \(vector\) 插入的時間是數組的 6.5 倍左右, 開啟 \(O2\) 優化的情況下, \(vector\) 插入的時間是數組的 2.5 倍左右, 較接近 \(O(3)\) 。