Skip to content

Dynamic Array

A dynamic array is an array that grows. It stores its elements in a fixed-size block of memory and, when that block fills up, allocates a larger one and copies everything across. Python's list, Java's ArrayList and C++'s std::vector are all dynamic arrays.

The guide's implementation is DynamicArray. For the other classic way to store a sequence, and how the two compare, see the singly linked list and the doubly linked list.

How it works

The array keeps two numbers: its length, the number of elements in use, and its capacity, the number of slots allocated. Appending writes into the next free slot. When the length reaches the capacity there is no free slot, so the array resizes: it allocates a new block, copies every element into it, and carries on.

A single resize costs O(n), because it copies all n elements. What matters is how often that happens, and that is decided by the growth policy: the rule for choosing the new capacity.

Growth policies

Policy New capacity In the library
Doubling twice the current capacity doubling
Geometric the current capacity times a factor above 1 geometric
Additive the current capacity plus a fixed step additive

Doubling is geometric growth with a factor of 2.

Amortized cost of append

With geometric growth, each resize is bigger than the last, but resizes also become rarer at exactly the same rate. Appending n elements copies fewer than 2n elements in total with doubling, so the average cost per append is constant. This is amortized O(1): an individual append is occasionally expensive, but any long run of appends averages out to a constant each.

With additive growth, resizes never become rarer. A step of 16 copies the whole array every 16 appends, forever, which adds up to about n² / 32 copies for n appends. Each append costs O(n) on average.

The two subsections below count the copies exactly. The formulas they lean on are collected in the math cheat sheet.

Counting the copies: additive growth

Take an array that grows by a fixed step of \(k\) slots and, to keep the arithmetic clean, starts with a capacity of \(k\). Append \(n\) elements and count every element copied by a resize.

When do resizes happen? The capacity is always a multiple of \(k\), and a resize happens when an append finds the array full. So the resizes happen at lengths

\[ k, \; 2k, \; 3k, \; \dots, \; mk \]

where \(m\) is the number of resizes. The last one happens at the largest multiple of \(k\) that is still below \(n\), so

\[ m = \left\lfloor \frac{n - 1}{k} \right\rfloor \]

What does each resize cost? A resize copies every element in the array. The \(i\)-th resize happens at length \(ik\), so it copies \(ik\) elements.

Add them up. The total number of copies \(C(n)\) is the sum over all \(m\) resizes. The step \(k\) is a constant, so it factors out, and what is left is the arithmetic series:

\[ \begin{aligned} C(n) &= \sum_{i=1}^{m} ik \\[4pt] &= k \sum_{i=1}^{m} i \\[4pt] &= k \cdot \frac{m(m+1)}{2} \end{aligned} \]

Put it in terms of \(n\). The number of resizes \(m\) is \(n / k\), give or take rounding. Substituting \(m \approx n / k\):

\[ \begin{aligned} C(n) &\approx \frac{k}{2} \cdot \frac{n}{k} \left( \frac{n}{k} + 1 \right) \\[4pt] &= \frac{n^2}{2k} + \frac{n}{2} \end{aligned} \]

Handling the rounding exactly moves the answer by at most \(n\). For every \(n \ge k\):

\[ \frac{n^2}{2k} - \frac{n}{2} \;\le\; C(n) \;\le\; \frac{n^2}{2k} + \frac{n}{2} \]

The \(n^2\) term dominates both bounds, so

\[ C(n) = \Theta(n^2) \]

The number of copies grows quadratically with the number of appends: doubling \(n\) quadruples the copying. With a step of \(k = 16\) the leading term is \(n^2 / 32\), the figure quoted above.

Amortize. The amortized cost of one append is the total cost of \(n\) appends divided by \(n\). Each append also does one write of its own, which adds \(n\) to the total:

\[ \frac{C(n) + n}{n} \approx \frac{n}{2k} + \frac{3}{2} = \Theta(n) \]

Each append costs linear time on average. The step \(k\) only appears in the denominator of a constant: a larger step makes the line shallower, but it is still a line.

Starting from any initial capacity

If the array starts with a capacity of \(c_0\) instead of \(k\), the resizes happen at lengths \(c_0, \; c_0 + k, \; c_0 + 2k, \; \dots\) and there are \(m = \lceil (n - c_0) / k \rceil\) of them. Numbering them from 0, the sum starts at index 0:

\[ \begin{aligned} C(n) &= \sum_{i=0}^{m-1} (c_0 + ik) \\[4pt] &= \sum_{i=0}^{m-1} c_0 + k \sum_{i=0}^{m-1} i \\[4pt] &= m \, c_0 + k \cdot \frac{(m-1)\,m}{2} \end{aligned} \]

