λDSA Learning Hubpart of DSA Atlas

Linked Lists

Beginner~3h · 8 lessons12 practice problems

Nodes and pointers instead of contiguous memory: O(1) splicing, reversal, fast/slow pointers, cycle detection, and the LRU-cache design pattern.

0 of 8 lessons checked off

Introduction

What it is

  • A linked list is a chain of nodes, each holding a value and a pointer to the next node. The list is reachable only through its head; memory is scattered, not contiguous.
  • Variants: singly linked (next only), doubly linked (next and prev), and circular (the tail points back into the list).

Why it matters

  • Linked lists invert the array trade-off: O(1) insertion/deletion at a known node, but O(n) access by index. When a structure must grow and splice constantly — queues, LRU caches, adjacency lists — pointers beat shifting.
  • For interviews, linked lists are the purest test of pointer discipline: one wrong assignment loses half the list, and interviewers watch for exactly that.

How it works

  • Every operation reduces to rewiring next pointers in the correct order: save what you're about to overwrite, then overwrite.
  • A dummy (sentinel) head node removes 'is this the first node?' special cases from insertions and deletions.
  • Two pointers moving at different speeds (fast/slow) answer questions about middles, cycles, and kth-from-end in one pass.

Where it's used

  • OS schedulers and timer wheels keep tasks in linked lists; music-player queues and undo chains are conceptual linked lists; memory allocators track free blocks with them.
  • LRU caches — asked constantly at Meta/Amazon — combine a hash map with a doubly linked list for O(1) get/put.

In interviews

  • Reverse a list (whole or in ranges), detect and locate a cycle, merge two sorted lists, remove kth from end, reorder list, copy a list with random pointers.
Analogy: A linked list is a treasure hunt: each clue holds a value and the location of the next clue. You can splice in a new clue instantly if you're standing at the right spot — but finding clue #500 means following 500 arrows.

Interactive diagram

Three pointers — prev, curr, and a saved next — flip every arrow in one O(n) pass.

1curr234prev
Three pointers

prev starts at None and curr at the head. The plan: walk the list once, flipping each node's next pointer backwards.

prev
None
curr
1

Lessons in this topic

Check off lessons as you go — your progress is saved in this browser.

  1. Singly linked lists and the node model

    Nodes, head, tail, None termination; why access is O(n).

    20 min
  2. Insertion (head, tail, position)

    Pointer rewiring order and the dummy-node trick.

    20 min
  3. Deletion and search

    Unlinking via the predecessor; why you track prev.

    20 min
  4. Reversing a linked list

    The three-pointer walk — the most-asked list question.

    25 min
  5. Fast and slow pointers

    Middle of list, cycle detection, cycle entry (Floyd).

    25 min
  6. Doubly linked lists

    prev pointers, O(1) delete-given-node, sentinel pairs.

    20 min
  7. Merging and intersections

    Zipper merge of sorted lists; aligning tails for intersection.

    20 min
  8. Design: LRU cache

    Hash map + doubly linked list = O(1) get/put with eviction.

    30 min

Operations

Traversal and insertion

Walk with a cursor until the splice point, then rewire two pointers — new node first, predecessor second.

Traversal and insertion
class ListNode:    def __init__(self, val: int = 0, next: "ListNode | None" = None):        self.val = val        self.next = nextdef insert_at_head(head: ListNode | None, val: int) -> ListNode:    """O(1): the new node simply adopts the old head."""    return ListNode(val, head)def insert_at_tail(head: ListNode | None, val: int) -> ListNode:    """O(n): walk to the end, then link. (Keep a tail pointer to make it O(1).)"""    node = ListNode(val)    if head is None:        return node    cursor = head    while cursor.next:
Time: Head: O(1) · tail/position: O(n)Space: O(1)

Edge cases

  • Insertion into an empty list — the sentinel makes it fall out naturally.
  • Index equal to length appends; index beyond it should raise.
  • Single-node lists: head and tail are the same node.

Common mistakes

  • Assigning prev.next = new before capturing the old prev.next — the suffix is lost.
  • Returning the old head after a head insertion (callers keep the stale head).

Delete a node

Deletion is bypassing: point the predecessor's next past the victim. The victim just becomes unreachable.

Delete a node
def delete_value(head: ListNode | None, val: int) -> ListNode | None:    """Remove the first node holding val. O(n) time, O(1) space."""    dummy = ListNode(0, head)    prev = dummy    while prev.next and prev.next.val != val:        prev = prev.next    if prev.next:                    # found it        prev.next = prev.next.next   # bypass — node is now unreachable    return dummy.next
Time: O(n) to find · O(1) to unlinkSpace: O(1)

