Week 10 · Phase 4 · Choosing well

What Python Lists Really Cost

append vs insert(0), pop() vs pop(0), + vs comprehension — measured, not guessed.

Which everyday list operations are cheap, and which quietly cost a fortune?

Lesson

Removing the front shifts the rest

pop(0) on [A, B, C, D]
before
ABCD
after
B ←C ←D ←
Remove every item from the front(n − 1) + (n − 2) + … + 1 = n(n − 1)/2 shifts

A Python list stores references in an array. Access by index is O(1). Removing its last item avoids shifting the others; repeatedly removing the first creates quadratic work.

An occasional expensive append can average out

A simplified capacity-doubling model, not exact Python capacities
capacity
124816
copy on growth
—1248
Over n appends, geometric copying stays linear1 + 2 + 4 + … < 2n

Most appends place one reference; a resize copies many. Spread all resizing work over the whole sequence: append costs O(1) amortised. An individual append can still take O(n). Python uses its own overallocation rule.

Choose the operation before the container

Operationlistcollections.deque
Read a middle indexO(1)O(n)
Append at an endO(1) amortisedO(1)
Remove from the frontO(n)O(1)
Search for a valueO(n)O(n)
Run in Colab · predict the result first
from collections import deque
queue = deque(["A", "B", "C"])
queue.append("D")
first = queue.popleft()
print(first, list(queue))

A deque fits a queue: add at the back, remove at the front. It does not make arbitrary searches constant-time.

Repeated copying can hide inside short code

Run in Colab · predict the result first
parts = ["algorithm", "analysis", "with", "Python"]
text = " ".join(parts)
print(text)
Naïve repeated prefix copying, n equal-size pieces1 + 2 + … + n = n(n + 1)/2 units

join builds the final text in work proportional to total output length. Some Python string-concatenation patterns are optimised, so timings may vary; use the copying model only when each step really copies the prefix. Repeated list result = result + [x] does copy the growing list.

Practice

Practice questions

10 test questions · 9 written questions · 19 total

Each question is followed by its answer and explanation. Hard questions also include a starting hint and smaller reasoning steps.

Test questions

Choose one option. Cost questions state their model and whether the bound is tight, expected or worst-case. Here lg means log₂, and heap positions start at 1.

Question 1 · easy

Inserting a key into a packed sorted array may require shifting items. What is the tightest listed worst-case upper bound?

  1. O(1)
  2. O(log n)
  3. O(n)
  4. O(n log n)

Answer: option C. Finding the spot is O(log n), but shifting the later items over is O(n).

Question 2 · easy

A dynamic array starts with capacity 1 and doubles only when full. How many doublings are needed to hold 5,000 items?

  1. 5 times
  2. 13 times
  3. 50 times
  4. 5,000 times

Answer: option B. 2¹² = 4,096 is too small and 2¹³ = 8,192 is enough.

Question 3 · medium

Growing a full dynamic array from capacity 1 to capacity 1,024 by doubling copies exactly how many existing items in total?

  1. 1,024 items
  2. 1,023 items
  3. 10,240 items
  4. 1,048,576 items

Answer: option B. 1 + 2 + 4 + … + 512 = 1,023, less than n.

Question 4 · medium

If the array instead grows by 100 slots each time it fills, inserting n items costs in total:

  1. Θ(n)
  2. Θ(n log n)
  3. Θ(n²)
  4. Θ(1)

Answer: option C. About n/100 expansions, each copying up to n items.

Question 5 · medium · course question

In a packed array-backed list of n items, inserting at the front shifts how many existing items?

  1. n
  2. 1
  3. 0
  4. n²

Answer: option A. Every existing item moves one slot to make room. This gives linear work for a front insertion.

Question 6 · medium · course question

For an ordinary array-backed list, what is the tight worst-case cost of reading an existing index?

  1. Θ(n)
  2. Θ(log n)
  3. Θ(n²)
  4. Θ(1)

Answer: option D. The slot address is computed directly from the index under the standard RAM model. No scan of preceding items is needed.

Question 7 · medium · course question

Why is a single append sometimes slower than most other appends to a dynamic array?

  1. the array must sort itself
  2. its capacity may be full, requiring allocation and copying
  3. append must always shift every existing item
  4. the new item must be the smallest