With \(m \approx n / k\) the leading term is still \(n^2 / (2k)\). The initial capacity changes only the lower-order terms.

Counting the copies: doubling

Run the same count for an array that starts with a capacity of 1 and doubles. Now the resizes happen at lengths

\[ 1, \; 2, \; 4, \; \dots, \; 2^{m-1} \]

and the last of them is below \(n\), so \(2^{m-1} < n\). The total is a geometric series:

\[ \begin{aligned} C(n) &= \sum_{i=0}^{m-1} 2^i \\[4pt] &= 2^m - 1 \\[4pt] &< 2n \end{aligned} \]

Fewer than \(2n\) copies for \(n\) appends, so \(C(n) = \Theta(n)\) and the amortized cost of an append is

\[ \frac{C(n) + n}{n} < 3 = \Theta(1) \]

The difference between the two policies is the difference between the two series. An arithmetic series sums to roughly the square of its number of terms, and additive growth has \(n / k\) terms. A geometric series sums to roughly twice its last term, and the last term here is below \(n\).

Measured

The benchmark below appends n integers to an empty array and times the whole run. The first tab divides each time by n, giving the cost of one append: a line that stays flat is constant cost per append, and one that climbs is not. The second tab shows the total time, where the steeper line is the quadratic one. Both axes are logarithmic.

DynamicArray(doubling)DynamicArray(geometric(1.5))DynamicArray(additive(16))list
10 ns100 ns1 µs10 µs1001K10K100K1Mn, the input size (log scale)time per operation (log scale)DynamicArray(doubling), n = 100: 216 nsDynamicArray(doubling), n = 1,000: 217 nsDynamicArray(doubling), n = 10,000: 237 nsDynamicArray(doubling), n = 100,000: 233 nsDynamicArray(doubling), n = 1,000,000: 238 nsDynamicArray(geometric(1.5)), n = 100: 256 nsDynamicArray(geometric(1.5)), n = 1,000: 240 nsDynamicArray(geometric(1.5)), n = 10,000: 252 nsDynamicArray(geometric(1.5)), n = 100,000: 265 nsDynamicArray(geometric(1.5)), n = 1,000,000: 263 nsDynamicArray(additive(16)), n = 1,000: 789 nsDynamicArray(additive(16)), n = 2,000: 1.44 µsDynamicArray(additive(16)), n = 5,000: 3.38 µsDynamicArray(additive(16)), n = 10,000: 6.58 µsDynamicArray(additive(16)), n = 20,000: 13.1 µsDynamicArray(additive(16)), n = 50,000: 32.7 µslist, n = 100: 13.8 nslist, n = 1,000: 13.6 nslist, n = 10,000: 14.5 nslist, n = 100,000: 15.3 nslist, n = 1,000,000: 20.4 ns

n DynamicArray(doubling) DynamicArray(geometric(1.5)) DynamicArray(additive(16)) list
100 216 ns 256 ns 13.8 ns
1,000 217 ns 240 ns 789 ns 13.6 ns
2,000 1.44 µs
5,000 3.38 µs
10,000 237 ns 252 ns 6.58 µs 14.5 ns
20,000 13.1 µs
50,000 32.7 µs
100,000 233 ns 265 ns 15.3 ns
1,000,000 238 ns 263 ns 20.4 ns

DynamicArray(doubling)DynamicArray(geometric(1.5))DynamicArray(additive(16))list
1 µs10 µs100 µs1 ms10 ms100 ms1 s1001K10K100K1Mn, the input size (log scale)total time (log scale)DynamicArray(doubling), n = 100: 21.6 µsDynamicArray(doubling), n = 1,000: 217 µsDynamicArray(doubling), n = 10,000: 2.37 msDynamicArray(doubling), n = 100,000: 23.3 msDynamicArray(doubling), n = 1,000,000: 238 msDynamicArray(geometric(1.5)), n = 100: 25.6 µsDynamicArray(geometric(1.5)), n = 1,000: 240 µsDynamicArray(geometric(1.5)), n = 10,000: 2.52 msDynamicArray(geometric(1.5)), n = 100,000: 26.5 msDynamicArray(geometric(1.5)), n = 1,000,000: 263 msDynamicArray(additive(16)), n = 1,000: 789 µsDynamicArray(additive(16)), n = 2,000: 2.88 msDynamicArray(additive(16)), n = 5,000: 16.9 msDynamicArray(additive(16)), n = 10,000: 65.8 msDynamicArray(additive(16)), n = 20,000: 261 msDynamicArray(additive(16)), n = 50,000: 1.63 slist, n = 100: 1.38 µslist, n = 1,000: 13.6 µslist, n = 10,000: 145 µslist, n = 100,000: 1.53 mslist, n = 1,000,000: 20.4 ms

