Singly Linked List¶
A linked list stores a sequence as a chain of nodes. Each node holds one element and a link to the node after it. Nothing is stored side by side: the nodes can sit anywhere in memory, and the links are what put them in order.
That is the opposite of a dynamic array, which keeps its elements in one contiguous block. The two are the classic implementations of the same idea, a list, and almost every difference between them follows from that one choice of layout.
The guide's implementation is
SinglyLinkedList.
Like DynamicArray, it implements
AbstractList,
the interface the two share.
How it works¶
graph LR
a["1"] --> b["2"] --> c["3"] --> none(["None"])
head([head]) -.-> a
tail([tail]) -...-> c
The list itself keeps three things:
head, a reference to the first node. Every walk through the list starts here.tail, a reference to the last node, so that adding at the end does not require walking the whole chain to find it.- a size count, so that
lendoes not require walking the chain either.
"Singly" linked means each node links only forward. Given a node you can reach everything after it and nothing before it. That one restriction decides which operations are cheap.
Working at the front¶
Adding at the front creates a node, points it at the current head, and makes it the new head. Removing from the front moves the head to the second node. Neither touches any other node, so both are O(1) no matter how long the list is.
An array cannot do this. Its first element lives in slot 0, so putting something in front of it means shifting every element one slot to the right, and removing it means shifting them all back. Both are O(n).
| Operation | In the library |
|---|---|
| Add at the front | prepend |
| Remove from the front | pop_front |
Working at the end¶
With a tail reference,
append
is O(1) too: link the new node after the tail, then move the tail to it.
Removing from the end is a different story. To unlink the last node you have to update the node before it, and there is no link pointing backward to find that node. The only way to reach it is to walk from the head, which is O(n). A singly linked list therefore has no cheap way to remove its last element. A doubly linked list, where each node also links backward, exists to fix exactly this.
Finding an element¶
An array can compute where element i lives: its elements are evenly spaced
in one block, so the position is a multiplication away. A linked list cannot.
A node's location says nothing about where the next one is, so the only way
to reach element i is to start at the head and follow i links.
Reading by position, searching for a value, and removing a value all need that walk, so all three are O(n).
remove
has one extra wrinkle. Unlinking a node means pointing the previous node past
it, so the walk has to carry a reference to the previous node as it goes.
Reversing¶
reverse
is the classic linked list exercise. It walks the list once and turns each
link around to point at the node before it. Because overwriting a link loses
the way forward, the walk keeps three references: the previous node, the
current node, and the next node, saved just before its link is changed. At
the end the old tail is the new head. No nodes are created, so it takes O(n)
time and O(1) extra space.
Linked list or dynamic array?¶
| Singly linked list | Dynamic array | |
|---|---|---|
| Add at the front | O(1) | O(n) |
| Remove from the front | O(1) | O(n) |
| Add at the end | O(1) | O(1) amortized |
| Remove from the end | O(n) | O(1) |
| Read by position | O(n) | O(1) |
| Search for a value | O(n) | O(n) |
| Memory | a node and a link per element | one block, with some unused slots |
Neither is better. A linked list wins when the work is at the front of the sequence, which is why queues are often built on one. An array wins whenever elements are read by position, and that is most of the time, which is why the array is the default list in nearly every language.
Measured¶
The benchmarks below time the library's SinglyLinkedList against its
DynamicArray, Python's built-in list (a dynamic array written in C), and
collections.deque, the standard library's structure for fast work at both
ends. All chart axes are logarithmic.
Adding at the front¶
Each run adds n elements to the front of an empty container. The first tab divides by n to give the cost of one insertion.
SinglyLinkedList.prependdeque.appendleftlist.insert(0, x)
| n | SinglyLinkedList.prepend |
deque.appendleft |
list.insert( |
|---|---|---|---|
| 100 | 222 ns | 26.9 ns | |
| 1,000 | 233 ns | 27.2 ns | 134 ns |
| 2,000 | 236 ns | ||
| 5,000 | 548 ns | ||
| 10,000 | 240 ns | 28.5 ns | 1.09 µs |
| 20,000 | 2.17 µs | ||
| 50,000 | 5.41 µs | ||
| 100,000 | 267 ns | 28.5 ns | |
| 1,000,000 | 291 ns | 40.6 ns |
SinglyLinkedList.prependdeque.appendleftlist.insert(0, x)
| n | SinglyLinkedList.prepend |
deque.appendleft |
list.insert( |
|---|---|---|---|
| 100 | 22.2 µs | 2.69 µs | |
| 1,000 | 233 µs | 27.2 µs | 134 µs |
| 2,000 | 471 µs | ||
| 5,000 | 2.74 ms | ||
| 10,000 | 2.4 ms | 285 µs | 10.9 ms |
| 20,000 | 43.3 ms | ||
| 50,000 | 271 ms | ||
| 100,000 | 26.7 ms | 2.85 ms | |
| 1,000,000 | 291 ms | 40.6 ms | |
| slope | 1.03 | 1.04 | 1.95 |
Measured on cs-survival-kit 0.6.0 · CPython 3.13.16 · Linux x86_64 · 2026-10-05. Each time is the best of five runs. The slope is fitted on a log-log scale: about 1 is linear, about 2 is quadratic. Both chart axes are logarithmic, so a power law is a straight line and its steepness is that slope.
The linked list and the deque are flat: an insertion costs the same whether the list holds a hundred elements or a million. The built-in list climbs steadily, because every insertion at index 0 shifts everything already there. On the total-time tab that is the difference between a line of slope 1 and a line of slope 2.
At the smallest sizes the built-in list still beats the linked list, because shifting a short run of slots in C is cheaper than creating one node in Python. Complexity describes how cost grows, not who wins at small sizes.
Removing from the front¶
Each run empties a container of n elements from the front.
SinglyLinkedList.pop_frontdeque.popleftlist.pop(0)
| n | SinglyLinkedList.pop_front |
deque.popleft |
list.pop( |
|---|---|---|---|
| 100 | 157 ns | 17.3 ns | |
| 1,000 | 173 ns | 30.3 ns | 106 ns |
| 2,000 | 163 ns | ||
| 5,000 | 352 ns | ||
| 10,000 | 179 ns | 35.2 ns | 697 ns |
| 20,000 | 1.35 µs | ||
| 50,000 | 3.34 µs | ||
| 100,000 | 186 ns | 35.4 ns | |
| 1,000,000 | 184 ns | 37.2 ns |
SinglyLinkedList.pop_frontdeque.popleftlist.pop(0)
| n | SinglyLinkedList.pop_front |
deque.popleft |
list.pop( |
|---|---|---|---|
| 100 | 15.7 µs | 1.73 µs | |
| 1,000 | 173 µs | 30.3 µs | 106 µs |
| 2,000 | 326 µs | ||
| 5,000 | 1.76 ms | ||
| 10,000 | 1.79 ms | 352 µs | 6.97 ms |
| 20,000 | 27 ms | ||
| 50,000 | 167 ms | ||
| 100,000 | 18.6 ms | 3.54 ms | |
| 1,000,000 | 184 ms | 37.2 ms | |
| slope | 1.02 | 1.07 | 1.89 |
Measured on cs-survival-kit 0.6.0 · CPython 3.13.16 · Linux x86_64 · 2026-10-05. Each time is the best of five runs. The slope is fitted on a log-log scale: about 1 is linear, about 2 is quadratic. Both chart axes are logarithmic, so a power law is a straight line and its steepness is that slope.
The same picture, for the same reason: removing the first element of an array shifts every remaining element one slot to the left.
Reading by position¶
Here n is the size of the container, and every run performs the same 100 reads at positions spread evenly across it. The time is not divided by n: a flat line means a read costs the same in a container of any size.
SinglyLinkedListDynamicArraylist| n | SinglyLinkedList |
DynamicArray |
list |
|---|---|---|---|
| 100 | 131 µs | 16.4 µs | 1.49 µs |
| 1,000 | 1.34 ms | 17.6 µs | 1.65 µs |
| 10,000 | 14.4 ms | 17.8 µs | 1.68 µs |
| 100,000 | 145 ms | 18 µs | 1.69 µs |
| slope | 1.02 | 0.01 | 0.02 |
Measured on cs-survival-kit 0.6.0 · CPython 3.13.16 · Linux x86_64 · 2026-10-05. Each time is the best of five runs. The slope is fitted on a log-log scale: about 1 is linear, about 2 is quadratic. Both chart axes are logarithmic, so a power law is a straight line and its steepness is that slope.
This is the linked list's weak spot and the array's strength. Both arrays are flat. The linked list's time grows in step with the size of the list, since each read walks from the head to the position it wants.
Adding at the end¶
Each run appends n elements to an empty container.
SinglyLinkedListDynamicArraylist
| n | SinglyLinkedList |
DynamicArray |
list |
|---|---|---|---|
| 100 | 214 ns | 162 ns | 22.1 ns |
| 1,000 | 221 ns | 151 ns | 19.5 ns |
| 10,000 | 227 ns | 184 ns | 20 ns |
| 100,000 | 261 ns | 169 ns | 19 ns |
| 1,000,000 | 276 ns | 168 ns | 29.1 ns |
SinglyLinkedListDynamicArraylist
| n | SinglyLinkedList |
DynamicArray |
list |
|---|---|---|---|
| 100 | 21.4 µs | 16.2 µs | 2.21 µs |
| 1,000 | 221 µs | 151 µs | 19.5 µs |
| 10,000 | 2.27 ms | 1.84 ms | 200 µs |
| 100,000 | 26.1 ms | 16.9 ms | 1.9 ms |
| 1,000,000 | 276 ms | 168 ms | 29.1 ms |
| slope | 1.03 | 1.01 | 1.02 |
Measured on cs-survival-kit 0.6.0 · CPython 3.13.16 · Linux x86_64 · 2026-10-05. Each time is the best of five runs. The slope is fitted on a log-log scale: about 1 is linear, about 2 is quadratic. Both chart axes are logarithmic, so a power law is a straight line and its steepness is that slope.
All three are flat, as the complexity table predicts: appending is O(1) for each of them. They differ only by a constant. The linked list pays for creating a node on every append, while the dynamic array usually just writes into a slot it already has.
Complexity¶
| Operation | Time | Notes |
|---|---|---|
| Add or remove at the front | O(1) | |
| Add at the end | O(1) | needs the tail reference |
| Read by position | O(n) | walks from the head |
| Search for a value | O(n) | |
| Remove a value | O(n) | finding it is the cost; unlinking is O(1) |
| Reverse | O(n) | in place, O(1) extra space |
| Length | O(1) | from the stored count |
| Iterate | O(n) |