Arrays & dynamic arrays
In one line
An array stores items side by side in memory so any index is reachable in O(1); a dynamic array adds automatic resizing so appends are O(1) on average.
What it is
An array is a block of memory split into equal-sized slots, one per element, stored one after another. Because every slot has the same size, the address of element i is simply start + i times slot size, so reading or writing any index takes the same constant time.
A fixed array cannot grow. A dynamic array (Python list, Java ArrayList, C++ vector, Go slice, JavaScript array) keeps a fixed array underneath with some spare capacity. When it fills up, it allocates a bigger block, copies everything over and frees the old one. Because the capacity grows by a multiple each time, the copying cost averages out to O(1) per append.
Why interviewers ask
Almost every coding question takes an array as input, so you are judged on knowing exactly what each operation costs. Interviewers check that you know index access is O(1) but inserting or deleting in the middle is O(n), that append is amortized O(1), and that you can explain why. These facts decide whether your solution is O(n) or quietly O(n^2).
How it works
- 1Read or write by index: compute start + i times slot size and jump there. This is O(1) and does not depend on the array's length.
- 2Append: if there is spare capacity, write into the next free slot and increase the length. O(1).
- 3Append when full: allocate a new block, usually 1.5x to 2x bigger, copy all n elements, then append. That single append costs O(n), but it happens rarely.
- 4Amortized cost: with doubling, the copies over n appends add up to at most about 2n (1 + 2 + 4 + ... + n), so the average cost per append is O(1).
- 5Insert or delete at position i: every element after i must shift one slot right or left, so the cost is O(n - i), which is O(n) in the worst case. Removing the last element is O(1).
- 6Search for a value in an unsorted array means checking each slot: O(n). If the array is sorted, binary search finds it in O(log n).
Worked example · Appending 5 items to a dynamic array that doubles
- Start: capacity 1, length 0.
- Append a: there is room. Array [a], capacity 1, length 1. Copies so far: 0.
- Append b: full, so grow to capacity 2 and copy a (1 copy). Array [a, b]. Copies: 1.
- Append c: full, grow to capacity 4 and copy a, b (2 copies). Array [a, b, c, _]. Copies: 3.
- Append d: room in slot 3. Array [a, b, c, d]. Copies: 3.
- Append e: full, grow to capacity 8 and copy 4 items. Array [a, b, c, d, e, _, _, _]. Copies: 7.
- 5 appends caused 7 copies in total, under 2 per append. However many items you add, the total stays under 2n, which is why append is amortized O(1).
- Now insert z at index 1: b, c, d and e each shift right by one (4 moves) before z is written. Inserting near the front is the expensive operation.
Picture it
An array is a row of numbered lockers in a corridor: you can walk straight to locker 37 without opening the others. A dynamic array is a tenant who, when the corridor is full, rents a corridor twice as long and moves everything in one go; moving is tiring, but it happens so rarely that each new locker is cheap on average.
Reference code
class DynamicArray:
def __init__(self):
self.cap = 1 # slots allocated
self.n = 0 # slots in use
self.data = [None] * self.cap
def get(self, i):
if not 0 <= i < self.n:
raise IndexError(i)
return self.data[i] # O(1): base + i * slot size
def append(self, x):
if self.n == self.cap: # full: double the capacity
self._resize(2 * self.cap)
self.data[self.n] = x
self.n += 1
def pop(self):
if self.n == 0:
raise IndexError("pop from empty array")
self.n -= 1
x, self.data[self.n] = self.data[self.n], None
return x
def _resize(self, new_cap):
new = [None] * new_cap
for i in range(self.n): # O(n) copy, but rare
new[i] = self.data[i]
self.data, self.cap = new, new_cap
A minimal dynamic array. get() is O(1) because it indexes directly. append() is O(1) except when the array is full; then _resize() copies everything into a block twice the size. Because the capacity doubles, total copying over n appends is under 2n, so append is amortized O(1). pop() from the end is O(1).
Key patterns
Index as a direct address
When: Keys are small integers in a known range (ages, scores 0-100, letters a-z)
How: Use an array of that size instead of a hash map: counts[x] += 1. It is faster and uses predictable memory.
Build a new array instead of inserting in the middle
When: You would otherwise call insert or delete inside a loop
How: Append the kept or transformed elements to a new list in one pass. That is O(n) total instead of O(n^2).
Work from the end
When: You need to add or remove often
How: Treat the end of the array as the active side (append and pop are O(1)). A stack is just a dynamic array used this way.
Pre-size when the length is known
When: Output size is known in advance
How: Create [0] * n (or reserve(n)) and fill by index to avoid repeated growth.
Real-world use
Python lists and Java ArrayList
Both are dynamic arrays. CPython over-allocates by roughly 12.5% plus a small constant when a list grows, and Java's ArrayList grows by 1.5x; both give amortized O(1) append and O(1) indexing, which is why they are the default collection in each language.
C++ std::vector and reserve()
A vector grows geometrically, and every growth invalidates pointers into the old block. Performance-sensitive code such as game engines calls reserve() up front when the final size is known to avoid repeated reallocation and copying.
NumPy arrays and columnar databases
NumPy stores numbers in one contiguous block, so loops run at memory speed and use the CPU cache well. Columnar stores like Apache Arrow, Parquet and ClickHouse keep each column as a contiguous array for the same reason: scanning millions of values in order is fast.
Go slices
A Go slice is a view (pointer, length, capacity) onto an array. append() reallocates when capacity runs out, roughly doubling for small slices and growing more slowly for large ones, so two slices can silently stop sharing memory after an append.
In a project
A sensor-logging service collects about 1 million temperature readings per hour per device and must return the reading at a given second instantly.
- Store readings for each device in a dynamic array indexed by seconds since the start of the hour, so lookup by time is a direct index: O(1).
- Pre-allocate capacity for 3,600 readings per hour block (reserve the size you know) so the array never resizes during ingestion.
- Only append at the end (readings arrive in time order), which is O(1); never insert in the middle.
- Keep values in a typed contiguous array (for example array('f') in Python or NumPy) instead of a list of objects to cut memory and speed up scans.
Impact: Lookups stay constant time regardless of history size, ingestion has no resize pauses, and storing compact floats instead of boxed objects cuts memory several times over, letting one server hold far more devices.
Trade-offs
Use it when
- You need fast access by position (O(1) index)
- You mostly add or remove at the end
- You scan data in order and want good cache performance
Avoid it when
- You insert or delete at the front or middle often: use a deque or linked list
- You need fast lookup by value: use a hash set or map
- Very large arrays that cannot afford an occasional full copy pause; pre-size or use chunked storage
Cost: Access O(1). Append amortized O(1), worst case O(n). Insert or delete at index i O(n - i). Pop from end O(1). Search O(n) unsorted, O(log n) sorted. Space O(n), with up to a constant factor of spare capacity.
Common mistakes
- Calling list.insert(0, x) or list.pop(0) inside a loop and believing the loop is O(n): each call is O(n), so the loop is O(n^2). Use collections.deque for the front.
- Saying append is always O(1): it is amortized O(1); a single append that triggers a resize is O(n).
- Using 'x in list' inside a loop; membership in a list is O(n), so convert to a set first.
- Forgetting that slicing (nums[1:]) copies the elements, costing O(k) time and memory, which adds up in recursion.
- Mixing up length and capacity, or holding a pointer or reference into a vector or slice that becomes stale after a resize.
Interview questions
Quick revision
- Index access O(1): address = start + i * slot size.
- Append amortized O(1); a resize copies all n elements.
- Grow by a factor (1.5x or 2x), not a constant, to keep append O(1) on average.
- Insert or delete in the middle O(n): elements must shift.
- Pop from the end O(1); pop from the front O(n) in a list, O(1) in a deque.
- Search O(n) unsorted, O(log n) with binary search on sorted data.
- Slices copy: nums[a:b] costs O(b - a).
- Contiguous memory = cache friendly, fast sequential scans.
Check yourself
1. What is the time complexity of reading nums[i] in an array of length n?
2. A dynamic array doubles its capacity when full. What is the amortized cost of append?
3. A loop runs n times and calls list.insert(0, x) each time. What is the total cost?
4. Why does a dynamic array grow by a multiple rather than by a fixed number of slots?
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.