Answer: option B. When capacity is exhausted, resizing can copy existing items. Most appends use an already available slot, giving constant amortised cost with geometric growth.

Question 8 · medium · course question

A slice copies k references from an ordinary Python list into a new list. What is the tight time bound in k?

  1. Θ(1)
  2. Θ(log k)
  3. Θ(k)
  4. Θ(k²)

Answer: option C. The operation must place k references in the new list. Copying references is different from recursively copying the referred-to objects.

Question 9 · medium · course question

Build a new list by repeatedly replacing result with result + [x] for n items. If each concatenation copies the growing list, what is the total copied-item growth?

  1. Θ(n²)
  2. Θ(n)
  3. Θ(log n)
  4. Θ(1)

Answer: option A. The copies have lengths 1,2,…,n. Their sum n(n+1)/2 grows quadratically; repeated append avoids copying the entire prefix on every step.

Question 10 · medium · course question

A dynamic array has capacity 8 and currently holds 8 items. It doubles capacity before storing one more. What are its new length and capacity?

  1. length 16, capacity 16
  2. length 8, capacity 9
  3. length 9, capacity 9
  4. length 9, capacity 16

Answer: option D. Only one new item is added, so length becomes 9. Doubling the allocated space changes capacity from 8 to 16.

Written questions

Read each question together with its explanation, trace or proof. Numbering continues from the test questions.

Question 11 · easy

List the seven dictionary operations.

Answer & reasoning

search, insert, delete, minimum, maximum, predecessor, successor.

Question 12 · easy

In a sorted array, what do search and insert cost, and why is insert slow?

Answer & reasoning

search O(log n) by binary search; insert O(n), because after finding the right spot every later item must shift one place to make room.

Question 13 · easy

Starting with a dynamic array of size 1 and doubling whenever it fills, how many doublings are needed before it can hold 1,000 items?

Answer & reasoning

10, since 2¹⁰ = 1,024 ≥ 1,000; in general ⌈lg n⌉.

Question 14 · medium

A dynamic array holds n = 2ᵏ items after growing from size 1 by doubling. How many item-copies were made in total over all the doublings? Show the total is less than n.

Answer & reasoning

the doublings copied 1, 2, 4, …, 2ᵏ⁻¹ items, totalling 2ᵏ − 1 = n − 1 < n. So the copying costs O(n) in total, or O(1) per item on average (“amortised”).

Question 15 · medium

Suppose the array grows by adding 10 slots each time it fills, instead of doubling. What does inserting n items cost in total?

Answer & reasoning

Θ(n²). About n/10 expansions copy 10, 20, 30, … items, whose sum grows quadratically. A multiplicative factor greater than 1, such as 1.5 or 2, gives linear total copying instead; doubling is convenient but not essential.

Question 16 · medium

You hold a pointer to a node x in a singly linked list, x is not the last node, and you have no pointer to the head. Delete x’s value from the list in O(1).

Answer & reasoning

copy the next node’s item into x, then splice out the next node: x.next = x.next.next. The list now contains the same values minus x’s original one, and no predecessor search was needed.

Question 17 · medium

Build a queue out of two stacks so that every operation is O(1) on average.

Answer & reasoning

enqueue pushes onto stack In. Dequeue pops from stack Out; if Out is empty, first pop everything from In and push it onto Out (which reverses the order). Each item is moved at most twice in its lifetime, so n operations cost O(n) in total.

Question 18 · hard

Design a stack that supports push, pop and get-minimum, all in O(1).

In simpler words: Keep the smallest stack value available without searching.

Starting hint: Remember the minimum after every push.

Answer & reasoning
Step by step
  1. Push 5,3,7. The main stack is [5,3,7]; the minimum stack is [5,3,3].
  2. Each minimum entry describes the main-stack prefix ending at the same depth.
  3. Pop both stacks together. The new minimum is still at the top. On the first push use x itself; handle empty-stack queries explicitly.

keep a second stack of running minima. On push(x), push x onto the main stack and push min(x, current top of the min stack) onto the min stack. On pop, pop both. The top of the min stack is always the minimum of the main stack.

Question 19 · hard

Design a cache that holds at most k items and evicts the least recently used one, with get and put both in O(1).

In simpler words: Evict the item that has gone longest without a recent use.

Starting hint: Use a hash map to find an item and a linked list to record recency.

