DSA Handbook

#Linked lists

Recognition in one line: the input is a chain of nodes and you are reordering, detecting a cycle, or finding a position relative to the end.

Linked lists are less common in modern interviews than they were, but they still appear — and they are unusually mechanical: three techniques cover nearly everything, and the difficulty is bookkeeping rather than insight.


#1 · Recognition cues

CueTechnique
"reverse the list / a portion of it"In-place reversal
"k-th from the end"Fast-slow with a fixed gap
"middle of the list"Fast-slow, fast moves two
"detect a cycle"Floyd's tortoise and hare
"where does the cycle start?"Floyd's, then reset one pointer to the head
"remove nodes" / "the head might change"Dummy head node
"merge two sorted lists"Parallel pointers
"palindrome list"Find middle, reverse half, compare
"reorder / interleave"Split, reverse, merge

The three techniques, and that is genuinely all of them:

   DUMMY HEAD      a fake node before the real head, so deleting the first
                   node needs no special case

   FAST-SLOW       two pointers at different speeds -- finds middles,
                   k-th-from-end, and cycles

   REVERSAL        three pointers walking the list, flipping each `next`

Interactive simulation — needs JavaScript.


#2 · The templates

class ListNode:
    def __init__(self, val=0, next=None):
        self.val, self.next = val, next
# DUMMY HEAD -- removes every "what if it is the first node?" special case
def remove_elements(head, target):
    dummy = ListNode(0, head)
    prev = dummy
    while prev.next:
        if prev.next.val == target:
            prev.next = prev.next.next     # unlink, do not advance
        else:
            prev = prev.next
    return dummy.next                      # the real head, possibly changed
# IN-PLACE REVERSAL -- three pointers. Type this until it is automatic.
def reverse_list(head):
    previous, current = None, head
    while current:
        nxt = current.next        # save the rest of the list BEFORE overwriting
        current.next = previous   # flip the arrow
        previous = current        # step both pointers forward
        current = nxt
    return previous               # `previous` is the new head

Saving current.next before overwriting it is the whole trick. Overwrite first and you have lost the rest of the list, permanently.

# FAST-SLOW -- middle, cycle, k-th from the end
def middle(head):
    slow = fast = head
    while fast and fast.next:      # BOTH guards, or AttributeError on odd lengths
        slow = slow.next
        fast = fast.next.next
    return slow                    # for even length, this is the SECOND middle

#3 · The ladder

#Easy

#ProblemSourceThe point
1Reverse Linked ListLC 206 · NeetCodeThe template. Type it from memory
2Merge Two Sorted ListsLC 21 · NeetCodeDummy head + parallel pointers
3Linked List CycleLC 141 · NeetCodeFloyd's, detection only
4Middle of the Linked ListLC 876Fast-slow
5Remove Duplicates from Sorted ListLC 83Single pass

#Medium

#ProblemSourceThe point
6Remove Nth Node From EndLC 19 · NeetCodeFast-slow with a gap, plus dummy head
7Reorder ListLC 143 · NeetCodeAll three techniques in one problem
8Linked List Cycle IILC 142Floyd's + the entry-point derivation
9Add Two NumbersLC 2 · NeetCodeCarry handling
10Copy List with Random PointerLC 138 · NeetCodeMap old → new, or interleave
11Reverse Linked List IILC 92Reversal of a sublist. Fiddly
12Palindrome Linked ListLC 234Middle + reverse + compare, O(1) space
13LRU CacheLC 146 · NeetCodeDoubly linked list + hash map

#Hard

#ProblemSourceThe point
14Reverse Nodes in k-GroupLC 25 · NeetCodeReversal, repeatedly, with reconnection
15Merge k Sorted ListsLC 23 · NeetCodeHeap of heads

If you only do four: 206, 19, 143, 146.


#4 · Worked example — LC 19, Remove Nth From End

Problem: remove the n-th node from the end, in one pass.

Two techniques together: a gap of n between two pointers finds the position, and a dummy head handles removing the actual head.

   list = 1 -> 2 -> 3 -> 4 -> 5,   n = 2      (remove 4)

   dummy -> 1 -> 2 -> 3 -> 4 -> 5

   advance `fast` n+1 = 3 steps from dummy:
   dummy -> 1 -> 2 -> 3 -> 4 -> 5
     ^slow          ^fast

   move both until fast falls off the end:
   dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> None
                 ^slow          ^fast

   slow now sits just BEFORE the node to remove.
   slow.next = slow.next.next  ->  3 -> 5
def remove_nth_from_end(head, n):
    dummy = ListNode(0, head)
    slow = fast = dummy

    # n+1, not n: we want `slow` to stop just BEFORE the target so we can
    # unlink it. Starting from dummy is what makes removing the head work.
    for _ in range(n + 1):
        fast = fast.next

    while fast:
        slow = slow.next
        fast = fast.next

    slow.next = slow.next.next
    return dummy.next
public ListNode removeNthFromEnd(ListNode head, int n) {
    ListNode dummy = new ListNode(0, head);
    ListNode slow = dummy, fast = dummy;

    for (int i = 0; i <= n; i++) fast = fast.next;   // n+1 steps

    while (fast != null) {
        slow = slow.next;
        fast = fast.next;
    }
    slow.next = slow.next.next;
    return dummy.next;                               // head may have changed
}

Return dummy.next, never head. If the head was removed, head points at a detached node. That is the single most common bug in this problem.


#5 · Worked example — LC 143, Reorder List

