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
- Part A · Array vs linked list: why we need it
- Part B · Pattern 1: basic traversal
- Part C · Pattern 2: fast & slow pointers
- Part D · Pattern 3: reversal with three pointers
- Part E · Pattern 4: linked list + stack
- Part F · Pattern 5: merge & sort
- Part G · Two extra tools the sheet uses (dummy node, recursion)
- Part H · Which sheet problem uses which pattern
- Part I · Revision page
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.
| linear | non-linear | |
|---|---|---|
| from one element you can move to… | only one next element | several elements |
| examples | arrays, strings, linked lists | trees (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:
- a value (the data), and
- a next pointer: the memory address of the next node. If there is no next node, it holds null (
Nonein Python), meaning "the chain ends here".
In the drawing, the address is shown as an arrow:
[1] → [2] → [3] → [4] → None ↑ head
class ListNode:
def __init__(self, val=0, next=None):
self.val = val # the data
self.next = next # the next node, or None at the endHead, walking, no index
- head: a pointer to the first node. Everything is reached from it.
- Walking:
curr = curr.nextmoves one node forward (head.next "means" the node 2 in the picture). - No indexing: the teacher stresses that you can't jump to "the 3rd element" like in an array or string. To reach position
iyou walkisteps, which is O(n) in the worst case. - One direction only: from node 2 you can go to 3, but never back to 1, because nothing in node 2 points backwards.
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
→ 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.
| array | linked list | |
|---|---|---|
| memory | one unbroken block | nodes anywhere, linked by addresses |
| size | fixed | grows / shrinks freely |
| insert / delete at a known spot | O(n), items shift | O(1), change 1–2 arrows |
| reach the i-th item | O(1) indexing | O(i), walk from head |
| extra memory per item | none | one 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
- Any question that needs to "look at every node" or "reach position k".
- Because there is no indexing, this walk is how you reach anything.
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).
→ 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.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
count = 0,curr = head.- While curr is not None: count += 1, curr = curr.next.
- Return count.
6Python template
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 countdef 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 count7Line by line
| line | what it means |
|---|---|
| while curr is not None: | Visit every node, the last one included. Ends with curr on None. |
| count += 1 | The work done at each node. Swap this line for other jobs (compare with a key, sum values, …). |
| curr = curr.next | The 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
| step | curr (start-at-0 version) | count | curr (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) |
| 4 | None → stop | 4 | 4 |
9Complexity & remember
- Time O(n), space O(1).
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
- Finding the middle of the list in one pass.
- Detecting a cycle (a list whose last node points back into the list instead of to None).
- (Later on the sheet, related ideas: start of a cycle, n-th node from the end, palindrome, twin sums, reorder list.)
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.
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)
- After move 1: slow on 2, fast on 3.
- After move 2: slow on 3, fast on 2 (3 → 4 → back to 2).
- After move 3: slow on 4, fast on 4 (2 → 3 → 4). They meet → cycle present.
→ (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
slow = fast = head.- While fast and fast.next exist: slow one step, fast two steps.
- Middle: return slow when the loop ends. Cycle: return True the moment
slow is fast; if the loop ends, return False.
6Python templates
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 evendef 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 cycle7Line by line
| line | what it means |
|---|---|
| slow = fast = head | Both 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.next | Speeds 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)
| move | slow | fast | fast's path this move | met? |
|---|---|---|---|---|
| start | [1] | [1] | – | (not checked) |
| 1 | [2] | [3] | 1 → 2 → 3 | no |
| 2 | [3] | [2] | 3 → 4 → 2 (back-arrow) | no |
| 3 | [4] | [4] | 2 → 3 → 4 | yes → 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
- Time O(n), space O(1): just two pointers, no set of visited nodes needed.
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
- "Reverse a linked list", "reverse in pairs" (
1 2 3 4 → 2 1 4 3), "reverse in groups of k". She names these as very common interview questions. - Whenever you need to go backwards but must not use extra space (compare with Pattern 4).
3Intuition: the three pointers
- prev: the node behind us; it's where the current node's arrow should point after flipping. At the start there's nothing behind node 1, so prev = None (and that's why 1 ends up pointing to None).
- curr: the node we're standing on, whose arrow we flip now.
- temp (often called next): when we flip
curr.nextbackwards, we lose our only link to the rest of the list. So before flipping, we save it in temp.
4Building it from her example
Standing on 1 (prev = None):
- Save:
temp = curr.next(temp = 2). Otherwise, after the flip, 2 3 4 would be unreachable. - Flip:
curr.next = prev(1 now points to None). - Move prev:
prev = curr(prev = 1). - 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.
→ 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.→ 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
prev = None,curr = head.- While curr: save, flip, move prev, move curr.
- Return prev.
6Python template
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 head7Line by line
| line | what it means |
|---|---|
| prev = None | Nothing is behind the first node; after reversal it becomes the tail, pointing to None. |
| temp = curr.next | Hold onto the rest of the list before we cut the link. |
| curr.next = prev | The actual reversal of one arrow. |
| prev = curr curr = temp | Shift the window one node forward, in this order. |
| return prev | New head. |
8Dry run on 1 → 2 → 3 → 4
| step | curr | temp | arrow flipped | reversed part (from prev) | rest (from temp) |
|---|---|---|---|---|---|
| 1 | [1] | [2] | 1.next: [2] → None | 1 → None | 2 → 3 → 4 |
| 2 | [2] | [3] | 2.next: [3] → [1] | 2 → 1 → None | 3 → 4 |
| 3 | [3] | [4] | 3.next: [4] → [2] | 3 → 2 → 1 → None | 4 |
| 4 | [4] | None | 4.next: None → [3] | 4 → 3 → 2 → 1 → None | (empty) |
| end | None | return prev = [4] |
None ← [1] [2] → [3] → [4] → None
↑ ↑
prev currNone ← [1] ← [2] [3] → [4] → None
↑ ↑
prev currNone ← [1] ← [2] ← [3] ← [4] None
↑ ↑
prev curr9Complexity & remember
- Time O(n), space O(1): the list is changed in place.
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:
- In place: you solve the problem by modifying the list itself (like reversal), with no extra data structure → O(1) extra space.
- Not in place: you copy values into an extra structure, such as a stack, and work there → O(n) extra space.
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)
- You need to come back (go right to left), you'd otherwise have to reverse the list, and extra space is allowed → think stack.
- You need to backtrack: you've already moved 1 → 2 → 3 and now realise you must go back and check something → think stack.
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.
- Walk list 1 and push every value: stack1 = [1, 2, 3, 4] (4 on top).
- Walk list 2: stack2 = [6].
- Pop 4 and 6: 4 + 6 = 10 → digit 0, carry 1.
- Stack2 is empty. Pop 3: 3 + carry 1 = 4 → digit 4, carry 0.
- Pop 2 → digit 2. Pop 1 → digit 1.
- Digits came out as 0, 4, 2, 1 (units first). Reverse → 1, 2, 4, 0. That's the answer.
→ (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.→ 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.→ 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
- Push all values of list 1 into stack1, and of list 2 into stack2.
- While stack1 or stack2 or carry: total = carry + popped values; digit = total % 10; carry = total // 10.
- Put each digit at the front of the answer list. Return its head.
6Python template
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 head7Line by line
| line | what 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 // 10 | 0 or 1, carried to the next column on the left. |
8Dry run: 1234 + 6
| round | popped | total | digit | carry | answer list so far |
|---|---|---|---|---|---|
| 1 | 4, 6 | 10 | 0 | 1 | 0 |
| 2 | 3, – | 3 + 1 = 4 | 4 | 0 | 4 → 0 |
| 3 | 2, – | 2 | 2 | 0 | 2 → 4 → 0 |
| 4 | 1, – | 1 | 1 | 0 | 1 → 2 → 4 → 0 |
Top of the stack in red.
9Complexity & remember
- Time O(n + m) (both lists once). Space O(n + m) for the stacks: this is the "not in place" price.
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
- Merge: merge two sorted lists, merge k sorted lists. She notes these are asked often at Amazon and many other big companies.
- Sort: rearrange one list so it ends up sorted (Sort List), or rearrange it in a given order (Reorder List).
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.
- 1 vs 2: 1 is smaller → keep 1 connected, move curr1 to 3.
- 3 vs 2: 2 is smaller → connect 2 after 1, move curr2 to 4.
- 3 vs 4: 3 is smaller → connect 3, move curr1 to 5.
- 5 vs 4 → 4; then 5 vs 6 → 5; list 1 runs out → attach the rest (6).
Result: 1 → 2 → 3 → 4 → 5 → 6. Her summary: compare two nodes, see which is smaller, connect it, move forward.
→ (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.→ (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)
- dummy = ListNode(0), tail = dummy.
- While both lists have nodes: attach the smaller front node to tail, advance that list, advance tail.
- Attach whatever is left of either list. Return dummy.next.
6Python templates
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.nextdef 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
| line | what it means |
|---|---|
| dummy = ListNode(0) tail = dummy | The 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.next | Reuse the existing node (no copying) and move that list forward. |
| tail.next = curr1 if … else curr2 | One list is empty; the other's remaining nodes are already sorted, so attach them in one go. |
| slow, fast = head, head.next | With 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 = None | Actually cut the list, so each half ends with None. |
8Dry run: merge 1 → 3 → 5 with 2 → 4 → 6
| step | curr1 | curr2 | compare | which .next changes | result after dummy |
|---|---|---|---|---|---|
| 1 | [1] | [2] | 1 ≤ 2 | dummy.next = [1] | 1 |
| 2 | [3] | [2] | 3 > 2 | 1.next = [2] | 1 → 2 |
| 3 | [3] | [4] | 3 ≤ 4 | 2.next = [3] | 1 → 2 → 3 |
| 4 | [5] | [4] | 5 > 4 | 3.next = [4] | 1 → 2 → 3 → 4 |
| 5 | [5] | [6] | 5 ≤ 6 | 4.next = [5] | 1 → … → 5 |
| 6 | None | [6] | loop ends | 5.next = [6] (leftover) | 1 → 2 → 3 → 4 → 5 → 6 |
dummy → [1] → [2]
↑
tail
curr1: [3] → [5] → None
curr2: [4] → [6] → Nonedummy → [1] → [2] → [3] → [4] → [5] → [6] → None
↑
dummy.next = answer9Complexity & remember
- Merge: time O(n + m), space O(1) (nodes are reused).
- Sort list: time O(n log n) (log n levels of halving, O(n) merging per level), space O(log n) for the recursion.
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).
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 oneremove 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.
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 restPart 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.
| pattern | sheet 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 node | 18, 22 Remove Duplicates II, 23 Partition List, 24, 14 |
Part I · Revision page
| pattern | signal in the question | pointers / tools | core move | time / space |
|---|---|---|---|---|
| traversal | count, length, find, insert / delete at a position | curr | curr = curr.next until None | O(n) / O(1) |
| fast & slow | middle, cycle | slow (1 step), fast (2 steps) | while fast and fast.next | O(n) / O(1) |
| reversal | reverse, go backwards in place, pairs, k groups | prev, curr, temp | save → flip → move prev → move curr | O(n) / O(1) |
| list + stack | need right-to-left or backtracking, extra space OK | stack(s) | push all, pop to read backwards | O(n) / O(n) |
| merge & sort | "sorted" lists, or "sort the list" | curr1, curr2, dummy, tail | attach the smaller front, move on | O(n + m) / O(1); sort O(n log n) |
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.
.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 listdef 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] 3Based on this video: Linked List Patterns | concept overview