Beginner: foundations · Stacks, queues and linked lists · topic 19 of 61 · 40 min
Linked lists
Explanation
Nodes and pointers
A linked list is a chain of nodes. Each node holds a value and a reference, or pointer, to the next node. The first node is the head. The last node's next is None, which marks the end. Unlike an array, the nodes do not sit side by side in memory; each node only knows where the next one is.
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
head = ListNode(1, ListNode(2, ListNode(3))) # 1 -> 2 -> 3
node = head
while node:
print(node.val, end=" ")
node = node.next
# Output: 1 2 3Singly and doubly linked
In a singly linked list each node points only to the next one, so you can walk forwards only. A doubly linked list also stores prev, a pointer to the node before, so you can walk both ways and unlink a node in O(1) once you hold a reference to it, at the cost of one extra pointer per node. LRU caches (a hash map plus a doubly linked list) and music playlists with "previous" use doubly linked lists. Java's LinkedList is doubly linked, and CPython's deque is a doubly linked list of blocks.
What each operation costs
| Operation | Array (Python list) | Singly linked list |
|---|---|---|
| Read the item at position i | O(1) | O(i): walk from the head |
| Insert or delete at the front | O(n): shift everything | O(1) |
| Insert after a node you already hold | O(n): shift the rest | O(1) |
| Insert at position i | O(n) | O(i) to walk there, then O(1) |
| Append at the end | O(1) amortised | O(n), or O(1) with a tail pointer |
| Search for a value | O(n) | O(n) |
| Extra memory per item | none | one pointer (two if doubly linked) |
There is no random access: you cannot jump to the fifth node, you must walk there. In practice linked lists are also slower to scan than arrays, because their nodes are scattered in memory and the processor's cache helps less.
Insert and delete
Insert at the head: make a new node whose next is the old head; it becomes the new head. Delete the head: head = head.next. To insert at position i, walk to the node just before it, prev, and splice: new.next = prev.next, then prev.next = new. The order of those two steps matters: set prev.next first and the rest of the list is lost.
head = ListNode(2, ListNode(3)) # 2 -> 3
head = ListNode(1, head) # insert at the head: 1 -> 2 -> 3
prev = head.next # the node holding 2
prev.next = ListNode(9, prev.next) # insert after it: 1 -> 2 -> 9 -> 3
head = head.next # delete the head: 2 -> 9 -> 3The dummy node trick
Deleting is awkward when the head itself might be deleted, because then the head changes. A dummy (or sentinel) node placed before the head removes that special case: every real node now has a node before it. Do the work starting from the dummy and return dummy.next at the end.
Reversing a list iteratively
Reversing turns every arrow around. Walk with three pointers: prev (the part already reversed, starting as None), cur (the node being turned) and nxt (saved first so the rest of the list is not lost). O(n) time and O(1) extra space.
def reverse(head):
prev, cur = None, head
while cur:
nxt = cur.next # 1. save the rest
cur.next = prev # 2. turn this arrow around
prev = cur # 3. move prev forward
cur = nxt # 4. move cur forward
return prev # the old tail is the new head| After step | prev (reversed part) | cur (rest) |
|---|---|---|
| start | None | 1 → 2 → 3 |
| 1st loop | 1 | 2 → 3 |
| 2nd loop | 2 → 1 | 3 |
| 3rd loop | 3 → 2 → 1 | None |
Reversing recursively
Recursively: reverse everything after the head, then hook the head on at the end. The recursion goes n calls deep, so it uses O(n) stack space, and CPython stops at about 1,000 nested calls by default with a RecursionError. Prefer the iterative version for long lists; recursion is covered in depth in Module 5.
def reverse_rec(head):
if head is None or head.next is None:
return head
new_head = reverse_rec(head.next)
head.next.next = head # the node after head now points back to it
head.next = None # cut the old forward link
return new_headPython has no built-in singly linked list, so the tasks define ListNode plus two helpers: build(values) turns a Python list into a linked list, and to_list(head) turns it back, which makes results easy to check.
Common mistakes
- Changing cur.next before saving it, which loses the rest of the list. Fix: nxt = cur.next first.
- Forgetting to return the new head after reversing or deleting the first node. Fix: always return the head, and use the returned value.
- Reading node.next.val when node.next is None (AttributeError: 'NoneType' object has no attribute 'val'). Fix: check node and node.next first.
- In recursive reversal, forgetting head.next = None, which leaves a two-node cycle at the end. Fix: cut the old link.
- Special-casing the head in every function. Fix: use a dummy node.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Linked lists · 1/8
This lesson is about linked lists: chains of nodes joined by pointers.
Prefer a video by a teacher? Find free videos on “linked list explained reverse a linked list iterative recursive python” on YouTube ↗
In real life: A music playlist with Next and Previous
A music app's play queue needs Next, Previous, and removing the current song, all instantly, even for a long queue. A doubly linked list fits: each song points to the songs before and after it, so moving or removing is a couple of pointer changes, O(1), with no shifting. Real apps add a hash map from song ID to node so they can find any song in O(1) too.
class Track:
def __init__(self, title):
self.title = title
self.prev = self.next = None
def link(*tracks):
for a, b in zip(tracks, tracks[1:]):
a.next, b.prev = b, a
return tracks[0]
now = link(Track("Morning raga"), Track("Road trip"), Track("Lo-fi study"))
now = now.next # Next
print(now.title)
now = now.prev # Previous
print(now.title)Practice (0/3 done)
Write reverse_list(head) that reverses a singly linked list IN PLACE (relink the existing nodes, do not create new ones) and returns the new head. Do it iteratively with three pointers, O(n) time and O(1) extra space; recursion would fail, because a test uses a list of 5,000 nodes. ListNode, build() and to_list() are given; keep them.
Write remove_elements(head, val) that removes EVERY node whose value equals val and returns the new head (which may be None). Use a dummy node so removing the head needs no special case. O(n) time, O(1) extra space. Example: 1 -> 2 -> 6 -> 3 -> 6 with val 6 becomes 1 -> 2 -> 3.
Write insert_at(head, index, val) that inserts a new node with value val so that it ends up at position index (0-based) and returns the head. index 0 makes it the new head; index equal to the length appends it at the end. If index is negative or greater than the length, return the list unchanged. O(index) time. Example: 1 -> 2 -> 3 with index 1 and val 9 becomes 1 -> 9 -> 2 -> 3.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Fast and slow pointers and merging listsStacks, queues and linked lists · same module
- Queues and dequesStacks, queues and linked lists · same module
- Singly linked listsC Programming (from zero to systems) · Data structures in C
- Mock problems: design-style data structuresInterview strategy and mock problems
- Recursion: functions that call themselvesRecursion and backtracking
- Binary search treesTrees and binary search trees
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
What does it cost to insert a new node at the head of a singly linked list with n nodes?
Answer: O(1)
Point the new node at the old head; nothing else moves.
Why can you not read the 5th node of a linked list in O(1)?
Answer: Nodes are not side by side in memory, so you must follow next pointers from the head
There is no address formula like start + i × size; you walk i steps.
To insert node new after node prev, which order is correct?
Answer: new.next = prev.next, then prev.next = new
Link the new node to the rest first; otherwise prev.next already points at new and the rest is lost.
What is the extra space of reversing a list recursively?
Answer: O(n), for the call stack
Each node adds one nested call, and long lists can hit Python's recursion limit.
What problem does a dummy node solve?
Answer: Deleting or inserting at the head no longer needs special code
With a node before the head, every real node has a predecessor; return dummy.next at the end.
Why does push_front take Node **head?Related
Answer: To change the caller's head pointer
The function must update the caller's pointer.
From Singly linked lists
With while fast and fast.next, which node does slow stop at for 10 → 20 → 30 → 40?Related
Answer: 30
For an even length this loop gives the second middle: slow moves to 20, then 30, while fast moves to 30, then off the end.
What does inserting at index 0 of a list of n items cost?Related
Answer: O(n)
Every existing item must shift one place to the right.
In Python, which call removes the least recently used key from an OrderedDict used as an LRU cache?Related
Answer: popitem(last=False)
Keys are kept from oldest to newest; last=False removes from the oldest end.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 1 more topic in “Stacks, queues and linked lists” before its exam: Fast and slow pointers and merging lists.
- Module 4 exam: Stacks, queues and linked lists · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Recursion and backtracking. 61 topics left in the course before the final exam.