◐ ai-teacher

🧩 Data Structures & Algorithms · Chapter 8

Linked lists

An array keeps its items shoulder to shoulder, so squeezing one in moves everyone. A linked list keeps each item wherever it likes and just remembers who comes next — so inserting is rewiring one arrow.

Study this lesson free in the app →

The basics

A linked list is a chain of nodes, where each node holds a value and a pointer (next) to the following node. The list itself is just a reference to the first node, the head; the last node points to nothing (None).

The problem it solves: in an array, inserting or deleting near the front shifts every later item — O(n). In a linked list, nodes can live anywhere in memory, so once you're at the right spot, inserting or deleting is just changing one or two pointers — O(1). The price: there's no index arithmetic. To reach the 500th node you must follow 499 arrows — O(n).

The mental model: a treasure hunt. Each clue tells you where the next clue is hidden. Adding a new clue between two others means rewriting one clue. Finding clue number 40 means walking through the first 39.

Words you must know 5

Node
value + next pointer (a doubly linked node also has prev).
Head / tail
first and last node. Keeping a tail pointer makes appending O(1).
Traversal
walking from the head following next: O(n).
Dummy (sentinel) node
a fake node before the head so inserting or deleting at the front isn't a special case.
Fast/slow pointers
two walkers at different speeds: find the middle or detect a cycle in O(n) time, O(1) space.

Space is O(n), plus one pointer per node (two for doubly linked).

Explained

Insert one song at the start of a 100,000-song playlist stored as an array: 100,000 songs shift one place.

Store it as a linked list: create one node, point it at the old first song, call it the new head. Two pointer writes.

But ask the linked list for song number 50,000, and it has to walk past 49,999 others to find it. Every data structure is a trade; this is the linked list's.

The treasure hunt

At a birthday party, each clue is hidden somewhere different, and each one says where the next is: "under the blue chair", "behind the clock", "in the left boot".

Adding a clue between "blue chair" and "clock" is easy: hide the new clue, then rewrite the blue-chair clue to point to it, and make the new clue point to the clock. Nothing else moves.

Removing a clue is just as easy: rewrite the clue before it to skip past it.

But if a child asks "where's clue number 9?", nobody knows without starting at clue 1 and following the trail. And if someone accidentally writes "go back to the blue chair" on a later clue, the hunt loops forever — a cycle.

Precisely

head
 │
 ▼
[ 3 | •]──▶[ 8 | •]──▶[ 2 | •]──▶ None

| Operation | Singly linked | Array | | --- | --- | --- | | access i-th item | O(n) | O(1) | | search by value | O(n) | O(n) | | insert/delete at front | O(1) | O(n) | | insert/delete after a node you hold | O(1) | O(n) | | append at end | O(1) with a tail pointer, else O(n) | O(1) amortised | | delete the last node | O(n) — need its predecessor | O(1) |

Doubly linked lists add a prev pointer to every node. Cost: more memory. Benefit: walk both ways, delete any node you hold in O(1) (it knows its own predecessor), and with a tail pointer, O(1) at both ends.

Why "O(1) insert" needs care: it's O(1) once you're at the position. Inserting at position 5,000 still means walking 5,000 nodes first.

Insert, delete, find

insert 5 at 1: walk to the node before position 1, point the new node at 4, point 1 at the new node — 1 → 5 → 4 → 7. No other node moves. delete 4: point 5 past 4, straight at 7. find 7: there's no shortcut — follow arrows from the head, 1 → 5 → 7. Rewiring is O(1); getting there is O(n).

Reverse the chain in place

Reverse walks the list once and flips each arrow to point backwards, so 3 → 8 → 2 → 6 → 9 becomes 9 → 6 → 2 → 8 → 3 without creating a single new node: O(n) time, O(1) space. Then find 3 has to walk all five nodes — 3 used to be the head and is now the tail.

The core operations

class Node:
    def __init__(self, val, next=None):
        self.val = val
        self.next = next

def push_front(head, val):           # O(1)
    return Node(val, head)           # new node becomes the head

def delete(head, target):            # O(n) to find, O(1) to unlink
    dummy = Node(0, head)            # sentinel: the head is no longer special
    prev = dummy
    while prev.next:
        if prev.next.val == target:
            prev.next = prev.next.next   # skip over it
            break
        prev = prev.next
    return dummy.next

