Linked lists

Linked list patterns

Dummy nodes, reverse, fast/slow pointers, merge, and cycle detection — the list toolkit for interviews — with plain-language explanation before templates.

Why dummy nodes and two pointers

Dummy heads remove special cases at the front. Fast/slow pointers find the middle or a cycle because fast laps slow if a loop exists. Reverse is three pointers (prev, cur, next) — draw one iteration before coding.

Analogy: train cars

Linked list train
Each car only knows the next coupler.

A linked list is a train: each car stores a coupler to the next. You can’t jump to car 50 without walking. Reverse = rehang every coupler to point backward. Fast/slow pointers = two conductors walking at different speeds to find the middle or a loop in the track.

Tools

Linked list moves
Dummy, fast/slow, reverse.
  • Dummy head for insert/delete at front
  • Fast/slow for mid & cycle
  • Three pointers to reverse
def reverse(head):
    prev = None
    while head:
        nxt = head.next
        head.next = prev
        prev = head
        head = nxt
    return prev
ListNode reverse(ListNode head) {
    ListNode prev = null;
    while (head != null) {
        ListNode nxt = head.next;
        head.next = prev;
        prev = head;
        head = nxt;
    }
    return prev;
}
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
boolean hasCycle(ListNode head) {
    ListNode slow = head, fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next; fast = fast.next.next;
        if (slow == fast) return true;
    }
    return false;
}

Drill list

  1. Reverse list / reverse k-group
  2. Detect cycle + find entrance
  3. Merge two sorted / merge k
  4. Remove nth from end (two pointers)
  5. Reorder list

Worked trace: reverse

List 1→2→3. prev=null, cur=1. Save next=2, point 1→null, advance prev=1, cur=2. Next iter: 2→1, then 3→2. Return prev=3. Drawing one iteration prevents the usual lost-next bug.

When to reach for this pattern

Lists punish off-by-one pointer bugs more than algorithmic insight. Interviews watch whether you use a dummy, draw before/after pointers, and handle empty / single-node cases.

Core template + worked trace

Iterative reverse with a dry-run:

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

def reverse_list(head: ListNode | None) -> ListNode | None:
    prev, cur = None, head
    while cur:
        nxt = cur.next     # save
        cur.next = prev    # flip
        prev = cur         # advance
        cur = nxt
    return prev

# 1→2→3→None
# step1: None←1  2→3 ; prev=1 cur=2
# step2: None←1←2  3 ; prev=2 cur=3
# step3: None←1←2←3 ; prev=3 cur=None → return 3
class ListNode {
    int val;
    ListNode next;
    ListNode(int val) { this.val = val; }
    ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}

ListNode reverseList(ListNode head) {
    ListNode prev = null, cur = head;
    while (cur != null) {
        ListNode nxt = cur.next;  // save
        cur.next = prev;          // flip
        prev = cur;               // advance
        cur = nxt;
    }
    return prev;
}

// 1→2→3→null
// step1: null←1  2→3 ; prev=1 cur=2
// step2: null←1←2  3 ; prev=2 cur=3
// step3: null←1←2←3 ; prev=3 cur=null → return 3

Dummy for delete / merge so you never special-case a new head:

def remove_elements(head, val):
    dummy = ListNode(0, head)
    cur = dummy
    while cur.next:
        if cur.next.val == val:
            cur.next = cur.next.next
        else:
            cur = cur.next
    return dummy.next
ListNode removeElements(ListNode head, int val) {
    ListNode dummy = new ListNode(0, head);
    ListNode cur = dummy;
    while (cur.next != null) {
        if (cur.next.val == val) cur.next = cur.next.next;
        else cur = cur.next;
    }
    return dummy.next;
}

Edge cases & common bugs

Complexity — say it aloud

Interview talk track

You: ‘I’ll reverse iteratively with three pointers — prev, cur, next — so I don’t lose the rest of the list. I’ll dry-run 1→2→3 on the board, then code. Edge cases: empty and single node.’

Practice set

  • Reverse Linked List
  • Merge Two Sorted Lists
  • Linked List Cycle / Cycle II
  • Middle of the Linked List
  • Remove Nth Node From End of List
  • Add Two Numbers
  • Reorder List
  • Copy List with Random Pointer

Harder follow-up

Harder variant: Reverse Nodes in k-Group — reverse every k nodes, leave the tail if shorter than k. Count k ahead first; if not enough, stop. Same three-pointer flip inside each group; stitch with a dummy.

Pattern bank: more questions + efficient solutions

Five list staples. Draw next pointers before coding; name the three locals (prev/curr/next) or the Floyd pair aloud.

Q: Reverse Linked List — reverse a singly linked list and return the new head.

def reverse_list(head):
    prev = None
    curr = head
    while curr:
        nxt = curr.next
        curr.next = prev
        prev = curr
        curr = nxt
    return prev
# O(n) time, O(1) space
ListNode reverseList(ListNode head) {
    ListNode prev = null, curr = head;
    while (curr != null) {
        ListNode nxt = curr.next;
        curr.next = prev;
        prev = curr;
        curr = nxt;
    }
    return prev;
}
// O(n) time, O(1) space

Q: Merge Two Sorted Lists — merge two sorted singly linked lists into one sorted list.

