Linked Lists

Node chains with O(1) insert/delete. Master fast & slow pointers.

Linked Lists

Each node holds a value + pointer(s) to next/previous node. No random access — must traverse.

Why Linked Lists?

  • O(1) insert/delete if you already have a reference to the node — arrays must shift elements
  • Grow and shrink dynamically with no resizing overhead
  • Used to implement: stacks, queues, hash map buckets, LRU caches

LRU Cache (classic linked list application)

Combine a hash map (O(1) lookup) + doubly linked list (O(1) move-to-front):

  • Most recently used (MRU) lives at the head
  • Least recently used (LRU) lives at the tail — evicted when cache is full
  • On access: O(1) lookup via hash map, then O(1) move node to head via prev/next pointers

Complexity

| Operation | Singly | Doubly | |------------------|---------|---------| | Access by index | O(n) | O(n) | | Insert at head | O(1) | O(1) | | Insert at tail | O(1)* | O(1)* | | Delete (known node) | O(1) | O(1) | | Search | O(n) | O(n) |

*With a tail pointer maintained.

Node Reference vs Value Comparison

# Reference comparison — same object in memory (identity)
node_a is node_b       # True only if they ARE the same node

# Value comparison — same data
node_a.value == node_b.value   # True if data matches, even different nodes

For intersection problems, use is (reference) — two lists intersect when their nodes are the same object, not just equal values.

Python Node Template

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

class DNode:
    def __init__(self, val):
        self.val = val
        self.next = None      # doubly
        self.prev = None

Fast & Slow Pointer (Floyd's Algorithm)

Time: O(n), Space: O(1) — no extra space needed.

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

def find_cycle_start(head):
    slow = fast = head
    # Phase 1: detect meeting point
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow is fast:
            break
    else:
        return None  # no cycle
    # Phase 2: reset slow to head, both advance at speed 1
    slow = head
    while slow is not fast:
        slow = slow.next
        fast = fast.next
    return slow  # cycle start node

Reverse a Linked List

def reverse(head):
    prev = None
    curr = head
    while curr:
        nxt = curr.next
        curr.next = prev
        prev = curr
        curr = nxt
    return prev   # new head

Find Middle Node

def find_middle(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
    return slow  # middle (or right-of-middle for even length)

Remove Duplicates (with buffer)

def remove_dups(head):
    seen = set()
    prev, curr = None, head
    while curr:
        if curr.val in seen:
            prev.next = curr.next  # skip duplicate
        else:
            seen.add(curr.val)
            prev = curr
        curr = curr.next

k-th From End (two-pointer)

def kth_from_end(head, k):
    fast = slow = head
    for _ in range(k):        # advance fast k steps
        fast = fast.next
    while fast:               # move both until fast reaches end
        slow = slow.next
        fast = fast.next
    return slow

Key Interview Tips

  • Always handle null/empty list and single node as first cases
  • Draw pointer reassignments before coding — it's easy to lose a node
  • Fast+slow pointer finds: midpoint, cycle start, nth-from-end — all O(n) time O(1) space
  • Use a dummy head node to simplify edge cases at the front of the list
  • For intersection: align lengths first, then walk together until node_a is node_b