Linear Structures
Arrays and memory
Contiguous cells and why index access is instant
The humblest data structure there is - a row of boxes in memory - and the one that quietly underpins strings, lists, matrices, and every tensor you will ever meet.
Memory is one long strip of numbered boxes. An array is just a promise: "give me boxes in a row, all the same size." That single promise - contiguous and uniform - is what makes reading any element instant, and it is also what makes inserting in the middle slow. Everything else in this course is built on top of it.
A street of equal-width houses
A street of houses in memory
Say an array of 32-bit integers starts at byte in memory, and each integer occupies bytes. Element does not need a search - its location is pure arithmetic. One multiply, one add, one fetch, and you have it, whether is 0 or 9,999,999:
The address formula (this is why indexing is O(1))
Here is where the block starts and is the size of one element in bytes. The formula never mentions , the array's length - that is exactly why random access is O(1). Reaching the millionth element costs the same as reaching the first.
# Same cost for A[0] or A[999999] - it's just arithmetic
def get(A, i):
addr = A.base + i * A.item_size # one multiply, one add
return memory[addr] # one fetch -> O(1)Contiguity buys a second, quieter prize: . When the CPU fetches one element it drags the neighbouring bytes into a fast cache line alongside it, betting you will want them next. Walk an array front-to-back and you mostly hit that cache - often far faster than the Big-O count alone suggests. Scatter the same data across memory (as a linked list does) and you forfeit that bet on every step.
Fixed vs. dynamic
list, Java's ArrayList, C++'s vector) hides that limit: it keeps spare capacity, and when it runs out it quietly allocates a bigger block and copies everything across. The demo below lets you watch both the pain of the middle-insert and the magic of that regrow.See the cost: shift, then grow
Two things arrays make you pay for. Inserting in the middle means every later element must slide one slot right to open a gap - so cost grows with how far from the end you insert. Appending to a full dynamic array triggers a resize: allocate double, copy everything, then continue. Switch modes and drive each one; the meters keep the score.
Pick an index, then Insert - count how many later elements have to move.
Rare pain, cheap on average
Two dimensions, one strip of memory
Memory is one-dimensional, so a 2-D array is a polite fiction. A grid with columns is stored one row after another - row-major order - and the index arithmetic just flattens the two coordinates into one offset:
Element lives slots into the flat strip. This is why looping a matrix row-by-row is cache-friendly (you walk memory in order) while column-by-column jumps around and thrashes the cache. Same data, same Big-O, very different wall-clock time - the layout is doing the talking.
Check yourself
Inserting a value at the front of an n-element array must shift every later element over, so it costs:
Doubling capacity on every resize makes the cost of append, averaged over many appends:
Why is reading A[i] O(1) but inserting in the middle O(n)?
Contrast a single address computation against a cascade of shifts. Try to state it, then check.
Lock it in
- An array is one promise: boxes in a row, all the same size - contiguous and uniform.
- Random access is O(1) because never mentions the length.
- Inserting in the middle is O(n): every later element slides one slot right to open a gap.
- A dynamic array keeps spare capacity and doubles on overflow, making append amortized O(1).
- A 2-D array is flattened row-major, so row-by-row loops stay cache-friendly.
Primary source
Ask your teacher