Coding · Data structures
Linked list explained: how it works, code and complexity
Short answer
A linked list is a sequence of nodes where each node stores a value and a reference (pointer) to the next node. Unlike an array, the nodes are not stored next to each other in memory, so inserting or removing a node at a known position takes O(1) time, but finding the k-th element takes O(n) because you must walk from the head. A doubly linked list also stores a pointer to the previous node.
What problem a linked list solves
An array (a Python list) keeps its elements in one contiguous block of memory. Reading a[i] is instant, but inserting at the front shifts every other element one slot to the right, which costs O(n). A linked list avoids that shift: each element lives in its own node, and nodes are connected by references. To insert, you only change a couple of references.
Linked lists are the building block behind many other structures: stacks, queues, hash table buckets (separate chaining), LRU caches and adjacency lists in graphs.
The idea in plain words
Think of a treasure hunt: each clue tells you where the next clue is. You cannot jump to clue number 7; you must follow clues 1 to 6 first. The list itself only remembers the first node, called the head. The last node points to nothing (None).
| Kind | Each node stores | Typical use |
|---|---|---|
| Singly linked | value, next | stacks, simple queues, hash buckets |
| Doubly linked | value, next, prev | deques, LRU caches, text editor buffers |
| Circular | like singly or doubly, but the tail points back to the head | round-robin scheduling, ring buffers |
Worked example: inserting and deleting
Start with the list 3 -> 7 -> 9. We insert 5 after 3, then delete 7.
- Create a new node with value 5.
- Set
new.nextto the node after 3 (the node 7). - Set
node3.nextto the new node. The list is now3 -> 5 -> 7 -> 9. - To delete 7, find the node before it (5) by walking from the head.
- Set
node5.next = node5.next.next, which skips 7. The list is now3 -> 5 -> 9. Python's garbage collector frees the unreachable node.
Python implementation of a singly linked list
This class keeps a reference to the head and a size counter. It supports adding at the front and back, deleting the first matching value, and iteration.
class Node:
def __init__(self, value, next=None):
self.value = value
self.next = next
class LinkedList:
def __init__(self):
self.head = None
self.size = 0
def push_front(self, value):
self.head = Node(value, self.head)
self.size += 1
def push_back(self, value):
node = Node(value)
if self.head is None:
self.head = node
else:
cur = self.head
while cur.next:
cur = cur.next
cur.next = node
self.size += 1
def remove(self, value):
prev, cur = None, self.head
while cur and cur.value != value:
prev, cur = cur, cur.next
if cur is None:
return False # not found
if prev is None:
self.head = cur.next # removing the head
else:
prev.next = cur.next
self.size -= 1
return True
def __iter__(self):
cur = self.head
while cur:
yield cur.value
cur = cur.nextitems = LinkedList()
for x in [3, 7, 9]:
items.push_back(x)
items.push_front(1)
items.remove(7)
print(list(items), items.size) # [1, 3, 9] 3push_back here walks the whole list, so it is O(n). Keeping an extra tail reference makes it O(1); that is what real queue implementations do.
Classic operations: reverse and cycle detection
Reversing a list in place is the most common linked list interview question. Walk the list once and turn each next pointer around.
def reverse(head):
prev = None
cur = head
while cur:
nxt = cur.next # remember the rest
cur.next = prev # turn the pointer around
prev, cur = cur, nxt
return prev # new head
def has_cycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow is fast:
return True
return False
head = Node(1, Node(2, Node(3)))
head = reverse(head)
print(head.value, head.next.value, head.next.next.value) # 3 2 1
print(has_cycle(head)) # Falsehas_cycle is Floyd's tortoise and hare algorithm: a slow pointer moves one step and a fast pointer moves two steps. If there is a loop, the fast pointer eventually meets the slow one; if not, it reaches None. It uses O(1) extra memory.
Time and space complexity
| Operation | Singly linked list | Python list (dynamic array) | Reason |
|---|---|---|---|
| Access k-th element | O(n) | O(1) | a linked list must walk k nodes |
| Insert or delete at the front | O(1) | O(n) | an array shifts every element |
| Insert or delete after a known node | O(1) | O(n) | only references change |
| Append at the end | O(1) with a tail pointer | O(1) amortized | both are cheap |
| Search for a value | O(n) | O(n) | both scan element by element |
| Extra memory | one or two references per node | some spare capacity | nodes are separate objects |
In practice, arrays are often faster even for operations where the linked list has a better Big O, because array elements sit next to each other in memory and use the CPU cache well. Linked list nodes are scattered.
Variants and common mistakes
- Forgetting the empty list: always check
head is Nonebefore readinghead.next. - Losing the rest of the list by overwriting
nextbefore saving it (seenxtinreverse). - Not updating
headwhen the first node is removed, ortailwhen the last node is removed. - Using recursion on very long lists: Python's default recursion limit is about 1000 frames, so iterative code is safer.
- A dummy (sentinel) head node removes most special cases: every real node then has a previous node.
- In a doubly linked list, every insert and delete must update four references; missing one corrupts the list silently.
When to use a linked list, and what to use in Python
Use a linked list when you insert and remove at the ends or in the middle at positions you already hold a reference to, and you rarely need random access. Examples: an LRU cache (doubly linked list plus hash map), the free list of a memory allocator, undo history, or the buckets of a hash table.
In everyday Python code you rarely write your own. For a queue or deque, use collections.deque, which gives O(1) appends and pops at both ends. For general sequences, use list. Write your own linked list for learning, for interviews, and when you need to splice nodes in O(1), as in an LRU cache.
from collections import deque
q = deque([1, 2, 3])
q.appendleft(0)
q.append(4)
print(q.popleft(), q.pop(), list(q)) # 0 4 [1, 2, 3]FAQ
Is a Python list a linked list?
No. A Python list is a dynamic array of references stored contiguously, so indexing is O(1) and inserting at the front is O(n). collections.deque is the closest built-in to a linked list for queue use.
Why is access in a linked list O(n)?
Nodes are not stored at predictable addresses. The only way to reach the k-th node is to start at the head and follow k next references.
When is a doubly linked list better than a singly linked list?
When you need to delete a node given only a reference to it, or move backwards. With a prev pointer, deletion is O(1) without searching for the previous node, at the cost of one extra reference per node.
How do you find the middle of a linked list in one pass?
Use two pointers: move one by one step and the other by two steps. When the fast pointer reaches the end, the slow pointer is at the middle.
What is a sentinel node?
A dummy node placed before the first real node (and sometimes after the last). It holds no data, but it means every real node has a neighbor, so insert and delete code needs fewer special cases for the empty list and the head.