Problem: reorder L0 → L1 → … → Ln into L0 → Ln → L1 → Ln-1 → …

All three techniques in one problem, which is why it is the best single linked-list exercise.

   1 -> 2 -> 3 -> 4 -> 5

   STEP 1  find the middle (fast-slow)          -> 3
   STEP 2  reverse the second half              -> 1 -> 2 -> 3    5 -> 4
   STEP 3  interleave the two halves            -> 1 -> 5 -> 2 -> 4 -> 3
def reorder_list(head):
    if not head or not head.next:
        return

    # 1. middle
    slow = fast = head
    while fast.next and fast.next.next:
        slow = slow.next
        fast = fast.next.next

    # 2. reverse everything after `slow`, and CUT the link so the first half
    #    terminates -- forgetting this creates a cycle.
    second = slow.next
    slow.next = None
    previous = None
    while second:
        nxt = second.next
        second.next = previous
        previous = second
        second = nxt

    # 3. interleave
    first, second = head, previous
    while second:
        first_next, second_next = first.next, second.next
        first.next = second
        second.next = first_next
        first, second = first_next, second_next
public void reorderList(ListNode head) {
    if (head == null || head.next == null) return;

    ListNode slow = head, fast = head;
    while (fast.next != null && fast.next.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }

    ListNode second = slow.next;
    slow.next = null;                    // CUT -- otherwise you build a cycle
    ListNode prev = null;
    while (second != null) {
        ListNode next = second.next;
        second.next = prev;
        prev = second;
        second = next;
    }

    ListNode first = head;
    second = prev;
    while (second != null) {
        ListNode firstNext = first.next, secondNext = second.next;
        first.next = second;
        second.next = firstNext;
        first = firstNext;
        second = secondNext;
    }
}

slow.next = None is the line people forget. Without it the first half still points into the reversed second half and you produce an infinite loop — which shows up as a hang, not an exception.


#6 · Worked example — LC 142, Cycle II

Problem: find the node where the cycle begins.

The derivation is the interview, not the code.

   Let:  a = distance from head to the cycle entry
         b = distance from entry to the meeting point
         c = remaining cycle length, so cycle = b + c

   When they meet:
     slow travelled  a + b
     fast travelled  a + b + k(b + c)      for some number of laps k
     fast = 2 * slow, so:
        a + b + k(b+c) = 2(a + b)
        k(b+c)         = a + b
        a              = k(b+c) - b
        a              = (k-1)(b+c) + c

   Read the last line: the distance from the HEAD to the entry equals the
   distance from the MEETING POINT to the entry, plus whole laps.

   So: reset one pointer to the head, advance both one step at a time,
       and they meet exactly at the entry.
def detect_cycle(head):
    slow = fast = head
    while fast and fast.next:
        slow, fast = slow.next, fast.next.next
        if slow is fast:                     # cycle confirmed
            slow = head                      # reset ONE pointer to the head
            while slow is not fast:          # now both move ONE step
                slow, fast = slow.next, fast.next
            return slow                      # the entry point
    return None
public 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;
}

Being able to derive a = (k-1)(b+c) + c on the whiteboard is what distinguishes understanding from memorising here. Interviewers ask for it.


#7 · Same problem in disguise

ProblemReally is
Middle of the List (LC 876)Fast-slow, fast moves two
Palindrome List (LC 234)Middle + reverse + compare
Reorder List (LC 143)Middle + reverse + interleave
Remove Nth From End (LC 19)Fast-slow with a fixed gap
Reverse in k-Group (LC 25)Reversal, repeated, with reconnection
Intersection of Two Lists (LC 160)Two pointers switching lists — equalises path length

Middle + reverse solves three of these. Learn those two operations properly and half the medium problems collapse.


#8 · Failure modes

BugSymptomFix
Returning head after removing itWrong list returnedAlways return dummy.next
Not saving current.next before overwritingLost the rest of the listSave first, then flip
fast.next.next without both guardsAttributeError / NPEwhile fast and fast.next
Forgetting to cut in Reorder ListInfinite loop, program hangsslow.next = None
No dummy head when the head may changeSpecial-case bugsUse one; it is one line
Off-by-one in the fast-slow gapRemoves the wrong noden + 1 steps, from dummy

#9 · Interview questions on this pattern

QuestionWhat to say
⭐ "Reverse a list in place."Three pointers: save next, flip current's pointer, advance both. Saving next before overwriting is the whole trick. O(n) time, O(1) space.
"Find the middle in one pass."Fast-slow: fast moves two, slow one. When fast falls off the end, slow is at the middle. For even length, decide which middle you want and say so.
⭐ "Where does the cycle start?"Floyd's to find a meeting point, then reset one pointer to the head and advance both one step — they meet at the entry. And derive why: the head-to-entry distance equals the meeting-point-to-entry distance plus whole laps.
"Why a dummy head?"It removes every special case where the head itself changes — deletion of the first node, insertion before it. One line, and it deletes a class of bugs.
"Reverse in k-groups?"Reverse each block of k, reconnect the tails, leave a trailing partial group untouched unless told otherwise. Ask which, because it changes the answer.
"Recursive or iterative reversal?"Iterative — O(1) space. Recursion is O(n) stack, which matters on a long list and is a real risk in Python.

#Stop condition

You are done with this pattern when you can:

  1. type the reversal template from memory with no bugs,
  2. explain why dummy.next is returned rather than head,
  3. derive the Floyd's entry-point result on a whiteboard,
  4. solve Reorder List including the cut, and
  5. name which technique a problem needs from its statement.