Answer & reasoning
Step by step
  1. The list front is most recent; the back is least recent. Map each key directly to its list node.
  2. For a successful get or an update, detach the node and move it to the front using its two links.
  3. For a new item at capacity, remove the back node and its map entry. Hash lookup is expected O(1); list changes are O(1). Define the k=0 case as storing nothing.

Use a doubly linked list ordered by recency and a hash map from keys to nodes. Get moves a found node to the front. Put inserts or updates at the front; if capacity is exceeded, unlink the tail and remove its map entry. Pointer changes are worst-case O(1); the combined operations are expected O(1) with suitable hashing, not a universal worst-case guarantee.

Three core tasks

Predict on paper, run the code, then explain any difference. Use the animations below to inspect individual steps.

1. Trace

Draw each pop(0) while emptying four items. Count shifts.

Check your reasoning

3 + 2 + 1 + 0 = 6 shifts.

2. Calculate

How many shifts occur when emptying 1,000 items from the front?

Check your reasoning

1,000 × 999 / 2 = 499,500.

3. Change one thing

Replace a list queue with deque. Which operation changes, and which stays linear?

Check your reasoning

Use popleft() for O(1) front removal. Searching for an arbitrary value still costs O(n).

Explore the animations & more worked tasks

10.9Try it yourself

Task 1 — verify the table

