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)
10 ns100 ns1 µs1001K10K100K1Mn, the input size (log scale)time per operation (log scale)SinglyLinkedList.prepend, n = 100: 194 nsSinglyLinkedList.prepend, n = 1,000: 205 nsSinglyLinkedList.prepend, n = 10,000: 215 nsSinglyLinkedList.prepend, n = 100,000: 233 nsSinglyLinkedList.prepend, n = 1,000,000: 260 nsdeque.appendleft, n = 100: 16.9 nsdeque.appendleft, n = 1,000: 18.1 nsdeque.appendleft, n = 10,000: 18.1 nsdeque.appendleft, n = 100,000: 18.3 nsdeque.appendleft, n = 1,000,000: 25.9 nslist.insert(0, x), n = 1,000: 83.4 nslist.insert(0, x), n = 2,000: 140 nslist.insert(0, x), n = 5,000: 317 nslist.insert(0, x), n = 10,000: 760 nslist.insert(0, x), n = 20,000: 1.63 µslist.insert(0, x), n = 50,000: 4.05 µs

n SinglyLinkedList.prepend deque.appendleft list.insert(0, x)
100 194 ns 16.9 ns
1,000 205 ns 18.1 ns 83.4 ns
2,000 140 ns
5,000 317 ns
10,000 215 ns 18.1 ns 760 ns
20,000 1.63 µs
50,000 4.05 µs
100,000 233 ns 18.3 ns
1,000,000 260 ns 25.9 ns

SinglyLinkedList.prependdeque.appendleftlist.insert(0, x)
1 µs10 µs100 µs1 ms10 ms100 ms1001K10K100K1Mn, the input size (log scale)total time (log scale)SinglyLinkedList.prepend, n = 100: 19.4 µsSinglyLinkedList.prepend, n = 1,000: 205 µsSinglyLinkedList.prepend, n = 10,000: 2.15 msSinglyLinkedList.prepend, n = 100,000: 23.3 msSinglyLinkedList.prepend, n = 1,000,000: 260 msdeque.appendleft, n = 100: 1.69 µsdeque.appendleft, n = 1,000: 18.1 µsdeque.appendleft, n = 10,000: 181 µsdeque.appendleft, n = 100,000: 1.83 msdeque.appendleft, n = 1,000,000: 25.9 mslist.insert(0, x), n = 1,000: 83.4 µslist.insert(0, x), n = 2,000: 280 µslist.insert(0, x), n = 5,000: 1.59 mslist.insert(0, x), n = 10,000: 7.6 mslist.insert(0, x), n = 20,000: 32.7 mslist.insert(0, x), n = 50,000: 202 ms

n SinglyLinkedList.prepend deque.appendleft list.insert(0, x)
100 19.4 µs 1.69 µs
1,000 205 µs 18.1 µs 83.4 µs
2,000 280 µs
5,000 1.59 ms
10,000 2.15 ms 181 µs 7.6 ms
20,000 32.7 ms
50,000 202 ms
100,000 23.3 ms 1.83 ms
1,000,000 260 ms 25.9 ms
slope 1.03 1.04 2.02

Measured on cs-survival-kit 0.8.0 · CPython 3.13.16 · Linux x86_64 · 2026-10-08. 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: 192 nsSinglyLinkedList.pop_front, n = 1,000: 216 nsSinglyLinkedList.pop_front, n = 10,000: 225 nsSinglyLinkedList.pop_front, n = 100,000: 220 nsSinglyLinkedList.pop_front, n = 1,000,000: 222 nsdeque.popleft, n = 100: 13.1 nsdeque.popleft, n = 1,000: 19.6 nsdeque.popleft, n = 10,000: 21.8 nsdeque.popleft, n = 100,000: 22 nsdeque.popleft, n = 1,000,000: 22.9 nslist.pop(0), n = 1,000: 47.3 nslist.pop(0), n = 2,000: 69.1 nslist.pop(0), n = 5,000: 137 nslist.pop(0), n = 10,000: 539 nslist.pop(0), n = 20,000: 1.33 µslist.pop(0), n = 50,000: 3.48 µs