n DynamicArray(doubling) DynamicArray(geometric(1.5)) DynamicArray(additive(16)) list
100 21.6 µs 25.6 µs 1.38 µs
1,000 217 µs 240 µs 789 µs 13.6 µs
2,000 2.88 ms
5,000 16.9 ms
10,000 2.37 ms 2.52 ms 65.8 ms 145 µs
20,000 261 ms
50,000 1.63 s
100,000 23.3 ms 26.5 ms 1.53 ms
1,000,000 238 ms 263 ms 20.4 ms
slope 1.01 1.01 1.95 1.04

Measured on cs-survival-kit 0.8.0 · CPython 3.13.16 · Linux x86_64 · 2026-10-08. Each time is the best of five runs. The slope is fitted on a log-log scale: about 1 is linear, about 2 is quadratic. Both chart axes are logarithmic, so a power law is a straight line and its steepness is that slope.

Both geometric policies stay flat as n grows by four orders of magnitude, just like the built-in list. The additive column grows in step with n. list is faster throughout because it is implemented in C; what the two share is the shape of the curve.

Choosing a factor

Any factor above 1 gives amortized O(1) appends. The factor trades time for memory: a larger one resizes less often, but leaves more of the capacity unused straight after a resize.

geometric(1.25)geometric(1.5)geometric(2.0)geometric(3.0)
100 ns200 ns500 ns1001K10K100K1Mn, the input size (log scale)time per operation (log scale)geometric(1.25), n = 100: 310 nsgeometric(1.25), n = 1,000: 282 nsgeometric(1.25), n = 10,000: 286 nsgeometric(1.25), n = 100,000: 305 nsgeometric(1.25), n = 1,000,000: 313 nsgeometric(1.5), n = 100: 252 nsgeometric(1.5), n = 1,000: 237 nsgeometric(1.5), n = 10,000: 250 nsgeometric(1.5), n = 100,000: 263 nsgeometric(1.5), n = 1,000,000: 264 nsgeometric(2.0), n = 100: 217 nsgeometric(2.0), n = 1,000: 217 nsgeometric(2.0), n = 10,000: 237 nsgeometric(2.0), n = 100,000: 234 nsgeometric(2.0), n = 1,000,000: 239 nsgeometric(3.0), n = 100: 196 nsgeometric(3.0), n = 1,000: 227 nsgeometric(3.0), n = 10,000: 228 nsgeometric(3.0), n = 100,000: 228 nsgeometric(3.0), n = 1,000,000: 241 ns

n geometric(1.25) geometric(1.5) geometric(2.0) geometric(3.0)
100 310 ns 252 ns 217 ns 196 ns
1,000 282 ns 237 ns 217 ns 227 ns
10,000 286 ns 250 ns 237 ns 228 ns
100,000 305 ns 263 ns 234 ns 228 ns
1,000,000 313 ns 264 ns 239 ns 241 ns

geometric(1.25)geometric(1.5)geometric(2.0)geometric(3.0)
10 µs100 µs1 ms10 ms100 ms1001K10K100K1Mn, the input size (log scale)total time (log scale)geometric(1.25), n = 100: 31 µsgeometric(1.25), n = 1,000: 282 µsgeometric(1.25), n = 10,000: 2.86 msgeometric(1.25), n = 100,000: 30.5 msgeometric(1.25), n = 1,000,000: 313 msgeometric(1.5), n = 100: 25.2 µsgeometric(1.5), n = 1,000: 237 µsgeometric(1.5), n = 10,000: 2.5 msgeometric(1.5), n = 100,000: 26.3 msgeometric(1.5), n = 1,000,000: 264 msgeometric(2.0), n = 100: 21.7 µsgeometric(2.0), n = 1,000: 217 µsgeometric(2.0), n = 10,000: 2.37 msgeometric(2.0), n = 100,000: 23.4 msgeometric(2.0), n = 1,000,000: 239 msgeometric(3.0), n = 100: 19.6 µsgeometric(3.0), n = 1,000: 227 µsgeometric(3.0), n = 10,000: 2.28 msgeometric(3.0), n = 100,000: 22.8 msgeometric(3.0), n = 1,000,000: 241 ms

