DSA sheet · Linked List · concept video

Linked List Patterns Overview

This is the map for the whole linked list section. Before any coding, the teacher answers two questions: what is a linked list, and why do we need it when we already have arrays? Then she walks through the five patterns that she says cover the linked list questions asked in interviews: basic traversal, fast & slow pointers, reversal, linked list + stack, and merge / sort. For each one she shows the idea on a small drawing and names the problems that use it. These notes add a small, tested Python template for every pattern, so that each later page feels familiar.

This page adapts the usual order for a concept video:
① what a linked list is → ② array vs linked list → ③ each pattern: the idea → when to spot it → intuition → building it from her example → steps → Python template → line by line → dry run → complexity & remember → ④ which sheet problems use which pattern → ⑤ revision

Part 0 · What a linked list is

Linear vs non-linear data structures

The teacher starts here, because it tells us what kind of thing a linked list is.

linearnon-linear
from one element you can move to…only one next elementseveral elements
examplesarrays, strings, linked liststrees (a node has a left and a right child), graphs

So a linked list is a linear data structure: items in a row, each leading to exactly one next item.

The node

A linked list is made of nodes. Each node holds two things:

In the drawing, the address is shown as an arrow:

[1] → [2] → [3] → [4] → None
 ↑
head
the node class (LeetCode style)
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val      # the data
        self.next = next    # the next node, or None at the end

Head, walking, no index

Save next before you change a pointer

A node is reachable only through the arrow that points to it. If you overwrite a.next while nothing else points to the old next node, that node and everything after it are lost. So store the old next in a variable before rewiring. You'll see this rule again and again: in insertion, and above all in reversal (Pattern 3).

Part A · Array vs linked list: why we need it

Arrays are very useful, but the teacher lists three problems with them. Each one is a reason linked lists exist.

1Problem 1: an array needs one unbroken block of memory

All array items sit side by side. A linked list's memory is dynamic: each node can be anywhere in memory, because every node carries the address of the next one. The nodes are tied together by those addresses, not by being neighbours.

2Problem 2: an array's size is fixed

Once a (classic) array's size is set, it can't change. A linked list just keeps adding new nodes, so its size grows and shrinks freely.

(Python's list hides this by quietly copying itself into a bigger block when it fills up, but the "one block" rule still holds underneath.)

3Problem 3: inserting and deleting in an array is costly

Her example: the array [1, 2, 3, 4] (indices 0 to 3). Delete the 1. Now there's a hole at index 0, so every later item must shift one place: 2 to index 0, 3 to index 1, 4 to index 2.

index:   0   1   2   3
before: [1] [2] [3] [4]
delete 1, then shift everything:
after:  [2] [3] [4]        ← n − 1 moves → O(n)

Insertion has the same cost: to make room, items shift the other way. So insert and delete in an array are O(n).

In a linked list, nothing shifts. Her example: insert 5 between 1 and 2. Make node 5, point 5.next at 2, and point 1.next at 5. Two arrows change, nothing else moves → O(1).

before:  [1] → [2] → [3] → [4] → None

after:   [1] → [5] → [2] → [3] → [4] → None
          1.next changed      5.next = old 1.next
Doubt: is linked list insertion always O(1)?
→ The rewiring is O(1), once you're standing at the right node. Getting there still means walking from the head, O(n) in the worst case. So the honest comparison (my addition) is below: linked lists win when you already hold the spot (front of the list, or a node you're visiting anyway); arrays win at jumping to an index.
arraylinked list
memoryone unbroken blocknodes anywhere, linked by addresses
sizefixedgrows / shrinks freely
insert / delete at a known spotO(n), items shiftO(1), change 1–2 arrows
reach the i-th itemO(1) indexingO(i), walk from head
extra memory per itemnoneone pointer (next)

Part B · Pattern 1: basic traversal

The teacher calls this the very first pattern: once you know it, you can move on to the others.

1The idea in simple words

Traversal means visiting the nodes one by one, from the head to the end, and doing some work at each node: counting them, finding the length, finding a value, inserting a node at a position, deleting a node at a position.

2When to use it

3Intuition

A finger starts on the head and keeps hopping along the arrows until it lands on None. Each hop is head = head.next.