n SinglyLinkedList.pop_front deque.popleft list.pop(0)
100 192 ns 13.1 ns
1,000 216 ns 19.6 ns 47.3 ns
2,000 69.1 ns
5,000 137 ns
10,000 225 ns 21.8 ns 539 ns
20,000 1.33 µs
50,000 3.48 µs
100,000 220 ns 22 ns
1,000,000 222 ns 22.9 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: 19.2 µsSinglyLinkedList.pop_front, n = 1,000: 216 µsSinglyLinkedList.pop_front, n = 10,000: 2.25 msSinglyLinkedList.pop_front, n = 100,000: 22 msSinglyLinkedList.pop_front, n = 1,000,000: 222 msdeque.popleft, n = 100: 1.31 µsdeque.popleft, n = 1,000: 19.6 µsdeque.popleft, n = 10,000: 218 µsdeque.popleft, n = 100,000: 2.2 msdeque.popleft, n = 1,000,000: 22.9 mslist.pop(0), n = 1,000: 47.3 µslist.pop(0), n = 2,000: 138 µslist.pop(0), n = 5,000: 684 µslist.pop(0), n = 10,000: 5.39 mslist.pop(0), n = 20,000: 26.6 mslist.pop(0), n = 50,000: 174 ms

n SinglyLinkedList.pop_front deque.popleft list.pop(0)
100 19.2 µs 1.31 µs
1,000 216 µs 19.6 µs 47.3 µs
2,000 138 µs
5,000 684 µs
10,000 2.25 ms 218 µs 5.39 ms
20,000 26.6 ms
50,000 174 ms
100,000 22 ms 2.2 ms
1,000,000 222 ms 22.9 ms
slope 1.01 1.05 2.17

Measured on cs-survival-kit 0.8.0 · CPython 3.13.16 · Linux x86_64 · 2026-10-08. 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: 77.9 µsSinglyLinkedList, n = 1,000: 766 µsSinglyLinkedList, n = 10,000: 8.2 msSinglyLinkedList, n = 100,000: 85.2 msDynamicArray, n = 100: 9.47 µsDynamicArray, n = 1,000: 9.84 µsDynamicArray, n = 10,000: 10 µsDynamicArray, n = 100,000: 10.1 µslist, n = 100: 854 nslist, n = 1,000: 992 nslist, n = 10,000: 1.04 µslist, n = 100,000: 1.1 µs
n SinglyLinkedList DynamicArray list
100 77.9 µs 9.47 µs 854 ns
1,000 766 µs 9.84 µs 992 ns
10,000 8.2 ms 10 µs 1.04 µs
100,000 85.2 ms 10.1 µs 1.1 µs
slope 1.01 0.01 0.04

Measured on cs-survival-kit 0.8.0 · CPython 3.13.16 · Linux x86_64 · 2026-10-08. 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: 252 nsSinglyLinkedList, n = 1,000: 264 nsSinglyLinkedList, n = 10,000: 274 nsSinglyLinkedList, n = 100,000: 310 nsSinglyLinkedList, n = 1,000,000: 327 nsDynamicArray, n = 100: 219 nsDynamicArray, n = 1,000: 219 nsDynamicArray, n = 10,000: 240 nsDynamicArray, n = 100,000: 232 nsDynamicArray, n = 1,000,000: 247 nslist, n = 100: 14.4 nslist, n = 1,000: 14 nslist, n = 10,000: 14.8 nslist, n = 100,000: 14.5 nslist, n = 1,000,000: 22 ns

n SinglyLinkedList DynamicArray list
100 252 ns 219 ns 14.4 ns
1,000 264 ns 219 ns 14 ns
10,000 274 ns 240 ns 14.8 ns
100,000 310 ns 232 ns 14.5 ns
1,000,000 327 ns 247 ns 22 ns

SinglyLinkedListDynamicArraylist
1 µs10 µs100 µs1 ms10 ms100 ms1001K10K100K1Mn, the input size (log scale)total time (log scale)SinglyLinkedList, n = 100: 25.2 µsSinglyLinkedList, n = 1,000: 264 µsSinglyLinkedList, n = 10,000: 2.74 msSinglyLinkedList, n = 100,000: 31 msSinglyLinkedList, n = 1,000,000: 327 msDynamicArray, n = 100: 21.9 µsDynamicArray, n = 1,000: 219 µsDynamicArray, n = 10,000: 2.4 msDynamicArray, n = 100,000: 23.2 msDynamicArray, n = 1,000,000: 247 mslist, n = 100: 1.44 µslist, n = 1,000: 14 µslist, n = 10,000: 148 µslist, n = 100,000: 1.45 mslist, n = 1,000,000: 22 ms

n SinglyLinkedList DynamicArray list
100 25.2 µs 21.9 µs 1.44 µs
1,000 264 µs 219 µs 14 µs
10,000 2.74 ms 2.4 ms 148 µs
100,000 31 ms 23.2 ms 1.45 ms
1,000,000 327 ms 247 ms 22 ms
slope 1.03 1.01 1.04

Measured on cs-survival-kit 0.8.0 · CPython 3.13.16 · Linux x86_64 · 2026-10-08. 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)