Arrays, dynamic arrays and tuples

Topics in this section
5 cards

Arrays: everything in one row

An array stores its elements side by side in one block of memory. Every element has the same size, so the computer finds element i with one calculation: start address plus i times the element size. That is why reading any element by index takes the same short time, O(1), whether the array holds ten items or ten million.

The price is rigidity. A classic array has a fixed size chosen when it is created, and it is only fast when you know the index. Looking for a value without knowing where it is means checking elements one by one: O(n).

Seven numbers in consecutive memory cells, with addresses and indexes
In real life

Hourly temperatures for a day, the pixels of one row of an image, the 64 squares of a chess board: fixed-size data you read by position.

In code: Python · Rust
temps = [21, 19, 23, 25]   # a list: Python has no fixed-size array in the core language
temps[2]                    # 23, O(1)

from array import array
prices = array('d', [9.5, 10.0, 10.25])   # compact array of floats
  • Python lists store references to objects; array.array and NumPy store the raw values side by side.
  • In Rust the length of a fixed array is part of its type: [i32; 4] and [i32; 5] are different types.
Check yourself

Why does reading element 900,000 of an array take as long as reading element 3?

Dynamic arrays: an array that grows

Most of the time you don't know the size in advance, so languages offer a dynamic array: Python's list, Rust's Vec, Java's ArrayList. Inside it is an ordinary array with spare room. Its length is how many elements you stored; its capacity is how many fit before it must grow.

When it's full and you append, it allocates a bigger block, usually about twice the size, copies everything over, and frees the old block. Copying is slow, but it happens rarely, and each time it doubles the room. Spread over all appends, the cost per append is O(1) on average. This is called amortised O(1).

A full array of four elements is copied into a new array of eight before the fifth is added
In real life

A chat app appends every new message to the conversation. Thousands of appends, and only around a dozen of them trigger a copy.

In code: Python · Rust
msgs = []
msgs.append("hi")       # amortised O(1)
msgs.extend(["how", "are", "you"])
len(msgs)               # 4
  • If you know roughly how many items are coming, reserve the capacity up front (Vec::with_capacity in Rust) and skip the copies.
  • Python grows lists by a smaller factor than 2, but the principle is the same.
Check yourself

A dynamic array occasionally copies all its elements when it grows. Why is appending still described as O(1)?

Inserting in the middle

Adding at the end of a dynamic array is cheap. Adding at the front or in the middle is not: every element after that position has to move one step to make room, and removing works the same way in reverse. That is O(n), and with a million elements it's a million moves.

If a program keeps inserting or removing at the front, an array is the wrong tool. A deque (lesson 4) or a linked list (lesson 3) handles that end cheaply.

Inserting X at position 1 shifts b, c and d one place to the right
In real life

A task list where urgent tasks are inserted at the top: with 50 tasks nobody notices; with 5 million queued jobs every insert stalls the system.

In code: Python · Rust
tasks = ["b", "c", "d"]
tasks.insert(0, "a")   # O(n): shifts b, c, d
tasks.pop(0)            # O(n) again

from collections import deque
q = deque(tasks)
q.appendleft("a")       # O(1)
  • list.pop(0) in Python looks harmless but is O(n). Use collections.deque for queues.
  • Rust's Vec::swap_remove(i) removes in O(1) by moving the last element into the gap, when order doesn't matter.
Check yourself

A service keeps 2 million pending jobs in a Python list and takes the oldest with jobs.pop(0). It's getting slow. What is the cause?

Tuples: a small fixed record

A tuple groups a fixed number of values, often of different types, where each position has a meaning: (latitude, longitude), (status code, message). An array is a row of the same kind of thing; a tuple is one record.

In Python tuples are immutable: you can't change them after creation. That makes them hashable, so they can be dictionary keys and set members, which lists can't. In Rust a tuple's type lists every position, like (f64, f64, &str); whether you can change it depends on whether the variable is declared mut. When the positions need names, both languages offer something clearer: a named tuple or dataclass in Python, a struct in Rust.

A tuple of latitude, longitude and city name
In real life

Caching distances between cities with the pair (from, to) as the key; a function that returns both a result and an error code.

In code: Python · Rust
place = (51.50, -0.12, "London")
lat, lon, name = place          # unpacking
distance = {("London", "Paris"): 344}   # tuple as a dict key
# place[0] = 52  -> TypeError: tuples are immutable
  • Python: a list can't be a dict key because it can change; a tuple can, as long as everything inside it is hashable too.
  • Rust: tuples are fine for two or three values returned together; beyond that, a struct with named fields reads better.
Check yourself

In Python, why can ("London", "Paris") be a dictionary key while ["London", "Paris"] cannot?

What arrays cost

Putting it together for a dynamic array of n elements:

Read or write by index: O(1). Append at the end: amortised O(1). Insert or remove at the front or in the middle: O(n). Find a value without knowing its index: O(n), unless the array is sorted, in which case binary search finds it in O(log n).

Arrays also have an advantage the O-notation hides: elements sit next to each other, so the processor reads them into its in chunks. Walking through an array is often several times faster than walking through the same number of elements scattered around memory, as in a linked list.

In real life

Rule of thumb in interviews: if you mostly append and read by position, an array is the default. If you keep searching for values, you probably want a hash map (next lesson).

Check yourself

You need to check whether a value is present in an unsorted array of n items. What does it cost?