Dynamic arrays
A dynamic array hides a fixed array behind a size and a capacity, growing it geometrically when it fills up. This lesson explains capacity expansion, why geometric growth gives amortised O(1) appends, the internal buffer, shrinking and the memory overhead you pay.
Learning goals
- Explain size versus capacity
- Compare growth policies and their costs
- Describe how ArrayList manages its buffer
- Estimate the memory overhead of a dynamic array
Prerequisites
Capacity expansion
When size reaches capacity, a dynamic array allocates a bigger buffer, copies the existing elements and continues. The copy costs O(size), but it happens rarely. Try the three growth policies in the animation and watch the copy counters.
Doubling is the textbook rule; java.util.ArrayList grows by 50%.
1void add(int x) {2if (size == data.length) data = Arrays.copyOf(data, Math.max(1, data.length * 2));3data[size++] = x;4}
Dynamic array: capacity, growth and shrinking
Interactive · edit the inputsize elements in the buffer · length 0
data[] · capacity 1
A new dynamic array starts with capacity 1 and no elements.
1void add(int x) {2if (size == data.length) { // buffer is full3data = Arrays.copyOf(data, newCapacity()); // allocate bigger, copy size elements4}5data[size++] = x;6}7int removeLast() {8int x = data[--size];9if (size <= data.length / 4) data = Arrays.copyOf(data, Math.max(1, data.length / 2)); // optional shrink10return x;11}
Live state
size
0
capacity
1
Execution trace (dry run table)Show 21 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | size=0 capacity=1 | A new dynamic array starts with capacity 1 and no elements. |
| 2 | L2 | size=0 capacity=1 x=4 | size 0 < capacity 1: there is a spare slot, so adding is O(1). |
| 3 | L5 | size=1 capacity=1 x=4 | Write 4 into data[0] and increase size to 1. |
| 4 | L2 | size=1 capacity=1 x=8 | size 1 equals capacity 1: no room for 8, so the buffer must grow. |
| 5 | L3 | size=1 capacity=2 | Allocate capacity 2 and copy the 1 element (1 copy so far). |
| 6 | L5 | size=2 capacity=2 x=8 | Write 8 into data[1] and increase size to 2. |
| 7 | L2 | size=2 capacity=2 x=15 | size 2 equals capacity 2: no room for 15, so the buffer must grow. |
| 8 | L3 | size=2 capacity=4 | Allocate capacity 4 and copy the 2 elements (3 copys so far). |
| 9 | L5 | size=3 capacity=4 x=15 | Write 15 into data[2] and increase size to 3. |
| 10 | L2 | size=3 capacity=4 x=16 | size 3 < capacity 4: there is a spare slot, so adding is O(1). |
| 11 | L5 | size=4 capacity=4 x=16 | Write 16 into data[3] and increase size to 4. |
| 12 | L2 | size=4 capacity=4 x=23 | size 4 equals capacity 4: no room for 23, so the buffer must grow. |
| 13 | L3 | size=4 capacity=8 | Allocate capacity 8 and copy the 4 elements (7 copys so far). |
| 14 | L5 | size=5 capacity=8 x=23 | Write 23 into data[4] and increase size to 5. |
| 15 | L2 | size=5 capacity=8 x=42 | size 5 < capacity 8: there is a spare slot, so adding is O(1). |
| 16 | L5 | size=6 capacity=8 x=42 | Write 42 into data[5] and increase size to 6. |
| 17 | L2 | size=6 capacity=8 x=7 | size 6 < capacity 8: there is a spare slot, so adding is O(1). |
| 18 | L5 | size=7 capacity=8 x=7 | Write 7 into data[6] and increase size to 7. |
| 19 | L2 | size=7 capacity=8 x=9 | size 7 < capacity 8: there is a spare slot, so adding is O(1). |
| 20 | L5 | size=8 capacity=8 x=9 | Write 9 into data[7] and increase size to 8. |
| 21 | L11 | size=8 capacity=8 copies=7 | Final: size 8, capacity 8 (0 spare slots), 3 resizes that copied 7 elements in total. Doubling keeps total copies below 2n. |
Geometric growth
Geometric growth multiplies the capacity by a constant factor (2, 1.5, ...). Because the capacities form a geometric series, the total copy work for n appends is a constant multiple of n: with doubling, 1 + 2 + 4 + ... + n/2 < n. Growing by a fixed step (+10) instead makes the copies add up to about n²/20.
Pick the growth policy and compare the total copies.
| Policy | Capacity after growth | Total copy work for n appends |
|---|---|---|
| Doubling | 2 × old | < n |
| ×1.5 (ArrayList) | old + old / 2 | up to about 2n |
| Fixed step c | old + c | about n² / (2c) |
Dynamic array: amortised cost of append
Interactive · edit the inputbacking array (capacity 1)
cost of each append
Start with capacity 1. Each append costs 1 write, plus size copies whenever the array is full. Policy: Double (×2).
1void add(int x) {2if (size == data.length) { // full: allocate a bigger array3data = Arrays.copyOf(data, newCapacity(data.length)); // copies `size` elements4}5data[size++] = x; // the cheap part: 1 write6}7// doubling: newCapacity = 2 * capacity; grow-by-1: capacity + 1; Java ArrayList: capacity + capacity / 2
Live state
capacity
1
size
0
Execution trace (dry run table)Show 24 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | capacity=1 size=0 | Start with capacity 1. Each append costs 1 write, plus size copies whenever the array is full. Policy: Double (×2). |
| 2 | L5 | size=1 capacity=1 cost of this append=1 total cost=1 amortised=1 | Append #1 costs 1. Total so far 1, so the average is 1.00 per append. |
| 3 | L3 | capacity=1 new capacity=2 copies=1 | Full: allocate 2 slots and copy 1 element. This single append costs 2. |
| 4 | L5 | size=2 capacity=2 cost of this append=2 total cost=3 amortised=1.5 | Append #2 costs 2. Total so far 3, so the average is 1.50 per append. |
| 5 | L3 | capacity=2 new capacity=4 copies=2 | Full: allocate 4 slots and copy 2 elements. This single append costs 3. |
| 6 | L5 | size=3 capacity=4 cost of this append=3 total cost=6 amortised=2 | Append #3 costs 3. Total so far 6, so the average is 2.00 per append. |
| 7 | L5 | size=4 capacity=4 cost of this append=1 total cost=7 amortised=1.75 | Append #4 costs 1. Total so far 7, so the average is 1.75 per append. |
| 8 | L3 | capacity=4 new capacity=8 copies=4 | Full: allocate 8 slots and copy 4 elements. This single append costs 5. |
| 9 | L5 | size=5 capacity=8 cost of this append=5 total cost=12 amortised=2.4 | Append #5 costs 5. Total so far 12, so the average is 2.40 per append. |
| 10 | L5 | size=6 capacity=8 cost of this append=1 total cost=13 amortised=2.17 | Append #6 costs 1. Total so far 13, so the average is 2.17 per append. |
| 11 | L5 | size=7 capacity=8 cost of this append=1 total cost=14 amortised=2 | Append #7 costs 1. Total so far 14, so the average is 2.00 per append. |
| 12 | L5 | size=8 capacity=8 cost of this append=1 total cost=15 amortised=1.88 | Append #8 costs 1. Total so far 15, so the average is 1.88 per append. |
| 13 | L3 | capacity=8 new capacity=16 copies=8 | Full: allocate 16 slots and copy 8 elements. This single append costs 9. |
| 14 | L5 | size=9 capacity=16 cost of this append=9 total cost=24 amortised=2.67 | Append #9 costs 9. Total so far 24, so the average is 2.67 per append. |
| 15 | L5 | size=10 capacity=16 cost of this append=1 total cost=25 amortised=2.5 | Append #10 costs 1. Total so far 25, so the average is 2.50 per append. |
| 16 | L5 | size=11 capacity=16 cost of this append=1 total cost=26 amortised=2.36 | Append #11 costs 1. Total so far 26, so the average is 2.36 per append. |
| 17 | L5 | size=12 capacity=16 cost of this append=1 total cost=27 amortised=2.25 | Append #12 costs 1. Total so far 27, so the average is 2.25 per append. |
| 18 | L5 | size=13 capacity=16 cost of this append=1 total cost=28 amortised=2.15 | Append #13 costs 1. Total so far 28, so the average is 2.15 per append. |
| 19 | L5 | size=14 capacity=16 cost of this append=1 total cost=29 amortised=2.07 | Append #14 costs 1. Total so far 29, so the average is 2.07 per append. |
| 20 | L5 | size=15 capacity=16 cost of this append=1 total cost=30 amortised=2 | Append #15 costs 1. Total so far 30, so the average is 2.00 per append. |
| 21 | L5 | size=16 capacity=16 cost of this append=1 total cost=31 amortised=1.94 | Append #16 costs 1. Total so far 31, so the average is 1.94 per append. |
| 22 | L3 | capacity=16 new capacity=32 copies=16 | Full: allocate 32 slots and copy 16 elements. This single append costs 17. |
| 23 | L5 | size=17 capacity=32 cost of this append=17 total cost=48 amortised=2.82 | Append #17 costs 17. Total so far 48, so the average is 2.82 per append. |
| 24 | L7 | appends=17 total cost=48 return=2.824 | Doubling: total cost 48 < 3n = 51, so the amortised cost per append is O(1) (here 2.82). The expensive copies are rare and pay for themselves: aggregate analysis. Accounting method: charge 3 coins per append: 1 for the write, 1 to copy itself later, 1 to copy an older element. |
Amortized append
An individual append can cost O(n) when it triggers a resize, but any sequence of n appends costs O(n) in total, so the amortised cost per append is O(1). The guarantee is about the whole sequence, not about the average input.
The chart plots the cost of each append; the occasional tall bar is paid for by the many cheap ones.
Dynamic array: amortised cost of append
Interactive · edit the inputbacking array (capacity 1)
cost of each append
Start with capacity 1. Each append costs 1 write, plus size copies whenever the array is full. Policy: Double (×2).
1void add(int x) {2if (size == data.length) { // full: allocate a bigger array3data = Arrays.copyOf(data, newCapacity(data.length)); // copies `size` elements4}5data[size++] = x; // the cheap part: 1 write6}7// doubling: newCapacity = 2 * capacity; grow-by-1: capacity + 1; Java ArrayList: capacity + capacity / 2
Live state
capacity
1
size
0
Execution trace (dry run table)Show 24 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | capacity=1 size=0 | Start with capacity 1. Each append costs 1 write, plus size copies whenever the array is full. Policy: Double (×2). |
| 2 | L5 | size=1 capacity=1 cost of this append=1 total cost=1 amortised=1 | Append #1 costs 1. Total so far 1, so the average is 1.00 per append. |
| 3 | L3 | capacity=1 new capacity=2 copies=1 | Full: allocate 2 slots and copy 1 element. This single append costs 2. |
| 4 | L5 | size=2 capacity=2 cost of this append=2 total cost=3 amortised=1.5 | Append #2 costs 2. Total so far 3, so the average is 1.50 per append. |
| 5 | L3 | capacity=2 new capacity=4 copies=2 | Full: allocate 4 slots and copy 2 elements. This single append costs 3. |
| 6 | L5 | size=3 capacity=4 cost of this append=3 total cost=6 amortised=2 | Append #3 costs 3. Total so far 6, so the average is 2.00 per append. |
| 7 | L5 | size=4 capacity=4 cost of this append=1 total cost=7 amortised=1.75 | Append #4 costs 1. Total so far 7, so the average is 1.75 per append. |
| 8 | L3 | capacity=4 new capacity=8 copies=4 | Full: allocate 8 slots and copy 4 elements. This single append costs 5. |
| 9 | L5 | size=5 capacity=8 cost of this append=5 total cost=12 amortised=2.4 | Append #5 costs 5. Total so far 12, so the average is 2.40 per append. |
| 10 | L5 | size=6 capacity=8 cost of this append=1 total cost=13 amortised=2.17 | Append #6 costs 1. Total so far 13, so the average is 2.17 per append. |
| 11 | L5 | size=7 capacity=8 cost of this append=1 total cost=14 amortised=2 | Append #7 costs 1. Total so far 14, so the average is 2.00 per append. |
| 12 | L5 | size=8 capacity=8 cost of this append=1 total cost=15 amortised=1.88 | Append #8 costs 1. Total so far 15, so the average is 1.88 per append. |
| 13 | L3 | capacity=8 new capacity=16 copies=8 | Full: allocate 16 slots and copy 8 elements. This single append costs 9. |
| 14 | L5 | size=9 capacity=16 cost of this append=9 total cost=24 amortised=2.67 | Append #9 costs 9. Total so far 24, so the average is 2.67 per append. |
| 15 | L5 | size=10 capacity=16 cost of this append=1 total cost=25 amortised=2.5 | Append #10 costs 1. Total so far 25, so the average is 2.50 per append. |
| 16 | L5 | size=11 capacity=16 cost of this append=1 total cost=26 amortised=2.36 | Append #11 costs 1. Total so far 26, so the average is 2.36 per append. |
| 17 | L5 | size=12 capacity=16 cost of this append=1 total cost=27 amortised=2.25 | Append #12 costs 1. Total so far 27, so the average is 2.25 per append. |
| 18 | L5 | size=13 capacity=16 cost of this append=1 total cost=28 amortised=2.15 | Append #13 costs 1. Total so far 28, so the average is 2.15 per append. |
| 19 | L5 | size=14 capacity=16 cost of this append=1 total cost=29 amortised=2.07 | Append #14 costs 1. Total so far 29, so the average is 2.07 per append. |
| 20 | L5 | size=15 capacity=16 cost of this append=1 total cost=30 amortised=2 | Append #15 costs 1. Total so far 30, so the average is 2.00 per append. |
| 21 | L5 | size=16 capacity=16 cost of this append=1 total cost=31 amortised=1.94 | Append #16 costs 1. Total so far 31, so the average is 1.94 per append. |
| 22 | L3 | capacity=16 new capacity=32 copies=16 | Full: allocate 32 slots and copy 16 elements. This single append costs 17. |
| 23 | L5 | size=17 capacity=32 cost of this append=17 total cost=48 amortised=2.82 | Append #17 costs 17. Total so far 48, so the average is 2.82 per append. |
| 24 | L7 | appends=17 total cost=48 return=2.824 | Doubling: total cost 48 < 3n = 51, so the amortised cost per append is O(1) (here 2.82). The expensive copies are rare and pay for themselves: aggregate analysis. Accounting method: charge 3 coins per append: 1 for the write, 1 to copy itself later, 1 to copy an older element. |
Capacity vs size
Size is how many elements the user has stored; capacity is how many the buffer can hold. Always size ≤ capacity. ArrayList.size() reports the size and there is no public capacity getter, but you can reserve with ensureCapacity and release spare room with trimToSize.
Presizing with the expected count avoids every intermediate resize.
1List<Integer> xs = new ArrayList<>(1000); // capacity 1000, size 02xs.add(1); // size 13ArrayList<Integer> big = new ArrayList<>();4big.ensureCapacity(5000); // one allocation up front5big.trimToSize(); // capacity shrinks to size
Dynamic array: capacity, growth and shrinking
Interactive · edit the inputsize elements in the buffer · length 0
data[] · capacity 1
A new dynamic array starts with capacity 1 and no elements.
1void add(int x) {2if (size == data.length) { // buffer is full3data = Arrays.copyOf(data, newCapacity()); // allocate bigger, copy size elements4}5data[size++] = x;6}7int removeLast() {8int x = data[--size];9if (size <= data.length / 4) data = Arrays.copyOf(data, Math.max(1, data.length / 2)); // optional shrink10return x;11}
Live state
size
0
capacity
1
Execution trace (dry run table)Show 21 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | size=0 capacity=1 | A new dynamic array starts with capacity 1 and no elements. |
| 2 | L2 | size=0 capacity=1 x=4 | size 0 < capacity 1: there is a spare slot, so adding is O(1). |
| 3 | L5 | size=1 capacity=1 x=4 | Write 4 into data[0] and increase size to 1. |
| 4 | L2 | size=1 capacity=1 x=8 | size 1 equals capacity 1: no room for 8, so the buffer must grow. |
| 5 | L3 | size=1 capacity=2 | Allocate capacity 2 and copy the 1 element (1 copy so far). |
| 6 | L5 | size=2 capacity=2 x=8 | Write 8 into data[1] and increase size to 2. |
| 7 | L2 | size=2 capacity=2 x=15 | size 2 equals capacity 2: no room for 15, so the buffer must grow. |
| 8 | L3 | size=2 capacity=4 | Allocate capacity 4 and copy the 2 elements (3 copys so far). |
| 9 | L5 | size=3 capacity=4 x=15 | Write 15 into data[2] and increase size to 3. |
| 10 | L2 | size=3 capacity=4 x=16 | size 3 < capacity 4: there is a spare slot, so adding is O(1). |
| 11 | L5 | size=4 capacity=4 x=16 | Write 16 into data[3] and increase size to 4. |
| 12 | L2 | size=4 capacity=4 x=23 | size 4 equals capacity 4: no room for 23, so the buffer must grow. |
| 13 | L3 | size=4 capacity=8 | Allocate capacity 8 and copy the 4 elements (7 copys so far). |
| 14 | L5 | size=5 capacity=8 x=23 | Write 23 into data[4] and increase size to 5. |
| 15 | L2 | size=5 capacity=8 x=42 | size 5 < capacity 8: there is a spare slot, so adding is O(1). |
| 16 | L5 | size=6 capacity=8 x=42 | Write 42 into data[5] and increase size to 6. |
| 17 | L2 | size=6 capacity=8 x=7 | size 6 < capacity 8: there is a spare slot, so adding is O(1). |
| 18 | L5 | size=7 capacity=8 x=7 | Write 7 into data[6] and increase size to 7. |
| 19 | L2 | size=7 capacity=8 x=9 | size 7 < capacity 8: there is a spare slot, so adding is O(1). |
| 20 | L5 | size=8 capacity=8 x=9 | Write 9 into data[7] and increase size to 8. |
| 21 | L11 | size=8 capacity=8 copies=7 | Final: size 8, capacity 8 (0 spare slots), 3 resizes that copied 7 elements in total. Doubling keeps total copies below 2n. |
Internal buffer
Inside, a dynamic array is just an array plus a counter. ArrayList keeps an Object[] elementData and an int size; its first add allocates capacity 10, and growth is old + (old >> 1). Elements at index size and beyond are null so the garbage collector can reclaim removed objects.
A minimal version shows every part.
1class IntList {2private int[] data = new int[4];3private int size;4void add(int x) {5if (size == data.length) data = Arrays.copyOf(data, size * 2);6data[size++] = x;7}8int get(int i) {9if (i < 0 || i >= size) throw new IndexOutOfBoundsException("index " + i + ", size " + size);10return data[i];11}12int size() { return size; }13}
Shrinking
Shrinking releases memory after many removals. A naive rule, halve when size drops to half of capacity, can thrash: alternating add and remove at the boundary would resize every time. The standard fix is to shrink only when size falls to a quarter of capacity, halving the buffer so it ends up half full.
Use the removal field in the doubling policy to see the shrink step. ArrayList itself never shrinks automatically; call trimToSize() if memory matters.
Dynamic array: capacity, growth and shrinking
Interactive · edit the inputsize elements in the buffer · length 0
data[] · capacity 1
A new dynamic array starts with capacity 1 and no elements.
1void add(int x) {2if (size == data.length) { // buffer is full3data = Arrays.copyOf(data, newCapacity()); // allocate bigger, copy size elements4}5data[size++] = x;6}7int removeLast() {8int x = data[--size];9if (size <= data.length / 4) data = Arrays.copyOf(data, Math.max(1, data.length / 2)); // optional shrink10return x;11}
Live state
size
0
capacity
1
Execution trace (dry run table)Show 21 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | size=0 capacity=1 | A new dynamic array starts with capacity 1 and no elements. |
| 2 | L2 | size=0 capacity=1 x=4 | size 0 < capacity 1: there is a spare slot, so adding is O(1). |
| 3 | L5 | size=1 capacity=1 x=4 | Write 4 into data[0] and increase size to 1. |
| 4 | L2 | size=1 capacity=1 x=8 | size 1 equals capacity 1: no room for 8, so the buffer must grow. |
| 5 | L3 | size=1 capacity=2 | Allocate capacity 2 and copy the 1 element (1 copy so far). |
| 6 | L5 | size=2 capacity=2 x=8 | Write 8 into data[1] and increase size to 2. |
| 7 | L2 | size=2 capacity=2 x=15 | size 2 equals capacity 2: no room for 15, so the buffer must grow. |
| 8 | L3 | size=2 capacity=4 | Allocate capacity 4 and copy the 2 elements (3 copys so far). |
| 9 | L5 | size=3 capacity=4 x=15 | Write 15 into data[2] and increase size to 3. |
| 10 | L2 | size=3 capacity=4 x=16 | size 3 < capacity 4: there is a spare slot, so adding is O(1). |
| 11 | L5 | size=4 capacity=4 x=16 | Write 16 into data[3] and increase size to 4. |
| 12 | L2 | size=4 capacity=4 x=23 | size 4 equals capacity 4: no room for 23, so the buffer must grow. |
| 13 | L3 | size=4 capacity=8 | Allocate capacity 8 and copy the 4 elements (7 copys so far). |
| 14 | L5 | size=5 capacity=8 x=23 | Write 23 into data[4] and increase size to 5. |
| 15 | L2 | size=5 capacity=8 x=42 | size 5 < capacity 8: there is a spare slot, so adding is O(1). |
| 16 | L5 | size=6 capacity=8 x=42 | Write 42 into data[5] and increase size to 6. |
| 17 | L2 | size=6 capacity=8 x=7 | size 6 < capacity 8: there is a spare slot, so adding is O(1). |
| 18 | L5 | size=7 capacity=8 x=7 | Write 7 into data[6] and increase size to 7. |
| 19 | L2 | size=7 capacity=8 x=9 | size 7 < capacity 8: there is a spare slot, so adding is O(1). |
| 20 | L5 | size=8 capacity=8 x=9 | Write 9 into data[7] and increase size to 8. |
| 21 | L11 | size=8 capacity=8 copies=7 | Final: size 8, capacity 8 (0 spare slots), 3 resizes that copied 7 elements in total. Doubling keeps total copies below 2n. |
Memory overhead
A dynamic array wastes the spare capacity: right after a doubling the buffer is only half full, so up to 50% of the slots are unused (33% for ×1.5). During a resize both the old and the new buffer exist, so peak memory is about 1.5× to 3× the final size. On top of that, ArrayList<Integer> stores references plus a boxed object per element.
For large primitive data prefer int[] with your own size counter.
| Storage of 1,000,000 ints | Approximate memory |
|---|---|
| int[] (exact) | 4 MB |
| Dynamic int buffer, worst case just after doubling | up to 8 MB (+4 MB briefly while copying) |
| ArrayList<Integer> | about 4 MB references + 16 MB Integer objects |
Practice ladder
Now try it yourself
Easy
- How many resizes happen for 100 appends with doubling from capacity 1?
- What is the difference between size and capacity?
Medium
- Show that a shrink at size = capacity / 2 can cost O(n) per operation
- Why does ArrayList clear the slot of a removed element?
Advanced
- Prove that growth by a constant factor g > 1 gives amortised O(1) appends and find the total copies for g = 1.5
Key takeaways
- A dynamic array is an array plus size; resize by a constant factor.
- Geometric growth gives amortised O(1) appends; fixed-step growth does not.
- Spare capacity and boxed elements are the memory cost; presize when you can.