Ch 5 / 30 Linked Lists 0/0 exercises Exercises ↓

AmouAI Hub/Courses/Data Structures & Algorithms/Chapter 5

Part 2 · Linear Structures · Chapter 5

Linked Lists

A treasure hunt where each clue tells you where the next clue is. You can splice a new clue into the middle for free — but you can never skip ahead.

Reading
CLRS Ch. 10.2
Focus
Pointers, not positions
Cost
splice O(1) · index O(n)
Structures
singly · doubly · circular

By the end of this chapter you can

  1. Build a linked list from nodes and traverse it without an index
  2. Perform insertion and deletion by pointer surgery, and count the writes involved
  3. Explain precisely why indexing is O(n) here and O(1) in an array
  4. Reverse a list in place with three pointers, and say why the order of the three lines matters
  5. Detect a cycle in O(1) space with Floyd's two runners, and justify why they must meet
  6. Decide honestly when a linked list is the right choice — which is rarer than textbooks imply

1Giving up the street

Chapter 3 got O(1) indexing from contiguity. This chapter gives contiguity away, and gets something else in return.

The treasure hunt

A treasure hunt: each clue tells you where to find the next clue. The clues can be anywhere — under a bench, behind a painting, in another building. Their physical positions are irrelevant, because each one carries the address of its successor.

Adding a new clue to the middle of the hunt is trivial: change one clue to point at your new one, and make your new one point where the old one used to. Nothing else moves.

But there is no way to jump to “clue number seven.” You must follow six clues to get there. Always.

That is a linked list, exactly. A node holds a value and a reference to the next node. There is no block of contiguous memory, no uniform slot size, no address arithmetic — and therefore no O(1) indexing.

node.py
class Node:
    def __init__(self, value, nxt=None):
        self.value = value
        self.next = nxt          # a reference to another Node, or None

# Build 7 -> 3 -> 9 -> None, back to front
c = Node(9)
b = Node(3, c)
a = Node(7, b)
head = a

# Traverse. Note: no index anywhere.
cur = head
while cur is not None:
    print(cur.value)
    cur = cur.next
The trade, stated exactly

An array gets O(1) indexing from contiguity, and pays O(n) for insertion because everything must shift.

A linked list gets O(1) insertion from indirection, and pays O(n) for indexing because it must walk.

Same two operations, opposite bills. Neither is better — they are bets on different access patterns.

2Three flavours

Singly, doubly and circular. Each adds a pointer and buys a specific capability.

FlavourEach node holdsBuys youCosts you
Singly linkedvalue, nextThe simplest possible nodeYou can only move forward; deletion needs the previous node
Doubly linkedvalue, next, prevBackwards traversal; delete a node given only that nodeOne extra pointer per node, and two pointers to fix on every edit
Circularvalue, next (last → first)Endless rotation; no special case for the tailNo natural stopping point — loops need a different termination test
flavours.py
class DNode:
    """A doubly linked node. The extra pointer is what makes the LRU cache
       in Chapter 29 possible."""
    def __init__(self, value):
        self.value = value
        self.next = None
        self.prev = None


def delete_singly(head, target):
    """Singly linked: we must track prev ourselves, because a node
       cannot see backwards."""
    if head is None:
        return None
    if head.value == target:
        return head.next
    prev, cur = head, head.next
    while cur is not None:
        if cur.value == target:
            prev.next = cur.next      # route around it
            return head
        prev, cur = cur, cur.next
    return head


def delete_doubly(node):
    """Doubly linked: given only the node itself, O(1). No search, no head."""
    if node.prev:
        node.prev.next = node.next
    if node.next:
        node.next.prev = node.prev
    # Two writes. This is the operation an LRU cache needs a thousand
    # times a second, and it is why the extra pointer earns its keep.
The distinction that matters in interviews

“Deletion from a linked list is O(1)” is only true given a pointer to the right node. Deleting by value is O(n), because finding it is O(n).

The O(1) claim is real — it is precisely why an LRU cache pairs a hash map (which supplies the pointer instantly) with a doubly linked list (which does the O(1) surgery). One without the other is useless.

3Reversing in place

The classic linked-list exercise. Three pointers, six lines, and one ordering constraint that everyone gets wrong once.

To reverse the list we walk it once, flipping each arrow as we pass. The problem: the moment you change cur.next, you have destroyed the only reference to the rest of the list. So you must save it first.

reverse.py
def reverse(head):
    prev = None
    cur = head
    while cur is not None:
        nxt = cur.next        # 1. SAVE the rest of the list  ← do this first
        cur.next = prev       # 2. flip this node's arrow backwards
        prev = cur            # 3. prev moves forward
        cur = nxt             # 4. cur moves forward
    return prev               # prev is the new head

# Swap lines 1 and 2 and the list is destroyed: cur.next now points at
# prev, so `nxt = cur.next` walks backwards and you loop forever over
# two nodes. Try it in the playground — it is a good failure to see once.

The recursive version

reverse_recursive.py
def reverse_rec(head):
    if head is None or head.next is None:
        return head                  # base case: 0 or 1 nodes
    new_head = reverse_rec(head.next)  # reverse everything after me
    head.next.next = head              # my successor now points back at me
    head.next = None                   # I become the new tail
    return new_head

# Elegant, and O(n) SPACE — one stack frame per node. On a million-node
# list this raises RecursionError. The iterative version is O(1) space.
# When both exist and one uses less memory, prefer that one.
Say the order out loud
Save, flip, advance, advance. Four steps in that order. Every reversal bug is a violation of that sequence, and saying it aloud while you write the loop genuinely helps.

