一、為什么不是常數擴容而是成倍擴容
首先我們要明確數組是一塊連續的內存,在添加元素的過程中,如果我們的數組存不下了,則需要開辟一塊新的內存,把原來的元素復制到新開辟的地方,具體過程如下:
- 新開辟(allocate)足夠大小的內存
- 把舊元素復制到新的內存中
- 釋放(deallocate)原來的內存
其中第二步需要的時間復雜度為O(n),這樣我們有一個時間和空間的tradeoff,就是說如果我們新開辟的內存很大,一次開辟可以存很多新元素進去,我們reallocation、復制的次數會少,但是很可能會浪費很多空間;如果每次新開辟只比原來的內存大一點,空間浪費很少,不過reallocation、復制的次數會很多。
下面我們比較一下常數擴容和成倍擴容的時間復雜度:
1. 選擇常數擴容,即每次擴容的空間比原來大c
假如說初始數組有1個元素,每次擴容空間增加1,最終擴容成容納n個元素,則每次擴容,復制舊元素和加入新元素,添加元素(append)的次數如下:
擴容次數 數組長度 append次數
0 1 1 (復制0個,新加入1個)
1 2 2 (復制1個,新加入1個)
2 3 3 (復制2個,新加入1個)
... ... ...
n-1 n n
共計 1 + 2 + 3 + ... + n = \(O(n^2)\),均攤到每個元素則是\(O(n)\)。(或者可以這樣理解,到最后n個元素中,第一個元素(最老的元素)被append了n次,第二個元素被append了n-1次,以此類推,最后一個元素被append了1次)
擴容c個只有字母前面倍數的差別,均攤還是\(O(n)\)
2. 選擇成倍擴容
每次擴容2倍,最終擴容成容納n個元素,則復制的次數如圖:
圖中是被copy的次數,我們這里算被append的次數(被copy的次數加1):
最終數組里的n個元素中,n個元素append次數至少是1,n/2個元素append次數至少是2,n/4個元素append次數至少是3,以此類推,則共計 n + n/2 + n/4 + n/8 + ... = \(O(n)\),均攤到每個元素是\(O(1)\)
(解釋一下這個式子,以第二項為例,n/2個元素是包含在n個元素里的,這n/2個元素的1次append已經在第一項里算過一遍了,所以每一項不用乘被append次數)
常數擴容均攤的時間復雜度為\(O(n)\),根據時間復雜度的比較,我們選擇成倍擴容
二、為什么選擇2倍擴容或1.5倍擴容?
不選擇更大倍數的擴容是為了避免浪費更多空間
選擇1.5倍擴容還有一個好處,就是可以使用前面釋放的空間,如圖所示:
第一部分來自 https://www.drdobbs.com/c-made-easier-how-vectors-grow/184401375 解釋了為什么不用常數擴容
第二部分來自 https://blog.csdn.net/qq_44918090/article/details/120583540 面試題:C++vector的動態擴容,為何是1.5倍或者是2倍
第二個鏈接關於擴容問題寫的很全面