Skip to content

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 len does 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)
100 ns1 µs10 µs1001K10K100K1Mn, the input size (log scale)time per operation (log scale)SinglyLinkedList.prepend, n = 100: 222 nsSinglyLinkedList.prepend, n = 1,000: 233 nsSinglyLinkedList.prepend, n = 10,000: 240 nsSinglyLinkedList.prepend, n = 100,000: 267 nsSinglyLinkedList.prepend, n = 1,000,000: 291 nsdeque.appendleft, n = 100: 26.9 nsdeque.appendleft, n = 1,000: 27.2 nsdeque.appendleft, n = 10,000: 28.5 nsdeque.appendleft, n = 100,000: 28.5 nsdeque.appendleft, n = 1,000,000: 40.6 nslist.insert(0, x), n = 1,000: 134 nslist.insert(0, x), n = 2,000: 236 nslist.insert(0, x), n = 5,000: 548 nslist.insert(0, x), n = 10,000: 1.09 µslist.insert(0, x), n = 20,000: 2.17 µslist.insert(0, x), n = 50,000: 5.41 µs

n SinglyLinkedList.prepend deque.appendleft list.insert(0, x)
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)
10 µs100 µs1 ms10 ms100 ms1001K10K100K1Mn, the input size (log scale)total time (log scale)SinglyLinkedList.prepend, n = 100: 22.2 µsSinglyLinkedList.prepend, n = 1,000: 233 µsSinglyLinkedList.prepend, n = 10,000: 2.4 msSinglyLinkedList.prepend, n = 100,000: 26.7 msSinglyLinkedList.prepend, n = 1,000,000: 291 msdeque.appendleft, n = 100: 2.69 µsdeque.appendleft, n = 1,000: 27.2 µsdeque.appendleft, n = 10,000: 285 µsdeque.appendleft, n = 100,000: 2.85 msdeque.appendleft, n = 1,000,000: 40.6 mslist.insert(0, x), n = 1,000: 134 µslist.insert(0, x), n = 2,000: 471 µslist.insert(0, x), n = 5,000: 2.74 mslist.insert(0, x), n = 10,000: 10.9 mslist.insert(0, x), n = 20,000: 43.3 mslist.insert(0, x), n = 50,000: 271 ms

n SinglyLinkedList.prepend deque.appendleft list.insert(0, x)
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)
10 ns100 ns1 µs1001K10K100K1Mn, the input size (log scale)time per operation (log scale)SinglyLinkedList.pop_front, n = 100: 157 nsSinglyLinkedList.pop_front, n = 1,000: 173 nsSinglyLinkedList.pop_front, n = 10,000: 179 nsSinglyLinkedList.pop_front, n = 100,000: 186 nsSinglyLinkedList.pop_front, n = 1,000,000: 184 nsdeque.popleft, n = 100: 17.3 nsdeque.popleft, n = 1,000: 30.3 nsdeque.popleft, n = 10,000: 35.2 nsdeque.popleft, n = 100,000: 35.4 nsdeque.popleft, n = 1,000,000: 37.2 nslist.pop(0), n = 1,000: 106 nslist.pop(0), n = 2,000: 163 nslist.pop(0), n = 5,000: 352 nslist.pop(0), n = 10,000: 697 nslist.pop(0), n = 20,000: 1.35 µslist.pop(0), n = 50,000: 3.34 µs

n SinglyLinkedList.pop_front deque.popleft list.pop(0)
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)
1 µs10 µs100 µs1 ms10 ms100 ms1001K10K100K1Mn, the input size (log scale)total time (log scale)SinglyLinkedList.pop_front, n = 100: 15.7 µsSinglyLinkedList.pop_front, n = 1,000: 173 µsSinglyLinkedList.pop_front, n = 10,000: 1.79 msSinglyLinkedList.pop_front, n = 100,000: 18.6 msSinglyLinkedList.pop_front, n = 1,000,000: 184 msdeque.popleft, n = 100: 1.73 µsdeque.popleft, n = 1,000: 30.3 µsdeque.popleft, n = 10,000: 352 µsdeque.popleft, n = 100,000: 3.54 msdeque.popleft, n = 1,000,000: 37.2 mslist.pop(0), n = 1,000: 106 µslist.pop(0), n = 2,000: 326 µslist.pop(0), n = 5,000: 1.76 mslist.pop(0), n = 10,000: 6.97 mslist.pop(0), n = 20,000: 27 mslist.pop(0), n = 50,000: 167 ms

n SinglyLinkedList.pop_front deque.popleft list.pop(0)
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
1 µs10 µs100 µs1 ms10 ms100 ms1001K10K100Kn, the input size (log scale)total time (log scale)SinglyLinkedList, n = 100: 131 µsSinglyLinkedList, n = 1,000: 1.34 msSinglyLinkedList, n = 10,000: 14.4 msSinglyLinkedList, n = 100,000: 145 msDynamicArray, n = 100: 16.4 µsDynamicArray, n = 1,000: 17.6 µsDynamicArray, n = 10,000: 17.8 µsDynamicArray, n = 100,000: 18 µslist, n = 100: 1.49 µslist, n = 1,000: 1.65 µslist, n = 10,000: 1.68 µslist, n = 100,000: 1.69 µs
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
10 ns20 ns50 ns100 ns200 ns500 ns1001K10K100K1Mn, the input size (log scale)time per operation (log scale)SinglyLinkedList, n = 100: 214 nsSinglyLinkedList, n = 1,000: 221 nsSinglyLinkedList, n = 10,000: 227 nsSinglyLinkedList, n = 100,000: 261 nsSinglyLinkedList, n = 1,000,000: 276 nsDynamicArray, n = 100: 162 nsDynamicArray, n = 1,000: 151 nsDynamicArray, n = 10,000: 184 nsDynamicArray, n = 100,000: 169 nsDynamicArray, n = 1,000,000: 168 nslist, n = 100: 22.1 nslist, n = 1,000: 19.5 nslist, n = 10,000: 20 nslist, n = 100,000: 19 nslist, n = 1,000,000: 29.1 ns

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
10 µs100 µs1 ms10 ms100 ms1001K10K100K1Mn, the input size (log scale)total time (log scale)SinglyLinkedList, n = 100: 21.4 µsSinglyLinkedList, n = 1,000: 221 µsSinglyLinkedList, n = 10,000: 2.27 msSinglyLinkedList, n = 100,000: 26.1 msSinglyLinkedList, n = 1,000,000: 276 msDynamicArray, n = 100: 16.2 µsDynamicArray, n = 1,000: 151 µsDynamicArray, n = 10,000: 1.84 msDynamicArray, n = 100,000: 16.9 msDynamicArray, n = 1,000,000: 168 mslist, n = 100: 2.21 µslist, n = 1,000: 19.5 µslist, n = 10,000: 200 µslist, n = 100,000: 1.9 mslist, n = 1,000,000: 29.1 ms

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)