4Floyd's two runners

A list with a loop never ends. Detecting that in constant space is one of the prettiest small results in the subject.

If some node's next points back to an earlier node, traversal never terminates. Detecting it with a set of visited nodes is easy and costs O(n) memory. Floyd's algorithm does it in O(1).

The running track

Two runners start together on a circular track, one twice as fast as the other. The fast one must eventually lap the slow one — the gap between them closes by exactly one position per step, so it cannot be avoided.

On a straight track, the fast runner simply reaches the end and stops. They never meet again.

So: if the two runners meet, the track is a loop. If the fast one runs off the end, it is not.

floyd.py
def has_cycle(head):
    slow = fast = head
    while fast is not None and fast.next is not None:
        slow = slow.next            # one step
        fast = fast.next.next       # two steps
        if slow is fast:            # `is`, not `==` — identity, not value
            return True
    return False                    # fast ran off the end: no cycle

# O(n) time, O(1) space.
#
# Why it terminates: once both runners are inside the loop, the fast one
# gains exactly one position per step on the slow one. A gap that shrinks
# by one every step and lives on a finite circle must reach zero.

Finding where the loop starts

A second, less obvious step. Once the runners meet, reset one to the head and advance both one step at a time. They meet again exactly at the entrance to the loop.

cycle_start.py
def cycle_start(head):
    slow = fast = head
    while fast and fast.next:
        slow, fast = slow.next, fast.next.next
        if slow is fast:
            slow = head                 # reset one runner
            while slow is not fast:     # now BOTH move one step
                slow, fast = slow.next, fast.next
            return slow                 # the loop entrance
    return None

# The proof is a short piece of algebra: if the tail before the loop has
# length T and the meeting point is M steps into a loop of length L, then
# the distance from the meeting point back round to the entrance is exactly
# T. So two walkers, one from the head and one from the meeting point,
# moving at the same speed, arrive together.
The transferable idea

Two pointers moving at different speeds is a general technique, not a party trick. It also gives you:

  • The middle node in one pass — when fast reaches the end, slow is halfway.
  • The n-th from last node — start fast n ahead, then move both together.
  • Whether a list is a palindrome — find the middle, reverse the second half, compare.

All in one pass and O(1) space. All in the exercises.

5When to actually use one

Textbooks love linked lists. Production code uses them far less than you would expect, and it is worth understanding why.

Index i
O(n)
Insert at head
O(1)
Insert after node
O(1)
Delete given node*
O(1)
Search by value
O(n)
Space overhead
+1 pointer/node

* doubly linked. Singly linked needs the previous node, so it is O(n) unless you already have it.

The case against

Three real objections, in order of how much they matter in practice:

  1. Cache locality. Array elements sit together, so one memory fetch loads several of them into the CPU cache. Linked-list nodes are scattered, so each hop is potentially a cache miss — and a cache miss costs roughly a hundred times a cache hit. In real benchmarks an array scan routinely beats a linked-list scan by an order of magnitude, even when the Big-O favours the list.
  2. Memory overhead. Every node carries at least one extra pointer. In Python, every node is also a full object with its own header — typically 50+ bytes to store one integer.
  3. You usually do not have the pointer. The O(1) insert assumes you already hold a reference to the right node. If you have to search for it, you have paid O(n) and the advantage is gone.

The case for

Linked lists are the right answer when you genuinely hold pointers to the nodes you edit, and you edit constantly:

  • LRU caches — a hash map supplies the pointer, the list supplies the order. Chapter 29.
  • Deques and queuescollections.deque is a doubly linked list of array blocks, getting cache locality and O(1) at both ends.
  • Adjacency lists in graphs, and chaining in hash tables — many short lists, always appended to at the head.
  • Persistent / immutable structures — a functional list can share its entire tail with the version before it. No array can do that.
In Python specifically

You will rarely write a linked list in production Python. list covers the array case and collections.deque covers the queue case, both implemented in C.

Learn linked lists anyway — not because you will write one, but because trees and graphs are linked structures. Every intuition you build here about following references, saving before you overwrite, and thinking in pointers transfers directly to Chapters 14 through 23.

What to carry forward

  • A linked list trades contiguity for indirection: O(1) splicing, O(n) indexing. Exactly the opposite bill from an array.
  • The O(1) insert and delete are only O(1) once you hold a pointer to the right node. Finding it is O(n).
  • Reversal is save, flip, advance, advance — in that order. Overwrite before saving and you lose the rest of the list.
  • Floyd's two runners detect a cycle in O(1) space, because a faster runner on a circular track must lap a slower one.
  • Different-speed pointers also give you the middle node, the n-th from last, and palindrome checking in one pass.
  • In practice, cache locality usually beats the better Big-O. Learn linked lists because trees and graphs are linked structures.

>_Playground

A complete singly linked list to poke at. Try breaking the reversal by swapping two lines — that failure teaches more than the working version.

scratch.pypython not loaded
Real Python, running in your browser. Nothing is installed or uploaded.
Output appears here. The first run takes a few seconds while Python loads.

Exercises

Checked automatically the moment you submit. Work top to bottom — each one assumes the last. Your answers are saved in this browser.

All Warm-up Core Challenge Reset chapter

Chapter 6 — Stacks

A pile of plates: last on, first off. That single restriction is what matches brackets, powers undo, and explains how functions call each other.

Continue →