def middle(head):                    # fast/slow: O(n) time, O(1) space
    slow = fast = head
    while fast and fast.next:
        slow = slow.next             # 1 step
        fast = fast.next.next        # 2 steps
    return slow                      # even length -> second middle

def has_cycle(head):                 # Floyd's algorithm
    slow = fast = head
    while fast and fast.next:
        slow, fast = slow.next, fast.next.next
        if slow is fast:             # same node, not just same value
            return True
    return False

Where linked lists earn their keep

Honest truth first: for most everyday jobs, an array is faster even where Big-O says otherwise. Array items sit next to each other, so the CPU fetches dozens at once; linked-list nodes are scattered, so each next can be a slow trip to main memory. Each node also carries pointer overhead — in Python a small node object costs tens of bytes beyond the value.

Linked lists win as building blocks where you already hold a pointer to the node:

Gotchas

Words to know

NodeA record holding a value and a pointer to the next node (and the previous one, in a doubly linked list).
Head / tailThe first and last nodes of the list; the tail's next is None.
Doubly linked listA list whose nodes point both forward and backward, allowing O(1) removal of a node you hold.
Dummy (sentinel) nodeA placeholder node before the head that removes special cases when the head changes.
Fast/slow pointersTwo pointers moving at different speeds (usually 2 and 1) to find the middle or detect a cycle in O(1) space.
CycleA node's next pointer leads back to an earlier node, so traversal never reaches None.
Cache localityThe speed advantage of data stored contiguously; arrays have it, scattered linked-list nodes don't.

In two minutes

Test yourself

1. What is the time complexity of reading the k-th element of a singly linked list?

  1. O(1)
  2. O(log k)
  3. O(k) — you must follow k pointers from the head
  4. O(k²)
Show the answer

O(k) — you must follow k pointers from the head — There's no address arithmetic: nodes are scattered, so you walk from the head. Arrays give O(1) here.

2. What does inserting a new node at the head of a singly linked list cost?

  1. O(1)
  2. O(n)
  3. O(log n)
  4. O(n) amortised
Show the answer

O(1) — Create the node, point it at the old head, make it the new head. Two pointer writes, whatever the list's length.

3. You hold a pointer to node p in a singly linked list. How do you delete the node after p, and what does it cost?

  1. Walk from the head to find it — O(n)
  2. `p.next = p.next.next` — O(1)
  3. Shift all later nodes left — O(n)
  4. It can't be done without a doubly linked list
Show the answer

`p.next = p.next.next` — O(1) — Skipping over the next node is one pointer change. Deleting p itself is the hard case in a singly linked list, because you need p's predecessor.

4. What are the time and extra-space costs of reversing a singly linked list iteratively?

  1. O(n²) time, O(1) space
  2. O(n) time, O(n) space
  3. O(n log n) time, O(1) space
  4. O(n) time, O(1) space
Show the answer

O(n) time, O(1) space — One pass with three pointers (prev, cur, nxt), reusing the existing nodes. A recursive version is also O(n) time but uses O(n) call-stack space.

5. How do you detect whether a linked list contains a cycle using O(1) extra space?

  1. Store every visited node in a set
  2. Count nodes until you reach n
  3. Move a slow pointer 1 step and a fast pointer 2 steps; if they meet, there's a cycle
  4. Reverse the list and compare
Show the answer

Move a slow pointer 1 step and a fast pointer 2 steps; if they meet, there's a cycle — Floyd's algorithm: in a cycle, the fast pointer gains one step per iteration and must land on the slow one. A set also works but costs O(n) space; counting needs to know n, which a cyclic list doesn't have.

6. An LRU cache needs O(1) lookup by key and O(1) 'move this entry to most-recent'. Which combination?

  1. Hash map + doubly linked list
  2. Sorted array
  3. Two stacks
  4. Hash map + Python list
Show the answer

Hash map + doubly linked list — The hash map finds the node in O(1); the doubly linked list unlinks it and re-inserts it at the front in O(1). With a Python list, moving an entry to the front is O(n).

There are 9 questions, 4 puzzles and 5 recall cards for this lesson in the app — the recall cards come back on later days, just before you would forget.

Learn it so it stays. Read it, test yourself, then recall it on two different days. The app schedules that for you, free.

Start this lesson →