Edge cases

  • Deleting the head — the sentinel means no special branch.
  • Value absent: list is returned unchanged.
  • Deleting the only node returns None (empty list).

Common mistakes

  • Stopping the scan at the node itself instead of its predecessor — you need prev to rewire.
  • Checking prev.next.val without first checking prev.next isn't None.

Reverse the list

Walk once, flipping each node's next to point backwards. Save curr.next before overwriting or the remainder is lost.

1curr234prev
Three pointers

prev starts at None and curr at the head. The plan: walk the list once, flipping each node's next pointer backwards.

prev
None
curr
1
def reverse_list(head: ListNode | None) -> ListNode | None:    prev = None    curr = head    while curr:        nxt = curr.next      # 1. save the rest        curr.next = prev     # 2. flip the arrow        prev = curr          # 3. advance prev        curr = nxt           # 4. advance curr    return prev              # prev is the new head
Time: O(n)Space: O(1) — the recursive version costs O(n) stack

Edge cases

  • Empty list → returns None cleanly.
  • Single node → returned as-is, next already None.
  • Remember the old head is now the tail (next = None).

Common mistakes

  • Overwriting curr.next before saving it — the classic lost-suffix bug.
  • Returning curr (always None at loop exit) instead of prev.

Cycle detection (Floyd's tortoise and hare)

slow moves 1, fast moves 2. In a cycle the gap shrinks by 1 per tick, so they must meet; with no cycle, fast hits None.

1slow · fast2345
Tortoise and hare

slow moves one node per tick, fast moves two. If a cycle exists, fast laps slow inside the loop; if not, fast reaches None.

def has_cycle(head: ListNode | None) -> bool:    """O(n) time, O(1) space — no visited set needed."""    slow = fast = head    while fast and fast.next:        slow = slow.next        fast = fast.next.next        if slow is fast:          # identity, not value equality            return True    return False
Time: O(n)Space: O(1) — the hash-set version costs O(n) space

Edge cases

  • Empty or single-node list without self-loop: no cycle.
  • A node pointing to itself is the smallest cycle.
  • To find the cycle START, reset one pointer to head and step both by 1 until they meet.

Common mistakes

  • Comparing slow.val == fast.val instead of node identity (values can repeat).
  • Advancing fast without checking fast.next — AttributeError on None.

Merge two sorted lists

A zipper: repeatedly attach the smaller head to the merged tail. A dummy node holds the zipper's start.

Merge two sorted lists
def merge_sorted(a: ListNode | None, b: ListNode | None) -> ListNode | None:    """Splice two ascending lists into one. O(n + m) time, O(1) space."""    dummy = ListNode(0)    tail = dummy    while a and b:        if a.val <= b.val:            tail.next, a = a, a.next        else:            tail.next, b = b, b.next        tail = tail.next    tail.next = a if a else b     # attach the leftover run    return dummy.next
Time: O(n + m)Space: O(1) — reuses existing nodes

Edge cases

  • Either list empty → return the other.
  • Duplicate values: <= keeps the merge stable.
  • Don't forget the leftover tail after one list empties.

Common mistakes

  • Building new nodes instead of splicing existing ones (unneeded O(n) allocation).
  • Losing the merged head by advancing dummy itself instead of a tail cursor.

Complexity analysis

OperationBestAverageWorstSpace
Access by indexO(1) headO(n)O(n)
Search by valueO(1)O(n)O(n)O(1)
Insert / delete at headO(1)O(1)O(1)
Insert / delete at tailO(1) w/ tail ptrO(n)O(n)
Insert / delete after a known nodeO(1)O(1)O(1)
ReverseO(n)O(n)O(n)O(1)

Compare row by row with arrays: everything positional flips. Doubly linked lists make delete-given-node O(1) because the predecessor comes free.

Python implementation

Production-quality code with type hints, validation, and docstrings.

A complete singly linked list with sentinel head
from typing import Iterator, Optionalclass _Node:    __slots__ = ("val", "next")    def __init__(self, val: object, next: Optional["_Node"] = None):        self.val = val        self.next = nextclass SinglyLinkedList:    """Singly linked list with sentinel head and tail pointer.    push_front/push_back are O(1); positional ops are O(n)."""    def __init__(self) -> None:        self._head = _Node(None)      # sentinel — never holds data        self._tail = self._head

What interviewers expect you to know

Properties interviewers probe

  • No random access: index i costs O(n). If you need indexes, you likely want an array.
  • Insert/delete after a KNOWN node is O(1) — 'known' is the load-bearing word; finding it is O(n).
  • Doubly linked lists give O(1) delete of a given node because prev is free; singly linked needs the predecessor.

Follow-ups to expect

  • "Reverse it recursively — what's the space cost?" (O(n) stack vs O(1) iterative.)
  • "Where does the cycle START?" — after meeting, reset one pointer to head; step both by one; they meet at the entry (Floyd part 2).
  • "Find the middle in one pass" — fast/slow; when fast hits the end, slow is at the middle.
  • "Why does LRU need a doubly linked list?" — eviction removes the tail node given only the node, needing prev in O(1).

How to narrate pointer code

  • Say the order out loud: 'save next, flip, advance' — it proves you won't lose the suffix.
  • Declare the dummy node before writing edge-case code: 'I'll use a sentinel so head deletion isn't special.'
  • Draw 3 nodes max. Every list algorithm's correctness is visible with three boxes and arrows.

Common mistakes

Losing the rest of the list

Writing curr.next = prev before saving curr.next orphans everything after curr. The save comes first — always.

Null-pointer steps

fast.next.next when fast.next is None crashes. Guard: while fast and fast.next.

Forgetting to return the new head

After reversal or head insertion the entry point changed; returning the old head hands back the tail (or a stale node).

Deleting without the predecessor

In a singly linked list you unlink FROM the node before the victim. Standing on the victim is too late (unless you copy the next node's value — mention that trick explicitly).

Comparing values instead of nodes

Cycle and intersection checks compare identity (is), not equality (==) — duplicate values otherwise cause false positives.

Edge cases skipped

Empty list, single node, and all-nodes-identical break naive code. Test all three mentally before saying 'done'.

Practice problems

Hand-picked from the 355-problem DSA Atlas. Reveal hints and solutions progressively; mark and bookmark as you solve.

Easy (3)

Medium (7)

Hard (2)

Topic quiz

6 questions across concepts, code output, complexity, and scenarios. Answer all, then submit for explanations.

  1. Concept1. Why is inserting after a given node O(1) in a linked list but O(n) in an array?
  2. Code output2. After reverse_list on 1→2→3, what does the returned node contain and point to?
    def reverse_list(head):    prev = None    while head:        head.next, prev, head = prev, head, head.next    return prev
  3. Concept3. Floyd's cycle detection: why must fast and slow meet if a cycle exists?
  4. Scenario4. Design a structure with O(1) get(key) and O(1) evict-least-recently-used. What combination works?
  5. Complexity5. Recursively reversing a linked list of n nodes uses how much space?
  6. Concept6. What does a dummy/sentinel head node buy you?

Frequently asked questions

Are linked lists actually used in Python, given lists are arrays?

Directly, rarely — collections.deque (a linked structure of blocks) covers most needs. But the pointer patterns transfer to trees, graphs, and cache design, which is why interviews still test them.

When do I choose a doubly linked list?

When you must delete a node given only that node, or traverse both directions — LRU caches, browser history, text editors. The cost is one extra pointer per node and more rewiring per edit.

How do I find where a cycle begins, not just that one exists?

After fast and slow meet, move one pointer back to head; advance both one step at a time. They meet exactly at the cycle entry — a provable distance identity, and a favourite follow-up.

Summary & cheat sheet

Key takeaways

  • Linked list = O(1) splicing at a known node, O(n) everything positional.
  • Save-flip-advance: the reversal rhythm that never loses the suffix.
  • Sentinel nodes delete edge cases; tail pointers make appends O(1).
  • Fast/slow pointers answer middle, cycle, and kth-from-end in one pass.
  • LRU cache = hash map + doubly linked list; both parts are needed for O(1).

Formulas & cheat sheet

  • Middle: when fast reaches the end, slow has gone n/2 steps
  • Cycle entry: head-to-entry distance = meeting-point-to-entry distance (mod cycle length)
  • kth from end: advance lead pointer k steps, then move both until lead hits None

Interview checklist

  • I can reverse a list iteratively without notes.
  • I can write Floyd's algorithm and explain the meeting guarantee.
  • I use a dummy node whenever the head can change.
  • I can merge two sorted lists by splicing, not copying.
  • I can sketch the LRU design and say why each half is needed.