Array or Linked List? Score the Workload, Not the Table
Everyone learns the table. An array indexes in constant time and inserts at the front in linear time; a linked list is the other way round. It is correct, it is memorable, and it does not answer the question anyone actually has, which is which one should I use here.
The reason is that the table compares operations and real code runs a mixture of them. Whichever structure wins is a property of the workload, not of the structure. So rather than compare rows, score a program.
Two workloads, same data, opposite winners
Fire operations one at a time and watch both totals. The highlighted cells are the ones that structure had to touch to do the work.
An array and a singly linked list can hold exactly the same 7 values —
10, 20, 30, 40, 50, 60, 70 — and charge wildly different prices for the same work. The
array can jump straight to any position but has to shift every element to make room at the
front. The list can splice a new item onto the front by moving one pointer, but has to walk
from the head to reach position 5.
Two workloads, counted in element touches. Leaderboard — appending scores and reading positions — costs the array 8 steps and the list 27. Task queue — pushing and popping the front — costs the array 62 steps and the list 8. Neither structure is faster. The workload decides, which is why the textbook table can be memorised and still not answer the question.
Fire operations and watch both totals. Highlighted cells are the ones that structure had to touch. Then try each preset workload — they reach opposite verdicts on the same data.
- Array steps
- 0
- Linked list steps
- 0
- Operations run
- 0
The same state, as values
Contents
Last operation
Cells touched
One step = one element touched: a slot read, a slot written, or one node hop. Try each preset workload, then Reset and try the other.
Press Leaderboard and the array wins comfortably. Press Task queue and the linked list wins by more. Same seven starting values, same two structures, opposite conclusions — and nothing changed except which operations were asked for.
Why the costs move in opposite directions
An array is a single contiguous run of memory, so the address of item k is arithmetic: start plus
k times element size. It never looks at the items in between. That is why reading position 6 costs
one step no matter how long the array is.
The price is that “contiguous” is a promise the array has to keep. Inserting at the front means every existing element moves up one slot to open a gap, so the cost is the length of the array.
A linked list makes the opposite trade. Each node stores its value plus the address of the next node, so nothing is adjacent to anything and there is no arithmetic to exploit — reaching position 6 means starting at the head and following six pointers. But inserting at the front rewrites exactly one pointer, whatever the length.
Neither is a better structure. They are two different answers to what should be cheap.
The same scoring, as a program
This is the cost model the widget uses, written out. The numbers below are the same ones the two preset buttons produce.
def costs(n, op):
"""Element touches for one operation on a list of length n.
Array: has spare capacity, so append is one write. Indexing is address arithmetic.
Linked list: singly linked, with a head AND a tail pointer.
"""
kind, arg = op
if kind == "read":
return 1, arg + 1 # array jumps; list walks from the head
if kind == "append":
return 1, 1 # spare capacity vs a tail pointer
if kind == "prepend":
return n + 1, 1 # shift everything vs move one pointer
if kind == "remove_first":
return max(n - 1, 0), min(n, 1)
raise ValueError(kind)
def apply(n, op):
kind, _ = op
if kind == "append" or kind == "prepend":
return n + 1
if kind == "remove_first":
return max(n - 1, 0)
return n
def run(start_len, ops):
n, array, linked = start_len, 0, 0
for op in ops:
a, l = costs(n, op)
array += a
linked += l
n = apply(n, op)
return array, linked
LEADERBOARD = [("read", 2), ("read", 5), ("append", 90), ("read", 1),
("read", 5), ("append", 95), ("read", 6), ("read", 0)]
TASK_QUEUE = [("prepend", 71), ("remove_first", None), ("prepend", 72), ("prepend", 73),
("remove_first", None), ("remove_first", None), ("prepend", 74), ("remove_first", None)]
print(f"{'workload':<12} {'array':>6} {'list':>6} winner")
for name, ops in [("Leaderboard", LEADERBOARD), ("Task queue", TASK_QUEUE)]:
array, linked = run(7, ops)
winner = "array" if array < linked else "list"
ratio = max(array, linked) / min(array, linked)
print(f"{name:<12} {array:>6} {linked:>6} {winner}, {ratio:.1f}x cheaper")workload array list winner Leaderboard 8 27 array, 3.4x cheaper Task queue 62 8 list, 7.8x cheaper
Change the workloads and run it again. That is the exercise — not learning which structure is faster, but learning to look at what the code does before deciding.
What the model leaves out, and why it matters
The step counts above are honest about work done and silent about time taken, and the gap between those two is larger than beginners are usually told.
Arrays are much faster in practice than their step counts suggest, because a contiguous run of memory is exactly what CPU caches are built for. Walking an array pulls neighbouring elements into cache for free; walking a linked list chases pointers to addresses that may be anywhere, and each hop can be a cache miss costing far more than the “one step” this page charges it.
That is why the practical advice in most languages leans harder toward arrays than the table does.
Python’s list, JavaScript’s Array, Java’s ArrayList, C++‘s vector and Go’s slices are all
array-backed, and a linked list is the specialised choice you reach for deliberately.
What this page simplifies
- Steps, not seconds. Every figure counts element touches, which is deterministic and the same on any machine. It does not model cache behaviour, allocation, or pointer-chasing cost — see above, where those effects run in the array’s favour.
- The array never grows. Append is charged one step because spare capacity is assumed. What happens when it runs out is a real cost with a surprising answer, and it belongs in its own page.
- The list is singly linked with a tail pointer. Without one, appending would walk the whole list. Doubly linked lists pay extra memory per node to make backward moves cheap.
- Insertion in the middle is not scored. Both structures do badly there for different reasons: the array shifts, the list walks. Neither rescues you.