# assumes ListNode(val=0, next=None)
def merge_two_lists(a, b):
    dummy = cur = ListNode(0)
    while a and b:
        if a.val <= b.val:
            cur.next, a = a, a.next
        else:
            cur.next, b = b, b.next
        cur = cur.next
    cur.next = a or b
    return dummy.next
# O(n+m) time, O(1) extra
ListNode mergeTwoLists(ListNode a, ListNode b) {
    ListNode dummy = new ListNode(0), cur = dummy;
    while (a != null && b != null) {
        if (a.val <= b.val) { cur.next = a; a = a.next; }
        else { cur.next = b; b = b.next; }
        cur = cur.next;
    }
    cur.next = a != null ? a : b;
    return dummy.next;
}
// O(n+m) time, O(1) extra

Q: Linked List Cycle II — if a cycle exists, return the node where the cycle begins; else null (Floyd tortoise/hare).

def detect_cycle(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow is fast:
            slow = head
            while slow is not fast:
                slow = slow.next
                fast = fast.next
            return slow
    return None
# O(n) time, O(1) space
ListNode detectCycle(ListNode head) {
    ListNode slow = head, fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow == fast) {
            slow = head;
            while (slow != fast) {
                slow = slow.next;
                fast = fast.next;
            }
            return slow;
        }
    }
    return null;
}
// O(n) time, O(1) space

Q: LRU Cache — implement get/put in O(1) average with capacity; evict least-recently-used on overflow (HashMap + doubly linked list).

class LRUCache:
    class Node:
        __slots__ = ("key", "val", "prev", "next")
        def __init__(self, key=0, val=0):
            self.key, self.val = key, val
            self.prev = self.next = None

    def __init__(self, capacity):
        self.cap = capacity
        self.map = {}
        self.head, self.tail = self.Node(), self.Node()  # sentinels
        self.head.next, self.tail.prev = self.tail, self.head

    def _add(self, node):  # insert after head (MRU)
        nxt = self.head.next
        self.head.next = node
        node.prev, node.next = self.head, nxt
        nxt.prev = node

    def _remove(self, node):
        node.prev.next = node.next
        node.next.prev = node.prev

    def get(self, key):
        if key not in self.map:
            return -1
        node = self.map[key]
        self._remove(node)
        self._add(node)
        return node.val

    def put(self, key, val):
        if key in self.map:
            self._remove(self.map[key])
        node = self.Node(key, val)
        self.map[key] = node
        self._add(node)
        if len(self.map) > self.cap:
            lru = self.tail.prev
            self._remove(lru)
            del self.map[lru.key]
# O(1) get/put, O(capacity) space
class LRUCache {
    static class Node {
        int key, val; Node prev, next;
        Node(int k, int v) { key = k; val = v; }
    }
    final int cap;
    final Map<Integer, Node> map = new HashMap<>();
    final Node head = new Node(0, 0), tail = new Node(0, 0);

    LRUCache(int capacity) {
        cap = capacity;
        head.next = tail; tail.prev = head;
    }
    void add(Node n) {
        Node nxt = head.next;
        head.next = n; n.prev = head; n.next = nxt; nxt.prev = n;
    }
    void remove(Node n) { n.prev.next = n.next; n.next.prev = n.prev; }

    int get(int key) {
        if (!map.containsKey(key)) return -1;
        Node n = map.get(key);
        remove(n); add(n);
        return n.val;
    }
    void put(int key, int val) {
        if (map.containsKey(key)) remove(map.get(key));
        Node n = new Node(key, val);
        map.put(key, n); add(n);
        if (map.size() > cap) {
            Node lru = tail.prev;
            remove(lru); map.remove(lru.key);
        }
    }
}
// O(1) get/put, O(capacity) space

Q: Reverse Nodes in k-Group — reverse the list in groups of k nodes; leave the trailing < k nodes as-is.

# assumes ListNode(val=0, next=None)
def reverse_k_group(head, k):
    def reverse(a, b):  # reverse [a, b)
        prev, curr = None, a
        while curr is not b:
            nxt = curr.next
            curr.next = prev
            prev, curr = curr, nxt
        return prev

    dummy = ListNode(0)
    dummy.next = head
    group_prev = dummy
    while True:
        kth = group_prev
        for _ in range(k):
            kth = kth.next
            if not kth:
                return dummy.next
        group_next = kth.next
        new_head = reverse(group_prev.next, group_next)
        tail = group_prev.next
        group_prev.next = new_head
        tail.next = group_next
        group_prev = tail
# O(n) time, O(1) space
ListNode reverseKGroup(ListNode head, int k) {
    ListNode dummy = new ListNode(0, head), groupPrev = dummy;
    while (true) {
        ListNode kth = groupPrev;
        for (int i = 0; i < k; i++) {
            kth = kth.next;
            if (kth == null) return dummy.next;
        }
        ListNode groupNext = kth.next;
        ListNode prev = groupNext, curr = groupPrev.next;
        while (curr != groupNext) {
            ListNode nxt = curr.next;
            curr.next = prev;
            prev = curr;
            curr = nxt;
        }
        ListNode tail = groupPrev.next;
        groupPrev.next = kth;
        groupPrev = tail;
    }
}
// O(n) time, O(1) space

Pattern drill: 45 minutes

← Lattice