Arrays and Dynamic Arrays
Welcome to Phase 3. Every data structure you’ll meet from here on is either built on top of an array, or exists specifically to fix one of its limitations. Get comfortable with arrays first and half of interview prep gets easier immediately.
What you’ll learn
- Fixed-size array vs dynamic array, and why Python’s
listlistis the latter. - Why
list.appendlist.appendis amortized thanks to geometric over-allocation. - The real complexity of index, append, insert, and pop at different positions.
- The two-pointer technique for in-place array work.
- Prefix sums for O(1) range-sum queries after one O(n) pre-pass.
- LeetCode-style problems to drill the pattern.
Fixed array vs dynamic array
A fixed-size array (like a C array) reserves one contiguous block of
memory for exactly nn elements, decided up front. Growing it means
allocating a whole new block and copying everything over — there’s no
“just add one more”.
A dynamic array hides that pain behind a friendly appendappend. Python’s
built-in listlist is a dynamic array: contiguous storage under the hood, but
it over-allocates — when it runs out of room it grabs a bigger block
(roughly 1.125x the old capacity once the list is large) and copies the
existing pointers across, once. Because that expensive copy happens less and
less often as the list grows, the average cost per appendappend stays
— this is called amortized analysis.
import sys
arr = []
prev_capacity_hint = sys.getsizeof(arr)
growths = 0
for i in range(25):
arr.append(i)
size = sys.getsizeof(arr)
if size != prev_capacity_hint:
growths += 1
print(f"n={i + 1:<3} underlying buffer resized (event #{growths})")
prev_capacity_hint = size
print(f"\n25 appends, only {growths} real reallocations — the rest were free.")import sys
arr = []
prev_capacity_hint = sys.getsizeof(arr)
growths = 0
for i in range(25):
arr.append(i)
size = sys.getsizeof(arr)
if size != prev_capacity_hint:
growths += 1
print(f"n={i + 1:<3} underlying buffer resized (event #{growths})")
prev_capacity_hint = size
print(f"\n25 appends, only {growths} real reallocations — the rest were free.")Notice the buffer only resizes a handful of times across 25 appends, not
once per element. Each resize is , but it happens on a shrinking
fraction of the appends, so the total cost for nn appends is ,
which averages out to per append.
Watch a dynamic array double
When the backing block fills up, CPython allocates a bigger one and copies every existing element across before the new item is written in:
Complexity of common list operations
| Operation | Example | Complexity |
|---|---|---|
| Index / update | arr[i]arr[i], arr[i] = xarr[i] = x | |
| Append (end) | arr.append(x)arr.append(x) | amortized |
| Pop (end) | arr.pop()arr.pop() | amortized |
| Insert at front/middle | arr.insert(0, x)arr.insert(0, x) | |
| Pop from front/middle | arr.pop(0)arr.pop(0) | |
| Search / membership | x in arrx in arr | |
| Length | len(arr)len(arr) |
Two pointers
The two-pointer technique uses two indices moving through the array — usually from both ends inward, or both from the same side at different speeds — to solve problems in one pass instead of a nested loop.
def reverse_in_place(arr):
left, right = 0, len(arr) - 1
while left < right:
arr[left], arr[right] = arr[right], arr[left]
left += 1
right -= 1
return arr
def is_palindrome_array(arr):
left, right = 0, len(arr) - 1
while left < right:
if arr[left] != arr[right]:
return False
left += 1
right -= 1
return True
print(reverse_in_place([1, 2, 3, 4, 5]))
print(is_palindrome_array([1, 2, 3, 2, 1]))
print(is_palindrome_array([1, 2, 3]))def reverse_in_place(arr):
left, right = 0, len(arr) - 1
while left < right:
arr[left], arr[right] = arr[right], arr[left]
left += 1
right -= 1
return arr
def is_palindrome_array(arr):
left, right = 0, len(arr) - 1
while left < right:
if arr[left] != arr[right]:
return False
left += 1
right -= 1
return True
print(reverse_in_place([1, 2, 3, 4, 5]))
print(is_palindrome_array([1, 2, 3, 2, 1]))
print(is_palindrome_array([1, 2, 3]))Both functions do a single pass with extra space — no temporary copy of the array needed.
Prefix sums
If you need the sum of many sub-ranges of the same array, don’t re-sum each range from scratch (that’s per query). Precompute a prefix sum array once, then answer every range-sum query in .
def build_prefix(arr):
prefix = [0] * (len(arr) + 1)
for i, x in enumerate(arr):
prefix[i + 1] = prefix[i] + x
return prefix
def range_sum(prefix, left, right):
# inclusive sum of arr[left..right], O(1) after the O(n) pre-pass
return prefix[right + 1] - prefix[left]
nums = [2, 4, 6, 8, 10, 12]
prefix = build_prefix(nums)
print("sum(1..3):", range_sum(prefix, 1, 3)) # 4 + 6 + 8
print("sum(0..5):", range_sum(prefix, 0, 5)) # whole arraydef build_prefix(arr):
prefix = [0] * (len(arr) + 1)
for i, x in enumerate(arr):
prefix[i + 1] = prefix[i] + x
return prefix
def range_sum(prefix, left, right):
# inclusive sum of arr[left..right], O(1) after the O(n) pre-pass
return prefix[right + 1] - prefix[left]
nums = [2, 4, 6, 8, 10, 12]
prefix = build_prefix(nums)
print("sum(1..3):", range_sum(prefix, 1, 3)) # 4 + 6 + 8
print("sum(0..5):", range_sum(prefix, 0, 5)) # whole arrayOne pass builds the prefix array; every query after that is .
LeetCode problem set
| # | Problem | Difficulty | The twist |
|---|---|---|---|
| 1 | Two Sum | Easy | The canonical array-plus-hash-map warm-up: one pass, check the complement before inserting |
| 121 | Best Time to Buy and Sell Stock | Easy | A single scan tracking the running minimum — no need to store anything else |
| 283 | Move Zeroes | Easy | In-place partition with a write pointer; preserves order and never allocates |
Practice — real LeetCode problems
Each exercise is the actual LeetCode problem with its real method signature and LeetCode’s own examples as the test. Write the body, press Run, and match the expected output.
LC 121 — Best Time to Buy and Sell Stock · Easy
Problem. Given prices[i]prices[i] for day ii, choose one day to buy and a later
day to sell to maximise profit. Return the maximum profit, or 00 if none is
possible.
Constraints. 1 <= len(prices) <= 10^51 <= len(prices) <= 10^5, 0 <= prices[i] <= 10^40 <= prices[i] <= 10^4.
Examples. [7,1,5,3,6,4][7,1,5,3,6,4] gives 55 (buy at 1, sell at 6) ·
[7,6,4,3,1][7,6,4,3,1] gives 00 (never profitable)
Editorial
For each day, the best possible sale is that price minus the cheapest price seen before it. Tracking a running minimum makes that per day.
Time . Space .
[7,6,4,3,1][7,6,4,3,1] returning 00 matters: prices only fall, so no transaction is made
at all. Initialising best = 0best = 0 rather than to a negative sentinel is what encodes
“you may decline to trade”.
Note this is also Kadane’s algorithm
applied to the array of day-to-day differences — the maximum subarray sum of
[-1, 4, -2, 3, -2][-1, 4, -2, 3, -2] is 55. Seeing the connection is worth mentioning.
Follow-ups: “Unlimited transactions (LC 122)?” — sum every positive difference. “At most two transactions (LC 123)?” — DP over four states. “With a cooldown (LC 309) or a fee (LC 714)?” — state-machine DP.
LC 283 — Move Zeroes · Easy
Problem. Move all 00s to the end of the array in place, keeping the
relative order of the non-zero elements. You must not copy the array.
Constraints. 1 <= len(nums) <= 10^41 <= len(nums) <= 10^4,
-2^31 <= nums[i] <= 2^31 - 1-2^31 <= nums[i] <= 2^31 - 1.
Examples. [0,1,0,3,12][0,1,0,3,12] gives [1,3,12,0,0][1,3,12,0,0] · [0][0] gives [0][0]
Editorial
writewrite marks where the next non-zero belongs. Every non-zero found is swapped
into that slot, which simultaneously pushes whatever was there — necessarily a
zero, or the element itself — to the read position.
Time . Space .
Swapping matters. Assigning nums[write] = nums[read]nums[write] = nums[read] would also compact the
non-zeros, but you would then need a second loop to zero out the tail. The swap
does both jobs in one pass.
This is the same read/write pointer idea behind LC 26 and LC 27 — a same-direction two-pointer scan rather than the converging kind.
Follow-ups: “Minimise writes?” — assign only when read != writeread != write, skipping
no-op swaps. “Move zeroes to the front?” — scan from the right. “Remove all
instances of a value (LC 27)?” — identical structure.
LC 26 — Remove Duplicates from Sorted Array · Easy
Problem. Given a sorted array, remove duplicates in place so each unique
element appears once, keeping relative order. Return kk, the number of unique
elements; the first kk slots must hold them.
Constraints. 1 <= len(nums) <= 3 * 10^41 <= len(nums) <= 3 * 10^4, sorted non-decreasing.
Examples. [1,1,2][1,1,2] gives k = 2k = 2 with [1,2][1,2] ·
[0,0,1,1,1,2,2,3,3,4][0,0,1,1,1,2,2,3,3,4] gives k = 5k = 5 with [0,1,2,3,4][0,1,2,3,4]
Editorial
Sortedness guarantees duplicates are adjacent, so one pass suffices. The subtlety is what to compare against.
Time . Space .
Comparing with nums[write - 1]nums[write - 1] — the last element actually kept — is correct.
Comparing with nums[read - 1]nums[read - 1] happens to work here too, but the “last kept”
formulation is the one that generalises: LC 80 allows each value twice, and
there the test becomes nums[read] != nums[write - 2]nums[read] != nums[write - 2]. Learning the general shape
now saves rederiving it.
[0,0,1,1,1,2,2,3,3,4][0,0,1,1,1,2,2,3,3,4] includes a run of three, which is where a careless
comparison shows up.
Follow-ups: “Allow up to two of each (LC 80)?” — compare against
nums[write - 2]nums[write - 2]. “Unsorted input?” — you need a setset, so space, and
order preservation becomes a separate concern. “Return the array rather than the
count?” — slice to nums[:write]nums[:write].
Recap
- Python’s
listlistis a dynamic array: contiguous storage that over-allocates soappendappendis amortized . - End operations are cheap (); front/middle operations are expensive () because everything after them has to shift.
- Two pointers solve many array problems in one pass with extra space.
- Prefix sums trade one pre-pass for range-sum queries afterward.
Next: Strings — why Python strings are immutable, and the patterns that follow from that.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