Design and run a doubling experiment for each of these, and state the class you measured: data[n // 2], data.append(1), data.pop(0), x in data (for a missing x), data[:] (a full copy).

Animate it — run the doubling experiment on all five operations
Hint on method

For the O(1) operations one call is far too fast to time — repeat the operation 100 000 times inside the timed region and divide, keeping the list size fixed between repeats. For pop(0), rebuild the list before each timing.

Task 2 — fix a slow function

Here is a real-shaped piece of slow code. Find both problems and rewrite it.

slow.py
def unique_reversed(data):
    result = []
    for item in data:
        if item not in result:          # problem 1
            result = [item] + result    # problem 2
    return result
Animate it — step through slow.py and fast.py and count the work
Solution

Problem 1: not in result is an O(n) scan inside an O(n) loop → O(n²). Problem 2: building a new list every round is another O(n) per item. Fixed version, using a set for the membership test (week 11) and appending at the cheap end:

fast.py
def unique_reversed(data):
    seen = set()
    result = []
    for item in data:
        if item not in seen:      # O(1)
            seen.add(item)
            result.append(item)   # O(1)
    result.reverse()              # O(n), once
    return result

From O(n²) to O(n). At 100 000 items that is minutes down to milliseconds — measure both to see it.

Task 3 — the string version

Build a string of 50 000 words by repeated +=, then by collecting into a list and calling join. Time both, and report the ratio at 25 000 and 50 000 to investigate the observed scaling, including any interpreter optimisation.

Animate it — watch += copy the text while join writes it once
Expected

If every concatenation rebuilds the growing text, expect a near-four doubling ratio; if the interpreter optimises this loop, it may instead be near two. Joining fixed-length pieces once has linear total-character work. Report the measured outcome rather than forcing the illustrative §10.5 numbers.

Task 4 — see the amortised jumps

Adapt watch_capacity.py from §10.3 to append 1 000 items and record the length at every capacity jump. Roughly how does each new capacity compare with the previous one, and how does that explain "O(1) amortised"?

Animate it — append 1 000 items and record every capacity jump
Expected

Each jump lands at roughly (a bit more than) the previous capacity — the list grows by a near-constant factor, so the gaps between jumps get wider as n grows. Because each expensive copy buys room for proportionally more cheap appends, the copying cost spread across all appends averages to a constant: O(1) amortised.

Task 5 — list or deque?

For each scenario, say whether you would store the data in a list or a deque, and why in one sentence: (a) a to-do queue you always take from the front and add to the back; (b) a table of scores you look up by row number thousands of times; (c) a browser history where you add to one end and occasionally pop the other.

Animate it — pick list or deque, then run the scenario on both
Answers

(a) deque — both hot operations are at the ends, O(1) each; a list would be O(n) per pop(0). (b) list — random indexing by position is O(1) on a list but O(n) on a deque. (c) deque — adds and pops at the two ends are what a deque is for.

Check your understanding

10.10Self-check

Why is data.pop(0) O(n) while data.pop() is O(1)?

A list is a contiguous row of slots. Removing from the end leaves no gap; removing from the front leaves one that must be closed.

result = result + [i] inside a loop of n items is:

Copying a list of length k costs k. Doing that for k = 1, 2, 3 … n adds up to about n²/2.

You keep taking jobs from the front of a very long queue. The right change is:

A deque is designed for cheap operations at both ends: O(1) per removal instead of O(n).

"append is O(1) amortised" means:

Growing the list copies everything, but that happens rarely and each copy buys room for many cheap appends, so the average per append is constant.

To turn a quadratic text += word loop into a linear one, you should:

Strings get copied on every +=. Collecting pieces and joining once does the whole build in a single linear pass.

On a deque, reading a middle position dq[i] is:

A deque trades cheap middle indexing for cheap end operations. Use a list when you index into the middle a lot.
Extra material & reference
Optional depth · full technical reference

10.1What a list really is

Picture a row of numbered lockers, side by side, with no gaps. That is a Python list: a block of slots in order, each holding a reference to a value.

Two consequences follow from the "no gaps" part, and they explain everything else:

  • Reading a position is instant. To find item 4 719, the computer computes where the slot is and goes there. Length is irrelevant: O(1).
  • Inserting or removing at the front is expensive. If you take out the first locker, everything behind it has to shuffle forward one place to close the gap. That is n moves: O(n).
The mental picture to keep

Adding at the end is like putting a book on the end of a shelf. Adding at the front is like inserting a book at the start of a full shelf: every other book moves. Same act, entirely different price.

10.2The cost table

OperationExampleCostWhy
index read / writedata[i]O(1)Jump straight to the slot
lengthlen(data)O(1)Python keeps a running count
appenddata.append(x)O(1)*Write into the next free slot
pop from enddata.pop()O(1) amortisedNo tail shifts; occasional resizing may occur
insert at frontdata.insert(0, x)O(n)Everything shifts right
pop from frontdata.pop(0)O(n)Everything shifts left
membershipx in dataO(n)Look at items until found
delete by valuedata.remove(x)O(n)Find it, then shift
slicedata[a:b]O(k)Copies k items into a new list
concatenatea + bO(n+m)Builds a whole new list
sortdata.sort()O(n log n)See week 13
min / max / summax(data)O(n)Must inspect everything

* append is O(1) amortised: occasionally Python has to move the whole list into a bigger block of memory, which costs O(n), but it reserves extra capacity so total resizing work across many appends remains linear. The exact growth policy depends on the implementation. The next section watches that happen.

10.3Amortised append: watching the list grow

The lockers picture has one loose end: what happens when the row is full and you append one more? There is no free slot behind the last one. Python's answer is to rent a bigger row with spare capacity, potentially copy the existing references across, and then write the new one. That copy is O(n). So how can we call append cheap?

Because the expensive copies are rare, and they get rarer as the list grows. Use an idealised doubling model with capacities 1, 2, 4, 8, 16 …; this is not Python's literal allocation policy. For n items where n is a power of two, copying costs 1 + 2 + … + n/2 = n − 1. Together with n new writes, that is 2n − 1 units: a constant amortised cost per append. Other n also give linear total work. Sharing the cost of the rare big events across the many cheap ones is called amortising, and it is why we quote append as O(1).

You can see the capacity jumps directly. sys.getsizeof reports how many bytes a list occupies; watch it step up in chunks, not smoothly, as we append:

watch_capacity.py
import sys

data = []
last = -1
for i in range(17):
    size = sys.getsizeof(data)
    if size != last:                       # only print when it jumps
        print(f"len={len(data):>2}   {size} bytes  <- grew here")
        last = size
    else:
        print(f"len={len(data):>2}   {size} bytes")
    data.append(i)
len= 0 56 bytes <- grew here len= 1 88 bytes <- grew here len= 2 88 bytes len= 3 88 bytes len= 4 88 bytes len= 5 120 bytes <- grew here len= 6 120 bytes len= 7 120 bytes len= 8 120 bytes len= 9 184 bytes <- grew here len=10 184 bytes len=11 184 bytes len=12 184 bytes len=13 184 bytes len=14 184 bytes len=15 184 bytes len=16 184 bytes

Most appends land in already-rented space and cost nothing extra — those are the flat stretches. Only at a jump does Python copy the list into a bigger block, and each block lasts longer than the last, so the jumps thin out. Averaged over the whole run, the cost per append settles to a constant. That is exactly what "O(1) amortised" means: usually cheap, occasionally dear, constant on average.

The word to reach for

"Amortised" is the honest word for "individual calls can spike, but the long-run average is constant". It bounds the total work of a sequence rather than relying on random inputs. An individual resizing append can still have O(n) worst-case cost; that and O(1) amortised cost are compatible. This does not promise identical wall-clock time for every call. When someone asks the cost of append, "O(1) amortised" is the full and correct answer.

10.4Measuring it: front versus back

Test the model yourself. These builders use the same values but produce opposite orders: append preserves their arrival order, while front insertion reverses it. If order matters, include a final O(n) reversal when comparing equivalent outputs.

append vs insert(0)
import time

def build_append(n):
    data = []
    for i in range(n):
        data.append(i)          # cheap end
    return data

def build_insert_front(n):
    data = []
    for i in range(n):
        data.insert(0, i)       # expensive end
    return data

for n in [10000, 20000, 40000, 80000]:
    start = time.perf_counter(); build_append(n)
    t_app = time.perf_counter() - start

    start = time.perf_counter(); build_insert_front(n)
    t_ins = time.perf_counter() - start

    print(f"n={n:>6}  append {t_app:.4f}s   insert(0) {t_ins:.4f}s   ratio {t_ins/t_app:,.0f}x")
n= 10000 append 0.0008s insert(0) 0.0221s ratio 27x n= 20000 append 0.0016s insert(0) 0.0872s ratio 55x n= 40000 append 0.0031s insert(0) 0.3510s ratio 113x n= 80000 append 0.0062s insert(0) 1.4102s ratio 227x

Read the ratio column: it doubles every time n doubles. That is the fingerprint of one method being O(n) overall while the other is O(n²) — because n insertions at O(n) each is n × n. The two loops look almost identical on the page. One of them is a bomb with a slow fuse.

10.5Building strings and lists the fast way

The same trap appears when you build a list or a string by repeated concatenation. result = result + [x] creates a whole new list each round.

four ways to build the same list
import time

def by_concat(n):
    result = []
    for i in range(n):
        result = result + [i]        # new list every time: O(n) each
    return result

def by_append(n):
    result = []
    for i in range(n):
        result.append(i)             # O(1) each
    return result

def by_comprehension(n):
    return [i for i in range(n)]     # same work, less Python overhead

def by_list_range(n):
    return list(range(n))            # all the work happens in C

for func in [by_concat, by_append, by_comprehension, by_list_range]:
    start = time.perf_counter()
    func(20000)
    print(f"{func.__name__:>18}: {time.perf_counter() - start:.5f}s")
by_concat: 0.41230s by_append: 0.00151s by_comprehension: 0.00062s by_list_range: 0.00031s

A factor of over a thousand between the first and the last, for code that produces an identical result. The first is O(n²); the other three are O(n) with progressively smaller constants. Note the two different kinds of improvement on display: the jump from by_concat to by_append is a class change and it grows with n; the jump from by_append to list(range(n)) is a constant factor and it does not.

Same trap, with text

Repeated string concatenation can be quadratic when every step copies the growing result. Some Python interpreter contexts optimise this pattern, so a particular += benchmark need not show quadratic growth. Collect the pieces in a list and use "".join(pieces) once at the end. That single habit has rescued more slow scripts than any other tip in this course.

+= vs join, side by side
import time

def by_plus(n):
    text = ""
    for i in range(n):
        text += "word "        # repeated concatenation; runtime optimisations may affect scaling
    return text

def by_join(n):
    pieces = []
    for i in range(n):
        pieces.append("word ")  # O(1) each
    return "".join(pieces)      # one linear pass at the end

for n in [25000, 50000, 100000]:
    start = time.perf_counter(); by_plus(n)
    p = time.perf_counter() - start
    start = time.perf_counter(); by_join(n)
    j = time.perf_counter() - start
    print(f"n={n:>6}   += {p:.4f}s   join {j:.4f}s   ratio {p/j:,.0f}x")
n= 25000 += 0.0480s join 0.0018s ratio 27x n= 50000 += 0.1910s join 0.0035s ratio 55x n=100000 += 0.7640s join 0.0069s ratio 111x

These illustrative timings show the repeated-copy model; they are not a guaranteed output of this interpreter-dependent experiment. Record your actual ratios, even if += scales nearly linearly. Building pieces and joining once gives a predictable linear construction in the total number of characters.

10.6When you need a fast front: deque

Sometimes you genuinely need to add and remove at both ends — a queue of jobs, a history of moves. Python has a structure built for it.

list vs deque at the front
from collections import deque
import time

n = 100000

data = list(range(n))
start = time.perf_counter()
while data:
    data.pop(0)                      # O(n) each → O(n²) total
print(f"list.pop(0):    {time.perf_counter() - start:.3f}s")

data = deque(range(n))
start = time.perf_counter()
while data:
    data.popleft()                   # O(1) each → O(n) total
print(f"deque.popleft(): {time.perf_counter() - start:.3f}s")
list.pop(0): 1.812s deque.popleft(): 0.006s

Three hundred times faster, and the gap widens with n. You do not need to know how a deque works internally to use it. You do need the habit of asking "is there a structure designed for the operation I keep repeating?" — which is the whole of next week.

When a list is still right

A deque is not a free upgrade: reading a middle position, dq[i], is O(n) on a deque but O(1) on a list. Use a list when you index into the middle a lot; reach for a deque when your hot operations are at the ends. Right tool, right job — which needs the cost table for both.

10.7Measuring every operation at once

You have measured a few operations one pair at a time. Here is a single harness that times all the headline operations at a fixed size, so the O(1) rows and the O(n) rows sit next to each other and the difference is impossible to miss. The O(1) operations are far too fast to time once, so we repeat each many times inside the timed region and report the time per operation. Each repeat gets fresh input. Append grows its list, while pop removes 50 000 of the available 100 000 items; these are averages over changing sizes, not fixed-size single-operation measurements. The front-removal row drains its 2 000-item list exactly once. The output below is illustrative, and sorting this already sorted input exercises the adaptive best case.

cost_of_every_operation.py
import time

n = 100000
base = list(range(n))

def time_op(setup, op, repeats):
    """Best-of-3 total time for running op() `repeats` times."""
    best = None
    for _ in range(3):
        data = setup()
        start = time.perf_counter()
        for _ in range(repeats):
            op(data)
        t = time.perf_counter() - start
        if best is None or t < best:
            best = t
    return best / repeats            # seconds per single operation

fresh = lambda: list(range(n))

rows = [
    ("index read   data[n//2]", fresh, lambda d: d[n // 2],        200000),
    ("append       d.append(0)", fresh, lambda d: d.append(0),     200000),
    ("pop end      d.pop()",     fresh, lambda d: d.pop(),          50000),
    ("insert(0)    d.insert(0,0)", lambda: list(range(2000)), lambda d: d.insert(0, 0), 2000),
    ("pop(0)       d.pop(0)",    lambda: list(range(2000)), lambda d: d.pop(0),      2000),
    ("membership   -1 in d",     fresh, lambda d: (-1 in d),          200),
    ("sort         sorted(d)",   fresh, lambda d: sorted(d),           50),
]

print(f"{'operation':>26}   {'sec / op':>12}")
for label, setup, op, reps in rows:
    per = time_op(setup, op, reps)
    print(f"{label:>26}   {per*1e9:>9,.0f} ns")
operation sec / op index read data[n//2] 38 ns append d.append(0) 52 ns pop end d.pop() 49 ns insert(0) d.insert(0,0) 1,120 ns pop(0) d.pop(0) 980 ns membership -1 in d 1,410,000 ns sort sorted(d) 6,900,000 ns

Read it as three tiers. The O(1) operations — index, append, pop from the end — all sit around a few dozen nanoseconds, flat and cheap. The front operations insert(0) and pop(0) are twenty to fifty times dearer even on a much shorter list, because each one shifts the whole tail. The O(n) scans (in a missing value) and the sort of this already ordered list are in a different world entirely — a missing-value membership test on 100 000 items is nearly forty thousand times slower than an index read. The numbers on your machine will differ; the work models explain the expected trends, but precise relative tiers can vary.

One size is a snapshot, not a class

This table pins n and compares operations. It shows you which operations are expensive right now, but a single size cannot reveal a class — for that you need the doubling harness from weeks 5–7, watching how each number responds when n doubles. Use both: the snapshot to spot the costly operation, the doubling run to name its class.

10.8Common mistakes

MistakeWhy it is wrong
result = result + [x] in a loopCopies the whole list each round → O(n²). Use append.
Repeated string concatenationCan copy the growing text repeatedly; interpreter optimisations affect measurements. Collect pieces and join once for predictable linear construction.
Using a list as a queue with pop(0)Each removal shifts the tail → O(n²) over the queue. Use deque.popleft().
if x in my_list inside a loopA hidden O(n) scan under your O(n) loop → O(n²). Use a set for membership (week 11).
"append is O(n) because of the resize"The resize is O(n) but rare; averaged over all appends it is O(1) amortised.
"a deque is just a faster list"Middle indexing on a deque is O(n); it wins only at the ends.
Slicing to copy inside a loop, data[:]Each copy is O(n); doing it once per item is another accidental O(n²).

From the beginner notes · Lecture 4

Occasional expensive work can hide in an average

A dynamic array is an array that can grow; it keeps some empty slots for new items. In the simplified doubling model, growth copies 1, 2, 4, … items into successively larger arrays. To reach n = 2ᵏ stored items from capacity 1, the copies total n − 1, even though the final resize alone is expensive.

This gives constant copying cost per append when the total is spread over the whole sequence (amortised cost). It does not make every individual append constant-time. Growing by only ten slots instead repeatedly copies nearly all the items already stored, producing quadratic total copying.

The exact way Python reserves extra slots depends on its implementation; the doubling model explains the idea rather than specifying its precise behaviour. Keep “amortised over a sequence” separate from “expected over random choices” and “worst case for one operation”.

Engineering use. A data logger may tolerate occasional slow appends. A controller with a strict deadline may not tolerate the pause when storage grows.

Learning goals & class plan
By the end of this week you can
  • say what the common list operations cost, and why;
  • explain why append is cheap and insert(0, x) is not;
  • explain what "amortised O(1)" means and why append earns it;
  • measure the difference yourself instead of taking my word for it;
  • rewrite a slow list-building loop into a fast one;
  • read a cost table for a data structure and use it to make a choice.
Three-hour interactive studio

00:00–01:00: launch, predict–run–explain cycle, questions  ·  01:00–01:10: break  ·  01:10–02:00: worked variation, peer instruction, questions  ·  02:00–02:10: break  ·  02:10–03:00: core mechatronics practice, exam bridge, and exit ticket.

Ask at any point. Weekly self-checks stay private; optional extensions are not collected.

Need a slower explanation? Open the English + Türkçe reference guide.

Optional extra practice

10.11Optional Studio Extension

Optional practice · no submission or deadline
  1. Create AA_Week10.ipynb and complete Tasks 1–5.
  2. Reproduce §10.4 and §10.5 on your own machine and put all three ratio columns (insert-vs-append, and += vs join) in one table.
  3. Run watch_capacity.py from §10.3 up to 1 000 items and note the lengths at which the size jumped; write two sentences linking what you saw to the phrase "O(1) amortised".
  4. Run the §10.7 all-operations harness on your machine and paste the table; write one sentence naming the three tiers you see and which operations fall in each.
  5. Produce one labelled figure comparing append and insert(0, …) across four sizes.
  6. Write five sentences: you inherit a script that took 20 minutes on 10 000 rows and now must run on 100 000. Based on this week, name the two things you would look for first, and what you would expect the runtime to become if the culprit is quadratic.
Optional reference · Words from this week

10.12Words from this week

TermMeaning in plain words
contiguous storageItems kept in one unbroken row of slots — the reason indexing is instant.
shiftingMoving every later item to close or open a gap; the hidden cost at the front.
amortised O(1)Usually constant, occasionally expensive, cheap on average over many calls.
resize / capacityThe bigger block a list rents when it fills up; the rare O(n) copy behind append.
accidentally quadraticAn O(n) operation used once per item, giving O(n²) without any visible nested loop.
dequeA structure with cheap adds and removes at both ends, but O(n) middle indexing.
join"".join(pieces) — the linear way to build a big string.
Where this leads

You learned that a full list search and a front insertion cost O(n). Week 11 introduces average O(1) lookup with dictionaries and sets, under the usual hashing assumptions.