4Building it from her example: counting nodes

List 1 → 2 → 3 → 4. We loop while the pointer isn't None, adding 1 to a counter for each node and then hopping forward. When the pointer becomes None, we stop, and the counter holds the length.

Her warning about where count starts

She counts the jumps: from 1 to 2 is one jump, 2 to 3 two jumps, 3 to 4 three jumps. But there are four nodes. If you count jumps (that is, you stop when you are standing on the last node), a counter that starts at 0 ends at 3, one short. In that style you must start count at 1 (the node you're already standing on).

Doubt: so should count start at 0 or 1?
→ It depends on the loop, and it's worth seeing both:
• while curr is not None: you add 1 for every node, including the last, and stop on None → start at 0. (This also gives 0 for an empty list.)
• while curr.next is not None: you add 1 for every jump and stop on the last node → start at 1. (This one needs a separate check for an empty list, because None.next crashes.)
Both give 4 for 1 → 2 → 3 → 4.
Doubt: her snippet moves head itself. Isn't that the thing we must never do?
→ Inside a counting function, head is just a local name, and we never need the list again, so it's harmless there. As soon as you need to return or reuse the list (any insert or delete problem), walk with a copy, curr = head. The templates below always use curr, which is the safe habit.

5Steps

  1. count = 0, curr = head.
  2. While curr is not None: count += 1, curr = curr.next.
  3. Return count.

6Python template

Template · length (count every node, start at 0)
def length(head):
    count = 0
    curr = head                 # walk with a copy, keep head safe
    while curr is not None:     # stop after the last node
        count += 1              # one more node seen
        curr = curr.next        # hop to the next node
    return count
Template · length (count jumps, start at 1)
def length_by_jumps(head):
    if head is None:            # this style can't handle an empty list
        return 0
    count = 1                   # the node we're standing on
    curr = head
    while curr.next is not None:    # stop ON the last node
        curr = curr.next
        count += 1              # one more jump = one more node
    return count

7Line by line

linewhat it means
while curr is not None:Visit every node, the last one included. Ends with curr on None.
count += 1The work done at each node. Swap this line for other jobs (compare with a key, sum values, …).
curr = curr.nextThe one way to move in a linked list.
while curr.next is not None:Stops on the last node. Needed when you want to change the last node (insert at end), but it counts jumps, so count starts at 1.

8Dry run on 1 → 2 → 3 → 4

stepcurr (start-at-0 version)countcurr (jumps version)count
start[1]0[1]1
1[2]1[2]2
2[3]2[3]3
3[4]3[4]4 → stop (4.next is None)
4None → stop44

9Complexity & remember

Remember (traversal) while curr → visits every node, count from 0. while curr.next → stops on the last node, count from 1. Count nodes, length, insert at a position, delete from a position: all traversal.

Part C · Pattern 2: fast & slow pointers

1The idea in simple words

The teacher links this to the two-pointer approach from arrays, with a twist: both pointers start together, but they move at different speeds. slow takes one step at a time; fast takes two. So slow always covers half the distance that fast covers.

2When to use it

3Intuition

Two runners on a track start at the same line. One runs twice as fast. When the fast runner reaches the finish line, the slow one is exactly halfway. And if the track is a loop, the fast runner will eventually come up behind the slow one and catch them.

4Building it from her examples

Use 1: the middle

List 1 → 2 → 3. Both start on 1. After one move: slow on 2, fast on 3. Fast has reached the end, and slow is on 2, the middle. That's no accident: slow runs at half speed, so it has covered half of what fast covered.

When to stop: fast jumps two nodes (fast.next.next), so both fast and fast.next must exist, otherwise we'd read .next of None. Loop while fast and fast.next.

Doubt: what about an even number of nodes, like 1 → 2 → 3 → 4?
→ (My addition; the teacher shows the odd case.) Moves: slow 2 / fast 3, then slow 3 / fast None. The loop stops with slow on 3, the second of the two middles. That's what LeetCode 876 asks for. If a problem wants the first middle (2), start fast one step ahead (fast = head.next). You'll see this in Sort List below.
odd:   [1] → [2] → [3] → None         even:  [1] → [2] → [3] → [4] → None
              ↑     ↑                                        ↑            ↑
            slow  fast (fast.next is None: stop)            slow    fast = None: stop

Use 2: detecting a cycle

Without a cycle, the last node points to None. Fast reaches None first and the loop stops. Slow is always behind, so they never meet.

With a cycle, fast never finds None; it goes round and round. Her example: 1 → 2 → 3 → 4 and 4 points back to 2.

[1] → [2] → [3] → [4]
       ↑           │
       └───────────┘     (4.next is 2, not None)
Doubt: she says we can't know when they'll meet, only that they will. Why is meeting guaranteed? Couldn't fast jump over slow?
→ (My explanation.) Once both are inside the loop, look at the gap from fast up to slow, going forward. Each move, slow goes 1 ahead and fast goes 2 ahead, so the gap shrinks by exactly 1. A gap that shrinks by 1 each time must hit 0; it can't skip from 1 to −1. Gap 0 means they're on the same node. That takes at most "length of the loop" moves.

5Steps

  1. slow = fast = head.
  2. While fast and fast.next exist: slow one step, fast two steps.
  3. Middle: return slow when the loop ends. Cycle: return True the moment slow is fast; if the loop ends, return False.

6Python templates

Template · middle node (fast & slow)
def middleNode(head):
    slow = fast = head
    while fast is not None and fast.next is not None:
        slow = slow.next            # 1 step
        fast = fast.next.next       # 2 steps
    return slow                     # second middle when the length is even
Template · cycle check (fast & slow)
def hasCycle(head):
    slow = fast = head
    while fast is not None and fast.next is not None:
        slow = slow.next
        fast = fast.next.next
        if slow is fast:            # same node object: they met
            return True
    return False                    # fast hit the end: no cycle

7Line by line

linewhat it means
slow = fast = headBoth runners start on the same node.
while fast is not None and fast.next is not None:Fast needs two nodes ahead to jump. The and checks fast first, so fast.next is never read on None.
slow = slow.next fast = fast.next.nextSpeeds 1 and 2. Slow is always at half of fast's distance.
if slow is fast:is checks "the same node", not "equal values". Two different nodes may hold the same number.

8Dry run: her cycle example (4 → back to 2)

moveslowfastfast's path this movemet?
start[1][1]–(not checked)
1[2][3]1 → 2 → 3no
2[3][2]3 → 4 → 2 (back-arrow)no
3[4][4]2 → 3 → 4yes → True

Without the back-arrow (4 → None): move 1 slow 2, fast 3; then fast.next is 4 and fast.next.next is None, so move 2 puts fast on None; the loop ends → False.

9Complexity & remember

Remember (fast & slow) slow 1 step, fast 2 steps, loop while fast and fast.next. Fast at the end → slow at the middle. Fast meets slow → cycle.

Part D · Pattern 3: reversal with three pointers

The teacher rates this one very highly: know reversal well and you can handle about half of all linked list questions.

1The idea in simple words

Normally from 1 you can go to 2, and from 2 to 3, but you can never go back, because the arrows point forward only. Reversing means turning every arrow around, so that someone standing on 3 can only go to 2, not to 4.

before:         [1] → [2] → [3] → [4] → None
after:  None ← [1] ← [2] ← [3] ← [4]
                                  ↑
                              new head

2When to use it

3Intuition: the three pointers

4Building it from her example

Standing on 1 (prev = None):

  1. Save: temp = curr.next (temp = 2). Otherwise, after the flip, 2 3 4 would be unreachable.
  2. Flip: curr.next = prev (1 now points to None).
  3. Move prev: prev = curr (prev = 1).
  4. Move curr: curr = temp (curr = 2). Now everything before 2 is reversed.

Repeat on 2: save 3, point 2 at 1, prev = 2, curr = 3. Now everything before 3 is reversed. Keep going until curr is None; prev is then on 4, the new head.

Doubt: why must prev move before curr?
→ prev must become the node we just flipped, which is curr. If curr moved first, it would already be on the next node and we'd have lost the old one. The teacher says it plainly: point prev at curr first, and only then move curr to temp.
Doubt: why return prev, not curr or head?
→ When the loop ends, curr is None (we walked off the end), and the old head (1) is now the last node pointing to None. prev holds the last node we flipped, 4, which is the new first node.

Her advice: once you understand these three pointers, you don't need to memorise the code; the steps come naturally.

5Steps

  1. prev = None, curr = head.
  2. While curr: save, flip, move prev, move curr.
  3. Return prev.

6Python template

Template · reverse a list (prev / curr / temp)
def reverseList(head):
    prev = None
    curr = head
    while curr is not None:
        temp = curr.next      # 1. save the rest
        curr.next = prev      # 2. flip the arrow backwards
        prev = curr           # 3. prev steps onto curr
        curr = temp           # 4. curr steps onto the saved rest
    return prev               # the old last node is the new head

7Line by line

linewhat it means
prev = NoneNothing is behind the first node; after reversal it becomes the tail, pointing to None.
temp = curr.nextHold onto the rest of the list before we cut the link.
curr.next = prevThe actual reversal of one arrow.
prev = curr curr = tempShift the window one node forward, in this order.
return prevNew head.

8Dry run on 1 → 2 → 3 → 4

stepcurrtemparrow flippedreversed part (from prev)rest (from temp)
1[1][2]1.next: [2] → None1 → None2 → 3 → 4
2[2][3]2.next: [3] → [1]2 → 1 → None3 → 4
3[3][4]3.next: [4] → [2]3 → 2 → 1 → None4
4[4]None4.next: None → [3]4 → 3 → 2 → 1 → None(empty)
endNonereturn prev = [4]
after step 1
None ← [1]   [2] → [3] → [4] → None
        ↑     ↑
      prev   curr
after step 2
None ← [1] ← [2]   [3] → [4] → None
              ↑     ↑
            prev   curr
after step 4
None ← [1] ← [2] ← [3] ← [4]   None
                          ↑     ↑
                        prev   curr

9Complexity & remember

Remember (reversal) prev = None, curr = head. Save → flip → prev = curr → curr = temp. Return prev. Used in: reverse list, reverse in pairs, reverse in k groups, and many more.

Part E · Pattern 4: linked list + stack

1The idea in simple words

If we already have the linked list, why bring in a stack? The teacher first explains two words:

A stack is last-in, first-out: you push items on top and pop from the top. If you push the list's values from left to right, they pop out right to left. That is "walking backwards" without touching the list.

2When to use it (her two signals)

3Intuition

Sticky notes on a pile: walk the list writing one value per note and dropping it on the pile. Then take notes off the top: you're reading the list backwards.

4Building it from her example: adding two numbers

Two lists each hold the digits of a number, most significant digit first. Add them and return the sum as a list.

head1: [1] → [2] → [3] → [4] → None      (the number 1234)
head2: [6] → None                        (the number 6)
answer: [1] → [2] → [4] → [0] → None     (1240)

We can't add 1 and 6 at the heads; addition starts from the right (units digit). To reach 4 we walk to the end. Say 4 + 6 = 10: we write 0 and carry 1 to the left. But in a linked list we can't move left without reversing. That's the moment to use stacks.

  1. Walk list 1 and push every value: stack1 = [1, 2, 3, 4] (4 on top).
  2. Walk list 2: stack2 = [6].
  3. Pop 4 and 6: 4 + 6 = 10 → digit 0, carry 1.
  4. Stack2 is empty. Pop 3: 3 + carry 1 = 4 → digit 4, carry 0.
  5. Pop 2 → digit 2. Pop 1 → digit 1.
  6. Digits came out as 0, 4, 2, 1 (units first). Reverse → 1, 2, 4, 0. That's the answer.
Doubt 1: what if there's still a carry when both stacks are empty, e.g. 5 + 5?
→ (My addition.) The carry becomes one more digit at the front: 5 + 5 = 10 → 1 → 0. So the loop must keep going while either stack has items or the carry isn't 0.
Doubt 2: how do I "reverse" the digits cheaply when building the answer list?
→ Insert each new digit at the front of the answer list (head = ListNode(d, head)). The units digit is created first and ends up last; the final digit created ends up as the head. No separate reversal step.
Doubt 3: is this the same as "Add Two Numbers" (problem 24 on the sheet)?
→ Not exactly (my note). LeetCode 2 stores the digits in reverse (units digit at the head), so there you can add straight from the heads with no stack. The version she draws here, digits in normal order, is LeetCode 445 "Add Two Numbers II", which is the classic stack problem. The template below solves that one.

5Steps

  1. Push all values of list 1 into stack1, and of list 2 into stack2.
  2. While stack1 or stack2 or carry: total = carry + popped values; digit = total % 10; carry = total // 10.
  3. Put each digit at the front of the answer list. Return its head.

6Python template

Template · add two numbers with two stacks
def addTwoNumbers(l1, l2):
    s1, s2 = [], []
    while l1 is not None:          # push list 1, left to right
        s1.append(l1.val)
        l1 = l1.next
    while l2 is not None:          # push list 2
        s2.append(l2.val)
        l2 = l2.next
    head = None
    carry = 0
    while s1 or s2 or carry:       # right to left, like on paper
        total = carry
        if s1:
            total += s1.pop()
        if s2:
            total += s2.pop()
        head = ListNode(total % 10, head)   # new digit goes in front
        carry = total // 10
    return head

7Line by line

linewhat it means
s1.append(l1.val)Push; the last digit (units) ends up on top.
while s1 or s2 or carry:Keep adding while either number has digits left, or a carry is waiting.
if s1: total += s1.pop()The shorter number simply runs out; we treat missing digits as 0.
head = ListNode(total % 10, head)Keep the last digit of the sum; add it at the front, which builds the answer in the right order.
carry = total // 100 or 1, carried to the next column on the left.

8Dry run: 1234 + 6

roundpoppedtotaldigitcarryanswer list so far
14, 610010
23, –3 + 1 = 4404 → 0
32, –2202 → 4 → 0
41, –1101 → 2 → 4 → 0
stack1 before round 1
1234
stack2 before round 1
6
stack1 after round 2
12

Top of the stack in red.

9Complexity & remember

Remember (list + stack) Need to go right-to-left or backtrack, and extra space is allowed → push values on a stack, pop to read backwards. In place (reverse the list) = O(1) space; with a stack = O(n) space.

Part F · Pattern 5: merge & sort

1The idea in simple words

The signal is the word sorted: the question says the lists are sorted, or asks you to sort one. Then the merge step from merge sort is the tool: compare the front nodes of two sorted lists, connect the smaller one, move forward, repeat.

2When to use it

3Intuition

Two sorted piles of numbered cards, face up. Always take the smaller of the two top cards and add it to your output pile. When one pile runs out, put the whole other pile on the end; it's already sorted.

4Building it from her example

Lists 1 → 3 → 5 and 2 → 4 → 6, with curr1 on 1 and curr2 on 2.

Result: 1 → 2 → 3 → 4 → 5 → 6. Her summary: compare two nodes, see which is smaller, connect it, move forward.

Doubt: "connect it" to what? At the start there's no result list yet.
→ (My addition, the standard trick.) Use a dummy node as a fake start and a tail pointer on the last node of the result. Every step: tail.next = smaller node, then tail = tail.next. At the end, the real head is dummy.next. This saves us from a special "which node is the first?" check. See Part G.
Doubt: how does merging help with sorting one list?
→ (My addition.) Merge sort: cut the list in half at the middle (that's Pattern 2, fast & slow), sort each half the same way (recursion), then merge the two sorted halves. A list of 0 or 1 nodes is already sorted, which stops the recursion.

5Steps (merge)

  1. dummy = ListNode(0), tail = dummy.
  2. While both lists have nodes: attach the smaller front node to tail, advance that list, advance tail.
  3. Attach whatever is left of either list. Return dummy.next.

6Python templates

Template · merge two sorted lists (dummy + tail)
def mergeTwoLists(l1, l2):
    dummy = ListNode(0)              # fake start of the result
    tail = dummy                     # last node of the result so far
    curr1, curr2 = l1, l2
    while curr1 is not None and curr2 is not None:
        if curr1.val <= curr2.val:   # take the smaller front node
            tail.next = curr1
            curr1 = curr1.next
        else:
            tail.next = curr2
            curr2 = curr2.next
        tail = tail.next
    tail.next = curr1 if curr1 is not None else curr2   # leftover is already sorted
    return dummy.next
Template · sort a list (merge sort = middle + merge)
def sortList(head):
    if head is None or head.next is None:    # 0 or 1 node: already sorted
        return head
    slow, fast = head, head.next             # fast one ahead: slow stops at the FIRST middle
    while fast is not None and fast.next is not None:
        slow = slow.next
        fast = fast.next.next
    second = slow.next                       # start of the right half
    slow.next = None                         # cut the list in two
    return mergeTwoLists(sortList(head), sortList(second))

7Line by line

linewhat it means
dummy = ListNode(0) tail = dummyThe result list always has a "last node" to attach to, even before we've attached anything.
if curr1.val <= curr2.val:Pick the smaller front node (<= keeps equal values in their original order).
tail.next = curr1 … curr1 = curr1.nextReuse the existing node (no copying) and move that list forward.
tail.next = curr1 if … else curr2One list is empty; the other's remaining nodes are already sorted, so attach them in one go.
slow, fast = head, head.nextWith 2 nodes this makes slow stop on the first one, so the halves are 1 + 1. If fast started on head, slow would stop on the second node and a 2-node list would never split (endless recursion).
slow.next = NoneActually cut the list, so each half ends with None.

8Dry run: merge 1 → 3 → 5 with 2 → 4 → 6

stepcurr1curr2comparewhich .next changesresult after dummy
1[1][2]1 ≤ 2dummy.next = [1]1
2[3][2]3 > 21.next = [2]1 → 2
3[3][4]3 ≤ 42.next = [3]1 → 2 → 3
4[5][4]5 > 43.next = [4]1 → 2 → 3 → 4
5[5][6]5 ≤ 64.next = [5]1 → … → 5
6None[6]loop ends5.next = [6] (leftover)1 → 2 → 3 → 4 → 5 → 6
after step 2
dummy → [1] → [2]
               ↑
              tail
curr1: [3] → [5] → None
curr2: [4] → [6] → None
after step 6
dummy → [1] → [2] → [3] → [4] → [5] → [6] → None
         ↑
    dummy.next = answer

9Complexity & remember

Remember (merge & sort) "Sorted" in the question → merge. Compare fronts, attach the smaller to tail, move on, attach the leftover. Sort = split at the middle + sort halves + merge.

Part G · Two extra tools the sheet uses

The teacher's video lists five patterns. These two tools aren't separate patterns in her list, but many sheet problems lean on them, so here they are in one place.

1The dummy node

Many operations treat the head differently: deleting the head, or building a new list where you don't yet know the first node. A dummy node is a fake node placed before the head (dummy.next = head). Now every real node, the head included, has a node in front of it, so one rule handles all of them. At the end, return dummy.next (the real head, which may have changed).

Template · remove all nodes with a value (dummy node)
def removeElements(head, val):
    dummy = ListNode(0, head)        # fake node in front of the head
    curr = dummy
    while curr.next is not None:
        if curr.next.val == val:
            curr.next = curr.next.next   # skip it (works for the head too)
        else:
            curr = curr.next
    return dummy.next                # the real head, maybe a new one
remove 1 from  [1] → [2] → [1] → None

dummy → [1] → [2] → [1] → None
 ↑
curr     curr.next is 1 → skip:  dummy → [2] → [1] → None
         then move to [2], skip the last 1 → dummy → [2] → None
answer = dummy.next = [2]

Without the dummy, you'd need a separate loop for "while the head itself has the value". With it, there's no special case.

2Recursion on lists

The teacher solves every problem in the playlist both with a loop and with recursion. The shape is always: base case at None, handle this node, call yourself on head.next. Each call keeps its own head on the call stack. It costs O(n) stack space (and in Python, more than about 1000 nodes hits the recursion limit), so the loop is usually the better submission.

Template · length with recursion
def length_rec(head):
    if head is None:                 # empty list has length 0
        return 0
    return 1 + length_rec(head.next) # this node + the length of the rest
length_rec on 1 → 2 → 3, deepest
f(1) waits: 1 + ?f(2) waits: 1 + ?f(3) waits: 1 + ?f(None) → 0
coming back
f(1) = 1 + 2 = 3f(2) = 1 + 1 = 2

Part H · Which sheet problem uses which pattern

The teacher names some problems herself (marked ★). The rest is my mapping, based on how each problem is usually solved; some problems combine two patterns.

patternsheet problems
1 · basic traversal★ count nodes / length, ★ insert at a position, ★ delete at a position → 2 Design Linked List, 3 Search & Insert, 5 Delete Node, 4 Intersection of Two Lists, 6 Odd Even List, 19 Remove Duplicates, 16 Rotate List
2 · fast & slow★ middle, ★ cycle → 7 Middle of the List, 8 Linked List Cycle, 9 Cycle II, 10 Nth from End (a fixed gap instead of two speeds)
3 · reversal★ reverse a list, ★ reverse in pairs, ★ reverse in k groups → 11 Reverse List, 14 Reverse List II, 15 Swap Pairs, 17 Reverse k-Group; with fast & slow: 12 Palindrome, 13 Max Twin Sum
4 · list + stack★ add two numbers → 24 Add Two Numbers (see Doubt 3 in Part E), 25 Remove Nodes (a stack of "bigger" nodes), and the extra-space version of 12 Palindrome
5 · merge & sort★ merge two sorted, ★ merge k sorted, ★ sort list, ★ reorder list → 18 Merge Two Sorted, 26 Merge K Sorted, 20 Sort List, 21 Reorder List (middle + reverse + merge)
dummy node18, 22 Remove Duplicates II, 23 Partition List, 24, 14

Part I · Revision page

patternsignal in the questionpointers / toolscore movetime / space
traversalcount, length, find, insert / delete at a positioncurrcurr = curr.next until NoneO(n) / O(1)
fast & slowmiddle, cycleslow (1 step), fast (2 steps)while fast and fast.nextO(n) / O(1)
reversalreverse, go backwards in place, pairs, k groupsprev, curr, tempsave → flip → move prev → move currO(n) / O(1)
list + stackneed right-to-left or backtracking, extra space OKstack(s)push all, pop to read backwardsO(n) / O(n)
merge & sort"sorted" lists, or "sort the list"curr1, curr2, dummy, tailattach the smaller front, move onO(n + m) / O(1); sort O(n log n)
If you remember only 5 lines 1. Node = value + next. No index; you can only walk forward from the head.
2. Linked lists beat arrays at insert/delete (no shifting) and size (grows freely); arrays beat them at indexing.
3. slow 1 step, fast 2 steps → middle and cycle.
4. Reversal: prev / curr / temp; save before you flip. It unlocks about half the questions.
5. Need to go backwards with extra space → stack. See "sorted" → merge.
Mistakes to avoid ✗ reading .next or .val of None (check curr / fast / fast.next first)
✗ starting count at 0 when you count jumps (one short)
✗ flipping curr.next before saving it (rest of the list lost)
✗ moving curr before prev in reversal
✗ comparing nodes by value (==) in cycle detection instead of identity (is)
✗ forgetting the final carry in add two numbers
✗ moving head when you still need to return the list
test it yourself (paste under the templates above)
def build(vals):
    head = None
    for v in reversed(vals):
        head = ListNode(v, head)
    return head

def to_list(head):
    out = []
    while head:
        out.append(head.val)
        head = head.next
    return out

print(length(build([1, 2, 3, 4])), length_by_jumps(build([1, 2, 3, 4])))   # 4 4
print(middleNode(build([1, 2, 3])).val, middleNode(build([1, 2, 3, 4])).val)   # 2 3
loop = build([1, 2, 3, 4]); loop.next.next.next.next = loop.next          # 4 -> back to 2
print(hasCycle(loop), hasCycle(build([1, 2, 3, 4])))                    # True False
print(to_list(reverseList(build([1, 2, 3, 4]))))                         # [4, 3, 2, 1]
print(to_list(addTwoNumbers(build([1, 2, 3, 4]), build([6]))))           # [1, 2, 4, 0]
print(to_list(mergeTwoLists(build([1, 3, 5]), build([2, 4, 6]))))        # [1, 2, 3, 4, 5, 6]
print(to_list(sortList(build([4, 1, 3, 2]))))                            # [1, 2, 3, 4]
print(to_list(removeElements(build([1, 2, 1]), 1)), length_rec(build([1, 2, 3])))   # [2] 3

Based on this video: Linked List Patterns | concept overview