n geometric(1.25) geometric(1.5) geometric(2.0) geometric(3.0)
100 31 µs 25.2 µs 21.7 µs 19.6 µs
1,000 282 µs 237 µs 217 µs 227 µs
10,000 2.86 ms 2.5 ms 2.37 ms 2.28 ms
100,000 30.5 ms 26.3 ms 23.4 ms 22.8 ms
1,000,000 313 ms 264 ms 239 ms 241 ms
slope 1.00 1.01 1.01 1.02

Measured on cs-survival-kit 0.8.0 · CPython 3.13.16 · Linux x86_64 · 2026-10-08. Each time is the best of five runs. The slope is fitted on a log-log scale: about 1 is linear, about 2 is quadratic. Both chart axes are logarithmic, so a power law is a straight line and its steepness is that slope.

A larger step does not fix additive growth

It is tempting to think a big enough step makes additive growth acceptable. A larger step does start out cheap, but the cost per append still rises with n. It delays the quadratic behaviour without removing it.

additive(16)additive(256)additive(4096)doubling
1 µs10 µs1K10K100K1Mn, the input size (log scale)time per operation (log scale)additive(16), n = 1,000: 812 nsadditive(16), n = 2,000: 1.45 µsadditive(16), n = 5,000: 3.42 µsadditive(16), n = 10,000: 6.65 µsadditive(16), n = 20,000: 13.2 µsadditive(16), n = 50,000: 33 µsadditive(256), n = 5,000: 405 nsadditive(256), n = 10,000: 620 nsadditive(256), n = 20,000: 1.03 µsadditive(256), n = 50,000: 2.24 µsadditive(256), n = 100,000: 4.37 µsadditive(256), n = 200,000: 8.66 µsadditive(4096), n = 50,000: 343 nsadditive(4096), n = 100,000: 478 nsadditive(4096), n = 200,000: 738 nsadditive(4096), n = 500,000: 1.58 µsadditive(4096), n = 1,000,000: 2.96 µsdoubling, n = 1,000: 219 nsdoubling, n = 10,000: 240 nsdoubling, n = 100,000: 241 nsdoubling, n = 1,000,000: 245 ns

n additive(16) additive(256) additive(4096) doubling
1,000 812 ns 219 ns
2,000 1.45 µs
5,000 3.42 µs 405 ns
10,000 6.65 µs 620 ns 240 ns
20,000 13.2 µs 1.03 µs
50,000 33 µs 2.24 µs 343 ns
100,000 4.37 µs 478 ns 241 ns
200,000 8.66 µs 738 ns
500,000 1.58 µs
1,000,000 2.96 µs 245 ns

additive(16)additive(256)additive(4096)doubling
1 ms10 ms100 ms1 s1K10K100K1Mn, the input size (log scale)total time (log scale)additive(16), n = 1,000: 812 µsadditive(16), n = 2,000: 2.89 msadditive(16), n = 5,000: 17.1 msadditive(16), n = 10,000: 66.5 msadditive(16), n = 20,000: 265 msadditive(16), n = 50,000: 1.65 sadditive(256), n = 5,000: 2.03 msadditive(256), n = 10,000: 6.2 msadditive(256), n = 20,000: 20.6 msadditive(256), n = 50,000: 112 msadditive(256), n = 100,000: 437 msadditive(256), n = 200,000: 1.73 sadditive(4096), n = 50,000: 17.2 msadditive(4096), n = 100,000: 47.8 msadditive(4096), n = 200,000: 148 msadditive(4096), n = 500,000: 788 msadditive(4096), n = 1,000,000: 2.96 sdoubling, n = 1,000: 219 µsdoubling, n = 10,000: 2.4 msdoubling, n = 100,000: 24.1 msdoubling, n = 1,000,000: 245 ms

n additive(16) additive(256) additive(4096) doubling
1,000 812 µs 219 µs
2,000 2.89 ms
5,000 17.1 ms 2.03 ms
10,000 66.5 ms 6.2 ms 2.4 ms
20,000 265 ms 20.6 ms
50,000 1.65 s 112 ms 17.2 ms
100,000 437 ms 47.8 ms 24.1 ms
200,000 1.73 s 148 ms
500,000 788 ms
1,000,000 2.96 s 245 ms
slope 1.95 1.84 1.73 1.02

Measured on cs-survival-kit 0.8.0 · CPython 3.13.16 · Linux x86_64 · 2026-10-08. Each time is the best of five runs. The slope is fitted on a log-log scale: about 1 is linear, about 2 is quadratic. Both chart axes are logarithmic, so a power law is a straight line and its steepness is that slope.

Complexity

Operation Time Notes
Read or write by index O(1)
Append O(1) amortized O(n) for the append that triggers a resize
Pop from the end O(1)
Iterate O(n)

These assume a geometric growth policy.