Linked List vs Array Benchmark
Apologetic question: "How are many members joined into one body?"
Project 3 — Linked List vs Array Benchmark
“For just as the body is one and has many members, and all the members of the body, though many, are one body, so it is with Christ.” — 1 Corinthians 12:12
Chapter: 3 — Linked Lists and the Cost of Pointers
Due: End of Week 3
Submit: A link to a public GitHub repo containing linked_list.py, your test file, benchmark.py, the benchmark results, and README.txt. Real toolchain, real git — see Appendix A for the Python + git setup.
Allowed tools: Python 3.10+, the standard library (time, sys, unittest/pytest, random), a real editor, the textbook, your own DynamicArray from Project 2, and the given array_front_insert.py in this chapter’s code/ folder (the front-insert array you’re handed — see the front-insertion workload below). No external packages, no IDE running for you, no internet during the build.
AI: Phase 1 (wk 1–8): AI is OFF. No assistants, no autocomplete-beyond-a-word, no “explain this.” You cannot judge the cost of a structure you have never built and raced yourself — and the whole point of this project is to feel the gap between “O(1) insert” and “actually wins the race,” so that in Phase 2 you can direct an agent and know when its data-structure choice was wrong. Phase 2 (wk 9–16) turns agentic AI on and requires an agent-log.txt. Not yet.
The Setup
A ministry runs a long-lived event service. As people arrive, items stream in — check-ins, prayer requests, song requests — and a lead developer reached for a linked list because “everyone knows linked lists are O(1) to insert, and an array has to shift everything.” The service has gotten slow. The “fast” structure is, somehow, the slow part. The team is confused: the Big-O on the whiteboard clearly favors the linked list.
You’ve been brought in to settle it the only way it can be settled — by measurement.
You will build a singly linked list by hand, race it against your dynamic array from Project 2 across the workloads the service actually runs, and write the memo that tells the team the truth: where the linked list’s celebrated O(1) operations genuinely win, where they lose despite the asymptotics because every node is a scattered heap allocation that pointer-chases across cache lines, and which structure their actual workload deserves. This is the chapter’s whole lesson made into a deliverable: Big-O describes how cost grows; it does not describe how much it is, and the constant factor decides real races.
You are the architect. The team has the Big-O. You bring the numbers.
Learning Targets
By completing this project, you will demonstrate that you can:
- Implement a singly linked list by hand — nodes, head/tail pointers,
prepend,append,find,delete,__len__,__iter__— keeping head, tail, and length as honest invariants. - Implement the three delete cases (head, tail, middle) correctly, including the tail-pointer fixup.
- Build a fair benchmark harness that races two structures across multiple workloads with honest timing.
- Distinguish, in measurements, a same-Big-O-different-constant result from a different-Big-O result.
- Implement a doubly linked list and demonstrate the one workload it genuinely wins — the O(1) held-node delete — in numbers (Medium).
- Write an architectural memo that makes a tool choice from your own data and names where cache locality made the “worse” structure win (Hard) — the Phase 1 deliverable.
Normal Tier
Goal: Implement a singly linked list, test it, and race it against a dynamic array across four workloads — your own DynamicArray from Project 2 where its operations suffice, and the given array_front_insert.py for the one workload that needs a front insert Project 2’s Normal tier didn’t ask you to build (details under Required features, item 4).
Required features
-
Node— a class withvalueandnext(use__slots__). Nothing more. -
SinglyLinkedListwith:prepend(value)— O(1) front insert.append(value)— O(1) back insert, using a tail pointer (no walking).find(value)— return the first node holdingvalue, orNone. O(n).delete(value)— delete the first node holdingvalue; returnTrue/False. Handle head, tail, and middle correctly; keep_tailand_lengthhonest.__len__— O(1).__iter__— yield each value front to back.__str__— readable, e.g.SinglyLinkedList([10 -> 20 -> 30]).
-
Tests (
unittestorpytest), at least 8, proving:- Empty list: length 0, iterates to
[]. prependputs the value at the front.appendkeeps order and is reachable from the head.- All three delete cases, plus the tail-delete-then-append test (the bug from §3.8 / Rep 8).
deleteof an absent value returnsFalseand changes nothing.findreturns a node for present values,Nonefor absent.
- Empty list: length 0, iterates to
-
Benchmark harness (
benchmark.py) that races yourSinglyLinkedListagainst a dynamic array across four workloads, timing each fairly (median of ≥3 runs,time.perf_counter):- Random access — sum the elements at 2,000 random indices.
- Front insertion — insert
nelements at the front. - Back insertion (build) — append
nelements. - Full traversal — sum every element.
Which array do you race? Three of these four workloads (random access, back insertion, full traversal) need only
append,get,__len__, and__iter__— exactly what Project 2’s Normal tier had you build, so your ownDynamicArraycovers them. The front-insertion workload, though, needs an array that can insert at the front, and Normal-tier Project 2 only asked forappend(insert at the back). So you don’t have to go back and add a method, this chapter’scode/folder gives youarray_front_insert.py— a completeDynamicArraywithappend,get,__len__,__iter__, and an O(n)insert_front. Import that for the benchmark. (If you did Project 2’s Medium tier, you already wroteinsert(0, value), which is a front insert — you may race your own array instead;insert_front(value)is justinsert(0, value)renamed. Either path is full credit; say which you used in your README.) -
Results captured — a
results.xlsx(or a results table in your README) with your machine’s numbers andn, plus one sentence per workload explaining the outcome in terms of Big-O and the constant factor.
Example output
Linked list vs dynamic array, n = 40,000
BUILD (append n elements):
dynamic array .append 6.40 ms
linked list .append 8.94 ms
-> same Big-O; array wins the per-element constant cost.
TRAVERSE (sum every element):
dynamic array (contiguous) 1.69 ms
linked list (pointer-chase) 1.57 ms
-> same Big-O; ~tie in CPython (interpreter overhead masks the cache effect).
RANDOM ACCESS (2,000 random reads):
dynamic array O(1)/access 0.10 ms
linked list O(n)/access 470.37 ms
-> different Big-O; the array dominates.
FRONT INSERT (insert n at the front):
dynamic array O(n)/insert 7457.46 ms
linked list O(1)/insert 4.31 ms
-> the list's better Big-O finally pays: O(n) total beats O(n^2).
Normal-tier rubric (out of 100)
| Criterion | Points |
|---|---|
Node + list structure correct, runs cleanly | 6 |
prepend O(1), append O(1) with a tail pointer | 12 |
find correct, O(n) | 6 |
delete correct for head, tail, AND middle cases | 14 |
__len__ O(1) and __iter__ correct | 8 |
| Tests: at least 8, including the tail-delete-then-append test | 14 |
| Benchmark harness races both structures across all four workloads | 16 |
| Timing is fair (median of ≥3 runs, warm-up handled) | 8 |
results.xlsx with numbers + one sentence per workload (Big-O AND constant) | 12 |
| README + reflection + AI honesty line | 4 |
Medium Tier (+up to 25% extra credit)
M1. Doubly linked list
Implement a DoublyLinkedList with DNode (value, next, prev), append/prepend that return the node, delete_node(node) (O(1) splice of a held node), __iter__, and __reversed__. Test it, including the held-delete and the backward traversal.
M2. The held-delete win, in numbers
Add a fifth workload to your benchmark: delete a node you already hold a reference to. Race the doubly linked list’s O(1) delete_node against the array’s O(n) delete-at-index (which must shift every element after the gap left to close it). Time many deletes of each. Show the win — it should be a wall-clock chasm (milliseconds vs seconds at a few tens of thousands). Add the row to results.xlsx with the one-sentence explanation: this is the one workload the linked list genuinely wins, on Big-O and constant factor both.
M3. The memory cost of the second pointer
Measure (with sys.getsizeof, accounting for __slots__) the per-node memory of a singly vs doubly linked node, and report the total extra memory the doubly linked list costs at n = 1,000,000. State the trade in one sentence: what the second pointer buys (O(1) held-delete, backward walk) and what it costs (an extra reference per node).
Hard Tier (+up to 25% additional extra credit)
H1. The architect’s memo
Write MEMO.docx (one page, ~500–700 words) addressed to the ministry’s team. Using your own measurements, answer:
- For which specific, realistic workloads would you choose a linked list over a dynamic array? Name at least two concrete scenarios (e.g., an LRU cache that splices held nodes; a front-insert-heavy log with no random access). Cite your numbers.
- For which would you not? Name at least two (e.g., anything index-heavy; anything build-then-iterate). Cite your numbers — especially the random-access massacre.
- Where did cache locality (or per-node allocation) make the asymptotically “worse” structure win? Point at the exact row in your results and explain the constant factor. Be honest about the CPython traversal nuance: the cache effect is real but masked at the interpreter layer (§3.6) — say so, and say how the answer would change in C.
- What does the ministry’s actual service deserve, and why? Make the call. Defend it with constraints, not vibes.
The memo is the heart of this project. The code proves you can build; the memo proves you can decide. An agent cannot write this memo for you — it has never run your benchmark on your machine.
H2. Make the benchmark fair and defend it
A naive benchmark lies. Audit your own harness for at least three fairness traps and document how you handled each in MEMO.docx or a BENCHMARK_NOTES.txt:
- Warm-up / cold cache — first run is slow; take the median, discard the cold run.
- Construction cost bleeding into the measured op — build the structure outside the timed region; time only the operation under test.
- Garbage-collection pauses — consider
gc.disable()around a measured region (and re-enable it after), and note whether it changed your numbers. ntoo small to see the asymptotics — show at least one workload at two or three sizes so the shape (constant vs linear vs quadratic) is visible, not just one data point.
H3. The crossover point
For the front-insert workload, find (by measuring at several sizes) the approximate n at which the linked list overtakes the array — the crossover point where the O(n) total finally beats the O(n²) total. Plot or tabulate it. Write one paragraph: below the crossover, the array’s constant-factor advantage wins despite the worse Big-O; above it, the asymptotics take over. This is the most important single idea in Phase 1 — Big-O tells you who wins eventually; measurement tells you when “eventually” arrives.
Submission
Submit one URL — a public GitHub repo (see Appendix A for git setup).
What the repo must contain
linked_list.py— yourSinglyLinkedList(andDoublyLinkedListfor Medium+).- The array the benchmark imports — either the given
array_front_insert.py(copy it into your repo), or your own Project 2dynamic_array.pyif it has a front insert (insert(0, value)from Project 2 Medium). Whichever you race, commit it sopython3 benchmark.pyruns from a clean checkout. test_linked_list.py— the test suite. Confirm it passes before submitting.benchmark.py— the harness. Runnable withpython3 benchmark.py.results.xlsx— your machine’s numbers,n, and one sentence per workload.MEMO.docx— for Hard tier, the architect’s memo.README.txt— your reflection:
# Project 3 — Linked List vs Array Benchmark
**Tier targeted:** Normal / Medium / Hard
**Features done:** (list)
**Headline result:** (one line — which structure won which workload, and the surprise)
**The one place the list genuinely won:** (which workload, and the numbers)
**The place asymptotics lied at my scale:** (which row, and why — cache / allocation)
**What I learned:** (one paragraph)
**What I'd change:** (one sentence)
**AI usage:** NONE — Phase 1. Signed: <your name>
- Reflection comment block at the top of
benchmark.py— condensed version of the README fields. - The benchmark left runnable —
python3 benchmark.pyproduces your results table from a clean checkout.
Hints (Read Before You Begin)
- Build the list first, race second. Get
linked_list.pypassing every test before you write a single line ofbenchmark.py. A benchmark of a buggy list measures nothing. - Draw the pointers. Every insert and delete is a small sequence of pointer assignments, and the order matters. Sketch boxes and arrows on paper before you type — especially for
delete’s three cases and the doubly linkeddelete_node. - Keep the invariants honest. After every operation,
_head,_tail, and_lengthmust all be correct. The tail-delete bug (§3.8) is the classic; write Rep 8’s test early and let it guard you. - Time only the operation, not the setup. Build the structures before
t0 = perf_counter(). If your “front insert” timing includes constructing the array each run, you’re measuring the wrong thing. - Take the median, discard the cold run. The first run warms the caches and the interpreter. Run each measurement at least three times; report the median.
- Be honest about the traversal tie. In CPython the linked list won’t lose traversal by much — the interpreter overhead and int boxing mask the cache effect (§3.6). Report what you measured, not what the textbook predicted. That honesty is the assignment.
What Mastery Looks Like (Beyond the Rubric)
A great Project 3 does not just confirm the textbook — it complicates it, with evidence. The student who masters this project can say, in one breath: “Linked lists are O(1) to insert at the front, and that win is real exactly when the asymptotic gap is a whole factor of n — like front-insert-heavy workloads with no random access — and exactly when you already hold the node you want to splice, which is O(1) on a doubly linked list and O(n) on an array. For everything else — random access, build-then-iterate, membership tests — the array’s contiguity and bulk allocation win the constant-factor war even where the Big-O is tied, so in Python you just use a list. And I can show you the numbers for each.”
That sentence is the chapter. A student who can say it and back every clause with their own measurement has stopped reciting Big-O and started architecting with it.
A great memo makes a call. It does not hedge into “it depends” and stop. It says: given this service’s workload — streaming appends, occasional reads by position, no held-node splicing — the array (a Python list) is the right tool, and here are the three measured rows that prove the linked list would be slower; the team’s intuition was right about the Big-O and wrong about the constant factor. That is what an architect delivers.
Coach’s Note — The first time a student watches the “fast” structure lose the race by a factor of a thousand on random access, something clicks that no lecture can install: Big-O is a tool, not a god. It tells you the shape of the cost as
ngrows. It does not tell you the size of the cost at thenyou actually have. The architect carries both — the asymptotic shape and the measured constant — and the whole rest of this book, every database index and every server choice, is you carrying both at once. This project is where you first feel the weight of it in your own hands.
When You’re Done
- Run your test suite. Confirm every test passes, including the tail-delete-then-append test.
- Run
python3 benchmark.pyfrom a clean checkout. Confirm the results table prints. - Read your
results.xlsxaloud. Could a teammate who never saw your code learn, from it alone, which structure to choose for which workload? If not, rewrite the sentences. - (Hard) Read your
MEMO.docxas if you were the lead developer who chose the linked list. Does it change your mind? If it wouldn’t, it isn’t done. - Commit, push, submit the repo URL.
- Read Chapter 4 — stacks, queues, and deques, where the same data sits on an array or a linked list and the backing choice becomes a real tradeoff you’ll get to make.
A theological footnote. Paul’s image is the body: many members, one body, joined and interdependent — “the eye cannot say to the hand, ‘I have no need of you’” (1 Corinthians 12:21). A linked list is the most literal picture of that joining in all of computing: separate nodes, each holding the next, made one sequence only by the connections between them. And there is a cost to the joining — a pointer per link, a cache miss per step, an allocation per node. The architect names that cost honestly and chooses with eyes open, because pretending the joining is free is how the ministry’s “fast” service got slow. Count the cost of the connection. Then build the body well.
See you next week.