The previous lesson left two questions pending: why does accessing items[i] cost O(1), and what hidden reorganization makes append only O(1) amortized? To answer them we need to go down a level: understand how a computer's memory is physically organized and what an array is — the most elementary structure of all and the foundation on which almost every other one is built. By the end you will know exactly what Python's list does under the hood, which TaskFlow operations an array handles well... and which it doesn't, which will open the door to module 2.
Contents
- Memory as a row of numbered slots
- Static arrays: contiguity and O(1) access
- The price of contiguity: inserting and removing in the middle
- Dynamic arrays and amortized resizing
- Python's list under the hood (and a mention of array and NumPy)
- TaskFlow: when an array is enough and when it falls short
Memory as a row of numbered slots
You can picture a computer's RAM as a gigantic row of equally sized slots, numbered consecutively. Each slot's number is its memory address, and the processor can read or write any slot just by knowing its address, no matter how "far away" it is: going to slot 4 and to slot 40,000,000 costs the same. That is why this memory is called RAM: Random Access Memory — direct access.
Address: ... 1000 1001 1002 1003 1004 1005 1006 ...
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┐
Contents: │ ... │ ... │ ... │ ... │ ... │ ... │ ... │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘Two key ideas from this model:
- Every piece of data lives at some address. When you write
x = 42in Python, somewhere in that row the 42 gets stored, andxis the human way of referring to its address. Python hides the addresses from you (in C you handle them manually, with pointers), but you can peek withid(x), which returns an identifier based on the object's address. - Accessing by address is O(1). It is a hardware operation: the processor computes the address and goes there. No searching, no scanning.
On top of this model there is a fundamental decision that splits two worlds: storing the data contiguously (in consecutive slots) or scattered (each piece wherever there is room, connected by references). Arrays bet on contiguity; the linked lists of module 2 will bet on scattering. This entire lesson revolves around the consequences of that bet.
Static arrays: contiguity and O(1) access
A static array is a block of contiguous slots reserved all at once, with a fixed size, where every slot occupies the same amount of space. It is the native structure of languages like C or Java.
Contiguity carries an enormous prize. If the array starts at address 1000 and each element occupies 8 bytes, where is the element at index i? There's no need to search for it: it is computed.
address(element i) = base_address + i × element_size
Element 0 → 1000 + 0×8 = 1000
Element 1 → 1000 + 1×8 = 1008
Element 5 → 1000 + 5×8 = 1040One multiplication and one addition, whether i is 3 or 3 million: that is why access by index is O(1). This formula is the answer to the first pending question from lesson 01-04, and it is one of the most important ideas in the course: the array doesn't search for element i, it computes where it must be.
Let's simulate a static array in Python to feel its limits (Python doesn't have them natively, so we impose the restrictions ourselves):
class StaticArray:
"""Simulation of a fixed-size static array."""
def __init__(self, capacity):
self._capacity = capacity
self._data = [None] * capacity # reserve ALL the slots up front
self._used = 0 # how many are actually occupied
def get(self, i):
if not 0 <= i < self._used:
raise IndexError("index out of range")
return self._data[i] # direct access: O(1)
def add(self, element):
if self._used == self._capacity:
raise OverflowError("array full: no slots left")
self._data[self._used] = element
self._used += 1 # write into the first free slot: O(1)Points in the code worth attention:
[None] * capacityreserves all the slots at creation time: that is what "static" means. The memory is committed even if it goes unused.addis O(1)... until the array fills up. Then there is nothing to be done:OverflowError. A static array does not grow.- We distinguish
_capacity(reserved slots) from_used(slots holding real data). This distinction seems minor now, but it is the key to the dynamic arrays section.
board = StaticArray(3)
board.add("Design logo")
board.add("Write report")
board.add("Send invoice")
print(board.get(1)) # Write report (computed, not searched)
board.add("Call client") # OverflowError: array fullAn extra advantage of contiguity worth knowing: modern processors read memory in blocks (cache), so traversing contiguous data is even faster in practice than traversing scattered data: when reading element 0, the next ones come along "for free" in the same block.
The price of contiguity: inserting and removing in the middle
Everything contiguity gives away on access, it charges back on modifications. Think of a fully occupied row of seats at a movie theater: if someone wants to sit in the middle, everyone on one side has to shift over one seat.
Inserting at position i in an array requires shifting every element from i to the end one slot to the right:
Insert "X" at index 1:
Before: [ A ][ B ][ C ][ D ][ · ]
└────┴────┴─── all of these shift →
After: [ A ][ X ][ B ][ C ][ D ]Removing position i is the inverse move: the later elements shift left so no gap remains (contiguity admits no holes).
The cost, in the vocabulary of the previous lesson:
| Operation on an array | Cost | Reason |
|---|---|---|
Access index i |
O(1) | Address computed with the formula |
Modify index i |
O(1) | Same |
| Add at the end (with a free slot) | O(1) | Written into the first free slot |
Insert at index i |
O(n) | Shift up to n elements to the right |
Remove index i |
O(n) | Shift up to n elements to the left |
| Insert/remove at the front | O(n) | The previous row's worst case: everything shifts |
| Search by value (unsorted) | O(n) | There is no formula for values: you have to look |
Notice the asymmetry that defines the array: splendid at reading and modifying by position, expensive at reorganizing. Inserting at the front is the most painful case: every element moves.
Dynamic arrays and amortized resizing
The static array has an obvious practical problem: you have to guess the size in advance. How many tasks will a TaskFlow user have? 100? 100,000? Falling short is fatal (OverflowError) and overshooting wastes memory.
The dynamic array resolves the dilemma with an elegant strategy:
- Start with a static array of some capacity.
- Keep count of used slots versus total capacity (our
_usedand_capacity). - When an
appendfinds the array full: reserve a new, larger array (typically double or thereabouts), copy all the elements into the new one, and release the old one.
graph TB
A["Full array (cap. 4): [A][B][C][D]"] -->|append E| B["1. Reserve an array of capacity 8"]
B --> C["2. Copy A,B,C,D into the new one — O(n)"]
C --> D["3. Write E: [A][B][C][D][E][·][·][·]"]
D --> E["The next 3 appends will be O(1)"]
The copy step costs O(n), and here appears the answer to the second pending question from lesson 01-04. Why is append O(1) amortized and not O(n)? Because of the policy of doubling the capacity: after copying n elements, n free slots remain — meaning the next n appends are guaranteed O(1). The O(n) cost of the copy, spread across those n cheap appends, comes out to O(1) extra per operation.
Let's count it with numbers so it doesn't stay abstract: starting from capacity 1, performing 8 appends triggers copies of 1, 2, and 4 elements (when filling capacities 1, 2, and 4): 7 copies in total for 8 insertions — less than one copy per append. With 1,024 appends, the copies add up to 1,023: still ~1 per operation. The average does not grow with n: that is O(1) amortized, exactly the definition we gave in the previous lesson.
The honest counterpart: one particular append (the one that triggers the copy) can indeed be slow, and the dynamic array keeps spare slots reserved (extra memory of up to double what is used). Flexibility in exchange for occasional spikes and some memory: another one of those trade-offs you already know how to recognize.
Python's list under the hood (and a mention of array and NumPy)
The big reveal of this lesson: Python's list is exactly that, a dynamic array. It is not a "linked list" despite its name (that will be precisely the topic of module 2, and the naming mix-up is a classic trap). Now the entire cost table we gave in lesson 01-04 has a physical explanation:
| Operation in Python | Cost | Explanation from the array |
|---|---|---|
items[i] |
O(1) | Formula: base + i × size |
items.append(x) |
O(1) amortized | Writes into the first free slot; occasionally resizes |
items.insert(0, x) |
O(n) | Shifts every element to the right |
items.pop() (end) |
O(1) | Empties the last used slot, shifting nothing |
items.pop(0) (front) |
O(n) | Shifts every element to the left |
x in items |
O(n) | Search by value: no formula, just scanning |
A technical nuance worth knowing: since a list can mix types and Python objects have different sizes, the array's slots don't hold the objects themselves but references (addresses) to them, all of the same size; that is why the access formula keeps working. And CPython's growth policy isn't exactly doubling, but growing ~1.125× plus some headroom; the amortized analysis holds for any proportional growth.
We can even observe the resizing from the outside with sys.getsizeof, which gives the bytes the object occupies:
import sys
items = []
previous = sys.getsizeof(items)
for i in range(40):
items.append(i)
current = sys.getsizeof(items)
if current != previous: # it just resized!
print(f"With {i + 1:>2} elements: {previous} -> {current} bytes")
previous = currentTypical output (varies with the Python version):
With 1 elements: 56 -> 88 bytes
With 5 elements: 88 -> 120 bytes
With 9 elements: 120 -> 184 bytes
With 17 elements: 184 -> 248 bytes
With 25 elements: 248 -> 312 bytes
With 33 elements: 312 -> 376 bytesThe size doesn't grow with every append, but in jumps: each jump is a reservation of extra capacity, and between jumps the appends request no memory. It is amortized resizing seen live.
Two relatives of list worth knowing, as a mention only:
array.array(a standard library module): a homogeneous dynamic array that stores the numbers themselves (not references), saving a fair amount of memory when you have millions of values of the same type.- NumPy (
numpy.ndarray): the standard library of numerical computing; homogeneous, compact arrays with enormously fast vectorized operations. If TaskFlow ever computed statistics over millions of time records, NumPy would be the tool. It falls outside this course, but you should know it exists and that under the hood it is... a contiguous array, like everything in this lesson.
TaskFlow: when an array is enough and when it falls short
Let's close by applying the criterion to our project. TaskFlow's board, array (list) version:
board = [] # empty dynamic array
# Use case 1: add new tasks at the end — O(1) amortized
board.append({"id": 1, "title": "Design logo"})
board.append({"id": 2, "title": "Write report"})
# Use case 2: display the board in order — O(n), unavoidable and optimal
for position, task in enumerate(board, start=1):
print(f"{position}. {task['title']}")
# Use case 3: access the task at a given position — O(1)
print(board[0]["title"])For these three uses — adding at the end, listing in order, accessing by position — the list is the right choice, and no structure in the course will beat it at them. Its status as Python's default first option is well earned.
But look what happens with two other real TaskFlow use cases:
# Use case 4: the user drags a new task to the TOP of the board
board.insert(0, {"id": 3, "title": "Urgent!"}) # O(n): shifts everything
# Use case 5: complete task 2, wherever it is
for i, task in enumerate(board): # O(n): find it...
if task["id"] == 2:
del board[i] # ...and O(n): shift the rest
breakWith 50 tasks, none of this matters. But imagine the global activity feed of a corporate TaskFlow with hundreds of thousands of entries where the norm is inserting at the front and deleting from the middle: every operation would shift hundreds of thousands of references. The diagnosis, in the vocabulary you now command:
| Usage pattern in TaskFlow | Array (list)? |
Reason |
|---|---|---|
| Add at the end, list, read by position | Yes, ideal | O(1) / optimal O(n) / O(1) |
| Constantly insert and remove at the front or middle | Falls short | Every operation is O(n) due to the shifts |
| Search by id continuously | Falls short | O(n); we already saw an index makes it O(1) (module 5) |
What if there were a structure where inserting or removing in the middle shifted nothing, because the elements don't live contiguously but linked to one another, each wherever it landed in memory? There is: it's the linked list, the other great bet — scattering instead of contiguity — and the topic of module 2. As is only fair, it will have its own costs: losing contiguity means losing the magic O(1) access formula. There is no free lunch; there are informed choices.
Common Mistakes and Tips
- Believing Python's
listis a linked list. The name misleads: it is a dynamic array. This misunderstanding leads to assuming that inserting at the front is cheap, when it is O(n). In module 2 the comparison will become crystal clear. - Using
items.insert(0, x)oritems.pop(0)inside loops. It is Python's most common accidental O(n²): n operations of O(n). If you routinely need to add and remove at the front, there is a structure designed for it (collections.deque, which we will study in module 4). - Panicking over the resize spike. An occasional
appendcosting O(n) is almost never a real problem: the amortized O(1) is what counts in practice. Only in systems with strict latency constraints do the spikes matter. - Ignoring the reserve memory. A dynamic array can hold up to twice as many slots as it uses. With millions of numeric elements,
array.arrayor NumPy drastically cut consumption compared tolist. - Tip: faced with any new structure, always ask "is it contiguous or scattered inside?". The answer will tell you, without reading documentation, which operations will be cheap (access if contiguous; reorganization if scattered) and which expensive.
Exercises
Exercise 1: the access formula
An array of 8-byte integers starts at address 5000. (a) At what address is the element at index 12? (b) If an element sits at address 5096, what is its index? (c) Explain in one sentence why this formula stops working if the elements occupied different sizes.
Exercise 2: counting shifts
Starting from the array [10, 20, 30, 40, 50] (capacity 8, 5 slots used), state how many elements shift in each operation and the array's final state after applying them in order: (a) insert(0, 5); (b) append(60); (c) remove the element at index 2; (d) insert(3, 35).
Exercise 3: observing the O(n) cost of inserting at the front
Write a program that uses timeit to compare building a list of 50,000 elements in two ways: (a) with append (adding at the end) and (b) with insert(0, x) (adding at the front). Before running it, predict which will be slower and why, and check whether the difference grows when moving to 100,000 elements.
Solutions
Solution 1:
- (a)
5000 + 12 × 8 = 5096. - (b)
(5096 − 5000) / 8 = 12: the same element as in the previous part, computed in reverse. - (c) The formula multiplies the index by a fixed size; with variable sizes you cannot compute where element
istarts without scanning and adding up the sizes of all the previous ones (which is why Python'sliststores fixed-size references to the objects, not the objects themselves).
Solution 2:
- (a)
insert(0, 5): all 5 elements shift →[5, 10, 20, 30, 40, 50]. - (b)
append(60): 0 shift (there is a free slot, capacity 8) →[5, 10, 20, 30, 40, 50, 60]. - (c) remove index 2 (the value 20): the 4 later elements shift left →
[5, 10, 30, 40, 50, 60]. - (d)
insert(3, 35): the 3 elements from index 3 onward shift →[5, 10, 30, 35, 40, 50, 60].
Moral of the exercise: the cost of each operation depends on how many elements remain to the right of the modification point; that is why the worst case is always the front.
Solution 3:
Prediction: version (b) will be much slower, because each insert(0, x) shifts every element already present: the total cost is 0 + 1 + 2 + ... + (n−1) ≈ n²/2 shifts, i.e., O(n²); version (a) is n appends of O(1) amortized, i.e., O(n) total.
import timeit
def with_append(n):
items = []
for i in range(n):
items.append(i)
return items
def with_insert_front(n):
items = []
for i in range(n):
items.insert(0, i)
return items
for n in (50_000, 100_000):
t_a = timeit.timeit(lambda: with_append(n), number=3)
t_b = timeit.timeit(lambda: with_insert_front(n), number=3)
print(f"n={n}: append {t_a:.3f} s | insert(0) {t_b:.3f} s")Typical result: with 50,000 elements, append takes milliseconds and insert(0, ...) on the order of seconds; doubling to 100,000, append doubles (linear) but insert(0, ...) quadruples (quadratic), confirming the prediction. It is the physics of the array — the shifts — showing up exactly as the theory announces.
Conclusion
You have reached the bottom of the matter: memory is a row of numbered, directly addressable slots, and the array — contiguous data, equal slots — exploits that organization to offer O(1) access by index through a simple formula, paying for it with O(n) shifts when inserting or removing in the middle. Dynamic arrays add automatic growth with proportional resizing, whose spread-out cost yields the famous O(1) amortized append; and Python's list is exactly that, with array.array and NumPy as compact variants for massive numeric data. For TaskFlow, the array is perfect as a board that grows at the end and is read in order, and falls short when insertions and deletions at the front or in the middle abound.
This closes module 1: you now know what a data structure is, why choosing it well matters, which families exist, how to measure their efficiency with Big O, and on what physical foundation everything is built. In module 2 we start building for real: the linked list, the first structure we will craft from scratch, which gives up contiguity precisely to make cheap the operations where the array falters — and which we will turn into TaskFlow's definitive task board.
Data Structures Course
Module 1: Introduction to Data Structures
- What Are Data Structures?
- The Importance of Data Structures in Programming
- Types of Data Structures
- Algorithmic Complexity and Big O Notation
- Arrays and Memory: the Foundation of Data Structures
Module 2: Lists
Module 3: Stacks
- Introduction to Stacks
- Basic Stack Operations
- Stack Implementation
- Stack Applications
- Stack Exercises
Module 4: Queues
- Introduction to Queues
- Basic Queue Operations
- Circular Queues
- Priority Queues
- Double-Ended Queues (Deques)
- Queue Exercises
Module 5: Hash Tables and Dictionaries
- Introduction to Hash Tables
- Hash Functions and Collision Resolution
- Dictionaries and Sets in Practice
- Hash Table Exercises
Module 6: Trees
- Introduction to Trees
- Binary Trees
- Tree Traversals
- Binary Search Trees
- AVL Trees
- B-Trees
- Heaps
- Tree Exercises
Module 7: Graphs
- Introduction to Graphs
- Graph Representation
- Graph Search Algorithms
- Shortest Path Algorithms
- Minimum Spanning Trees
- Graph Applications
- Graph Exercises
