Hi👋SpeedAlgo • Deliberate Practice & Cognitive Ergonomics for Software Engineers

An interactive algorithm mastery and technical interview preparation platform published by Hi👋WebEnterprise. Built for senior and staff software engineers preparing for rigorous coding screens at top tech companies (FAANG/MAMAA).

13 Core Algorithmic Patterns & 225 Practice Problems

  • 1. Two Pointers (16 Paradigms, 43 Problems): Opposite ends, sorted pair sums, palindrome verification, trapping rain water, 3Sum, container with most water — includes the Sliding Window and Fast & Slow Pointers paradigms (contiguous subarray invariants, longest substrings, Floyd's cycle detection, linked list middle).
  • 2. Binary Search (9 Paradigms, 14 Problems): Monotonic predicate partitioning (feasible(x)), boundary search, rotated sorted arrays, square roots, minimum ship capacity, median of two sorted arrays, matrix median on value range.
  • 3. Bit Manipulation (5 Paradigms, 11 Problems): Counting set bits, XOR cancellation, bit reversal, bitmask pairing, maximum XOR with a bitwise trie.
  • 4. Math & Geometry (8 Paradigms, 14 Problems): Sieve of Eratosthenes, integer/roman conversions, modular arithmetic, geometric simulation.
  • 5. Tree/Graph Depth-First Search (15 Paradigms, 24 Problems): Path sums, lowest common ancestor, tree diameter, validating BSTs, tree DP (House Robber III, Binary Tree Cameras).
  • 6. Tree/Graph Breadth-First Search (5 Paradigms, 12 Problems): Level-order traversals, shortest path in unweighted graphs, zig-zag traversals, rotting oranges, word ladders.
  • 7. Graphs (12 Paradigms, 22 Problems): Topological sort, cycle detection, Dijkstra's algorithm, bipartite graph validation, network delay.
  • 8. Backtracking (6 Paradigms, 14 Problems): Subsets, permutations, combinations, constraint satisfaction, pruning, N-Queens, Sudoku solver.
  • 9. Dynamic Programming (13 Paradigms, 28 Problems): Memoization vs tabulation, 0/1 knapsack, unbounded knapsack, coin change, edit distance, longest common subsequence.
  • 10. Heap / Priority Queue (9 Paradigms, 11 Problems): Running medians, top-k elements, interval scheduling, IPO, k-way merges.
  • 11. Advanced Data Structures (7 Paradigms, 17 Problems): Trie (prefix tree), Union-Find (Disjoint Set Union), LRU Cache, LFU Cache, range-sum queries with updates.
  • 12. Stack (5 Paradigms, 9 Problems): Next greater element, largest rectangle in histogram, remove k digits, decode string, min stack.
  • 13. Intervals & Greedy (2 Paradigms, 6 Problems): Merge intervals, interval scheduling, meeting rooms by sweep line, jump game.

4-Stage Deliberate Practice Framework

  1. Stage 1 (Compare & Learn): Read the pattern's mental model, loop invariant and Big-O proof, compare the pattern's template with the problem's solution side by side, and use the step-by-step debugger. The solution can also be shown in C#, Java, TypeScript, C++, Go or Rust for almost every problem.
  2. Stage 2 (Active Recall): Rebuild the solution from memory in a code editor, with an invariant checklist and optional progressive hints, and check it against a judged test suite.
  3. Stage 3 (AI Interview Coach, coming soon): A mock-interview stage still in development; for now, students go straight from Stage 2 to Stage 4.
  4. Stage 4 (Solve on Your Own): Solve a new problem on your own against a 15-minute timer; your code is graded against a test suite. Python and TypeScript run in your browser and C# runs on our server (design problems grade in Python and TypeScript).

Equipped with a spaced-repetition review hub on a fixed 7-step schedule (1 day up to 90 days), a two-pane Studio workspace, and study notes.

Pricing, Access & Commercial Terms

  • Core Curriculum: 100% Free. No credit card required.
  • Coins: New accounts get 40 free coins once, the daily claim adds +20 coins once a day, and inviting a friend earns you +25 coins when they complete their first practice step. These are the only ways to earn coins.
  • AI Coach: Each AI coach question costs 1-5 coins, priced by the server according to its size.
  • BYOK (Bring Your Own Key): AI coaching is free with your own Google Gemini, OpenAI or OpenRouter API key (BYOK).
  • Extras: Coins also unlock extras for 24 hours: a Recall hint level (1 coin), a Solve solution reveal (2), a LeetCode problem card's solutions (1) and a pattern's extra canonical problems (1).
  • Payments & Refunds: Nothing on the site is sold for money: coins can only be earned, never bought. There are no subscriptions or charges, so there is nothing to refund.
  • Platform Operator: Hi👋WebEnterprise. Support & policies at hispeedalgo.com.
Skip to main content
Hi👋SpeedAlgo

Invariant-First Algorithmic Mastery

Arena
238Items
Theory Context•Two Pointers & Sliding Window
P0EasyMental model~4 min speedrun

Sentinel / Dummy Node

Unlocks
Linked List ReversalLRU Cache
The problem

Remove Linked List Elements (LC 203)(opens leetcode.com)

You get the head of a singly linked list and an integer val. Remove every node whose value equals val, and return the head of the list that is left (which may be empty).

Example 1
Input:head = [1,2,6,3,4,5,6], val = 6
Output:[1,2,3,4,5]
Both nodes holding value 6 are removed, including one in the interior and one at the tail.
1263456

Dummy Node

list whose head may change ➔ dummy.next = head ➔ rewire through prev, return dummy.next
Spot it when: The head itself may be removed, replaced or not known yet: deleting nodes, merging lists, building a new list, or rewiring a sublist that can start at the head. The same signal appears in arrays: the first or the last element, or the gap before the first and after the last, needs its own special case.
Dummy Node
Python
1from typing import Optional
2 
3class ListNode:
4 def __init__(self, val: int = 0, next: Optional["ListNode"] = None):
5 self.val = val
6 self.next = next
7 
8def remove_elements(head: Optional[ListNode], val: int) -> Optional[ListNode]:
9 """Remove Linked List Elements: a dummy head makes deleting the real head ordinary.
10 Time: O(N) | Space: O(1)
11 """
12 dummy = ListNode(0, head) # Every real node now has a previous node
13 prev = dummy
14 
15 # 1. Walk And Unlink Matching Nodes:
16 # prev always stays on the last kept node, so prev.next = prev.next.next removes the head the same way it removes any other node, and staying put after a removal lets the new prev.next be checked too.
17 while prev.next:
18 if prev.next.val == val:
19 prev.next = prev.next.next
20 else:
21 prev = prev.next
22 
23 # 2. Return Through The Dummy, Not head:
24 # head itself may have been removed, so dummy.next, not head, is always the current first real node.
25 return dummy.next
Invariant: Every node before prev.next has been checked and kept, and dummy.next is always the current head.

The head is the only node without a previous node, which is why deleting or inserting at the head usually needs its own if. A dummy node placed before head gives every real node a previous node, so prev.next = prev.next.next removes the head like any other node. The sentinel does not have to be a node. It can be a virtual index, value or bar that is never stored in the data. It gives the first and the last element the same rule as every other element. Missing Ranges (LC 163) starts with prev = lower - 1 and ends with curr = upper + 1, so the gap before the first number and the gap after the last number use the same check as every gap between two numbers. Largest Rectangle in Histogram (LC 84) adds a bar of height 0 at the end, so the bars still on the stack are handled by the same rule. The trap is returning head: when the first node is removed, head still points at the deleted node, so return dummy.next.

See it run

Remove Linked List Elements: remove_elements([6, 6, 1, 2, 6, 3], val = 6)

prev dummy · prev.next 6

prev dummy · prev.next 6

prev dummy · prev.next 6

prev dummy · prev.next 6

prev dummy · prev.next 1

prev dummy · prev.next 1

prev 1 · prev.next 2

prev 1 · prev.next 2

prev 2 · prev.next 6

prev 2 · prev.next 6

prev 2 · prev.next 3

prev 2 · prev.next 3

prev 3 · prev.next None

prev 3 · prev.next None

prev 3 · prev.next None · return head [6, 6, 1, 2, 3]

result [1, 2, 3]

Each circle is a node, and D is the dummy node. Arrows are next links. A removed node disappears from the picture and appears in the removed list.

  • prev the last node that was kept
  • prev.next the node that is checked now
  • new link the link that was just written
  • dummy.next the first kept node, which the function returns

dummy.next = head, so the first node has a previous node, the dummy D. prev starts at D.

prev.next.val is 6, which equals val = 6. This node must be removed.

prev.next = prev.next.next links dummy to 6. prev stays where it is, so the new prev.next is checked next.

prev.next.val is 6, which equals val = 6. This node must be removed.

prev.next = prev.next.next links dummy to 1. prev stays where it is, so the new prev.next is checked next.

prev.next.val is 1, which differs from val = 6. This node is kept.

prev moves forward to 1.

prev.next.val is 2, which differs from val = 6. This node is kept.

prev moves forward to 2.

prev.next.val is 6, which equals val = 6. This node must be removed.

prev.next = prev.next.next links 2 to 3. prev stays where it is, so the new prev.next is checked next.

prev.next.val is 3, which differs from val = 6. This node is kept.

prev moves forward to 3.

prev.next is None, so the loop stops.

The variable head still points at the first input node, 6, which was removed. Returning head would return a list that starts with 6.

return dummy.next gives the first kept node. The list is [1, 2, 3].

Dprev6prev.next61263
removed
empty
return head
empty
return dummy.next
empty
1 / 16
Gotchas1 of 5

Gotcha 1 of 5

  1. 1
    The trap

    Gotcha 1: Returning head instead of dummy.next

    Mistake
    return head
    Fix
    return dummy.next

    What happens: head still points at the first node of the input. When that node holds val, the loop unlinks it from dummy, but head does not change. Returning head returns a list that starts with a deleted node.

    Example: head = [6, 6, 1, 2, 6, 3], val = 6
    Mistake: returns [6, 6, 1, 2, 3]Fix: returns [1, 2, 3]
  2. 2

    Gotcha 2: Moving prev forward after a deletion

    Mistake
    if prev.next.val == val:
        prev.next = prev.next.next
    prev = prev.next
    Fix
    if prev.next.val == val:
        prev.next = prev.next.next
    else:
        prev = prev.next

    What happens: After a deletion, prev.next is a new node that has not been checked. Moving prev forward skips that check. When two nodes in a row hold val, the second one stays in the list.

    Example: head = [1, 6, 6, 2], val = 6
    Mistake: returns [1, 6, 2]Fix: returns [1, 2]
  3. 3

    Gotcha 3: Starting the two pointers at head instead of dummy

    What happens: Remove Nth Node From End of List (LC 19) moves a leading pointer ahead, then moves both pointers together. The trailing pointer must stop at the node before the one to remove. When the head itself must go, only dummy sits before it. Starting at head leaves no node before the head.

  4. 4

    Gotcha 4: Starting prev at lower instead of lower - 1

    Mistake
    prev = lower
    Fix
    prev = lower - 1

    What happens: prev stands for the number just before the first number that must be covered. That number is lower - 1. When prev is lower, the check curr - prev > 1 treats lower itself as already present. If lower is missing, the gap that starts at lower loses its first number.

    Example: nums = [2, 3], lower = 0, upper = 5
    Mistake: returns [[1, 1], [4, 5]]Fix: returns [[0, 1], [4, 5]]
  5. 5

    Gotcha 5: Using upper instead of upper + 1 as the final curr

    Mistake
    curr = nums[i] if i < len(nums) else upper
    Fix
    curr = nums[i] if i < len(nums) else upper + 1

    What happens: The final curr is a virtual number just after upper. It makes the last gap end at curr - 1, which is upper. When the code uses upper, the last gap ends at upper - 1, so upper is never reported as missing.

    Example: nums = [0, 1, 3, 50, 75], lower = 0, upper = 99
    Mistake: returns [[2, 2], [4, 49], [51, 74], [76, 98]]Fix: returns [[2, 2], [4, 49], [51, 74], [76, 99]]
Not this one if…
  • Every next pointer must point the other way, as in Reverse Linked List (LC 206): the head changes because the whole order flips, not because a node is added or removed at the front. See Linked List Reversal

Deep Dive

Why the head needs a node in front of it

A singly linked list stores only next links. To remove a node, you change the next link of the node before it. Every node has a node before it, except the head. So removing the head is a different operation: it changes the variable head, not a next link.

Without a dummy node, the code needs a separate loop for the head, such as while head and head.val == val: head = head.next, and then the usual loop for the rest. The same split appears when you insert at the front, or when you build a new list and do not know its first node yet.

A dummy node is one extra node placed before the head: dummy = ListNode(0, head). The value 0 is never read. Now every real node, including the head, has a node before it. One loop and one rule, prev.next = prev.next.next, remove any node.

Tracing [6, 6, 1, 2, 6, 3] with val = 6

prev starts at the dummy node. The loop runs while prev.next exists. At each step, prev.next is the node being checked, and prev is the last node that was kept.

The first 6 equals val, so prev.next = prev.next.next links the dummy to the second 6. prev does not move. The second 6 is now prev.next, and it is removed in the same way. The dummy is now linked to 1.

The values 1 and 2 differ from val, so prev moves forward over each. The next 6 is removed by linking 2 to 3. Then 3 is kept, and prev.next is None, so the loop stops. The result is [1, 2, 3]. The same code returns [1, 2, 3, 4, 5] for [1, 2, 6, 3, 4, 5, 6], which is Example 1 of Remove Linked List Elements (LC 203).

Two traps: staying put after a removal, and returning head

After prev.next = prev.next.next, the new prev.next has not been checked yet. So prev stays where it is, and the else branch moves it only when a node is kept. If prev moved after every step, two equal values in a row would leave the second one in the list. For [1, 6, 6, 2] and val = 6, that code returns [1, 6, 2] instead of [1, 2].

The second trap is the return value. After the first node is removed, the variable head still points at the removed node, and the removed node still links to the rest of the list. For the traced input, return head returns [6, 6, 1, 2, 3]. The dummy node always links to the first kept node, so return dummy.next returns [1, 2, 3].

If every node is removed, dummy.next is None, and the function returns an empty list. This case needs no extra code.

The sentinel does not have to be a node

A sentinel is an extra element that is not part of the real data. Its job is to remove a special case at an edge. In a linked list, the sentinel is a node. In an array or a range, it can be a virtual index, a virtual value or a virtual bar. The code never stores it. It only pretends that the element is there.

Missing Ranges (LC 163) gives a sorted array nums, a range lower to upper, and asks for every missing number, written as ranges. A gap before nums[0] and a gap after nums[-1] look like two extra cases. Set prev = lower - 1 before the loop, and let the final value of curr be upper + 1. Then each gap, including the first and the last, is the numbers strictly between prev and curr.

Code / Blueprint
def process_ranges(nums: list[int], lower: int, upper: int) -> list[list[int]]:
"""Missing Ranges: virtual anchors make the first and last gap ordinary.
Time: O(N) | Space: O(1) extra
"""
result = [] # Each item is one missing range [first, last]
prev = lower - 1 # 1. Virtual left anchor: the number just before the range
 
for i in range(len(nums) + 1):
# 2. Virtual right anchor: on the final pass, curr is the number just after the range.
curr = nums[i] if i < len(nums) else upper + 1
 
# 3. Uniform gap check: the numbers strictly between prev and curr are missing.
if curr - prev > 1:
result.append([prev + 1, curr - 1])
 
# 4. Advance: curr becomes the new left anchor.
prev = curr
return result
 

Trace nums = [0, 1, 3, 50, 75], lower = 0, upper = 99. prev starts at -1. curr = 0 gives a difference of 1, so there is no gap. curr = 1 has no gap either. curr = 3 gives [2, 2]. curr = 50 gives [4, 49]. curr = 75 gives [51, 74]. The final curr = 100 gives [76, 99]. The result is [[2, 2], [4, 49], [51, 74], [76, 99]], which is Example 1 of Missing Ranges (LC 163).

An empty nums needs no extra code. The loop runs once with curr = upper + 1, and the result is [[lower, upper]]. The time is O(N) because each number is read once, and the extra space is O(1) besides the result.

Largest Rectangle in Histogram (LC 84) uses the same idea with a bar. After the last bar, the code adds one more bar of height 0. That bar is smaller than every real bar, so it pops all the bars still on the stack, and the code needs no second loop to flush them.

Two traps. First, the offsets: prev = lower or a final curr = upper drops the number at that end of the range, because the gap is computed as prev + 1 to curr - 1. Second, the special-case version: writing separate code for nums[0] and nums[-1] repeats the gap rule three times, and one wrong comparison in any copy gives a wrong answer.

The cost, and where a dummy node helps

The loop looks at each node once, so the time is O(N). The only extra memory is the dummy node and the variable prev, so the space is O(1).

Use a dummy node when the first node of the answer can change. Remove Nth Node From End of List (LC 19) can remove the head. Merge Two Sorted Lists (LC 21) does not know which list gives the first node. Remove Duplicates from Sorted List II (LC 82) removes every copy of a repeated value, including copies at the start.

In Remove Nth Node From End of List (LC 19), start both pointers at dummy, not at head. The trailing pointer must stop at the node before the one to remove. If the head is removed, only dummy is before it.

60-Second Pitch

"The head is the only node with no node before it, so removing it needs a special case. I remove that case with a dummy node: dummy = ListNode(0, head). Then prev starts at the dummy. While prev.next exists, I look at it. If its value equals val, I set prev.next = prev.next.next and keep prev where it is. If not, I move prev forward. At the end I return dummy.next, not head, because head may point at a removed node. The time is O(N) and the space is O(1)."

Common Questions

What is the time and space complexity?

The time is O(N), because the loop looks at each node once. The space is O(1): one dummy node and one pointer. The removal changes links in place.

Can you solve it without a dummy node?

Yes. First move head forward with while head and head.val == val: head = head.next. Then run the same prev loop from head. It gives the same result, but the removal rule is written twice. The dummy node writes it once.

Why return dummy.next and not head?

head is the variable that was passed in, and it never changes. If the first node is removed, head still points at the removed node, and that node still links to later nodes. dummy.next always links to the current first node.

Why does prev stay in place after a removal?

The node that was after the removed node is now prev.next, and it has not been checked. If prev moved forward, the code would skip it. Two equal values in a row would leave the second one in the list.

Could you do it with recursion?

Yes. Set head.next to the result of the same function on head.next. Then return head.next if head.val == val, or head if not. It needs no dummy node, but the call stack uses O(N) space, and a long list can hit the recursion limit in Python.

SWE Reasoning

Spot it

  • The head of the list can be removed, replaced or not known yet.
  • The problem deletes nodes, merges lists, builds a new list or rewires a part of a list that can start at the head.
  • Your first draft has an if that only handles the head.
  • The function returns the new head.

The invariant, step by step

  1. dummy = ListNode(0, head) puts a node before the head. prev = dummy.
  2. while prev.next looks at one node at a time. prev.next is the node being checked.
  3. If prev.next.val == val, prev.next = prev.next.next removes it. prev stays, because the new prev.next is not checked yet.
  4. Otherwise, prev = prev.next keeps the node and moves on.
  5. Every node before prev.next has been checked and kept, and dummy.next is always the current first node.
  6. When prev.next is None, return dummy.next returns the list.

Traps

  • Returning head. If the first node was removed, head points at a node that is not in the list.
  • Moving prev after a removal. The new prev.next is skipped, so two equal values in a row leave one behind.
  • Starting two pointers at head instead of dummy. When the head must go, there is no node before it.
  • Using the dummy value. The value 0 in dummy is not part of the list and must not be compared or returned.

Practice

Canonical LeetCode:Remove Linked List Elements (LC 203)Merge Two Sorted Lists (LC 21)Remove Duplicates from Sorted List II (LC 82)Remove Nth Node From End of List (LC 19)Reverse Linked List II (LC 92)Add Two Numbers (LC 2)Partition List (LC 86)Swap Nodes in Pairs (LC 24)Reverse Nodes in k-Group (LC 25)Odd Even Linked List (LC 328)Missing Ranges (LC 163)
Theory Context•Two Pointers & Sliding Window
P0EasyMental model~4 min speedrun

Sentinel / Dummy Node

Unlocks
Linked List ReversalLRU Cache
The problem

Remove Linked List Elements (LC 203)(opens leetcode.com)

You get the head of a singly linked list and an integer val. Remove every node whose value equals val, and return the head of the list that is left (which may be empty).

Example 1
Input:head = [1,2,6,3,4,5,6], val = 6
Output:[1,2,3,4,5]
Both nodes holding value 6 are removed, including one in the interior and one at the tail.
1263456

Dummy Node

list whose head may change ➔ dummy.next = head ➔ rewire through prev, return dummy.next
Spot it when: The head itself may be removed, replaced or not known yet: deleting nodes, merging lists, building a new list, or rewiring a sublist that can start at the head. The same signal appears in arrays: the first or the last element, or the gap before the first and after the last, needs its own special case.
Dummy Node
Python
1from typing import Optional
2 
3class ListNode:
4 def __init__(self, val: int = 0, next: Optional["ListNode"] = None):
5 self.val = val
6 self.next = next
7 
8def remove_elements(head: Optional[ListNode], val: int) -> Optional[ListNode]:
9 """Remove Linked List Elements: a dummy head makes deleting the real head ordinary.
10 Time: O(N) | Space: O(1)
11 """
12 dummy = ListNode(0, head) # Every real node now has a previous node
13 prev = dummy
14 
15 # 1. Walk And Unlink Matching Nodes:
16 # prev always stays on the last kept node, so prev.next = prev.next.next removes the head the same way it removes any other node, and staying put after a removal lets the new prev.next be checked too.
17 while prev.next:
18 if prev.next.val == val:
19 prev.next = prev.next.next
20 else:
21 prev = prev.next
22 
23 # 2. Return Through The Dummy, Not head:
24 # head itself may have been removed, so dummy.next, not head, is always the current first real node.
25 return dummy.next
Invariant: Every node before prev.next has been checked and kept, and dummy.next is always the current head.

The head is the only node without a previous node, which is why deleting or inserting at the head usually needs its own if. A dummy node placed before head gives every real node a previous node, so prev.next = prev.next.next removes the head like any other node. The sentinel does not have to be a node. It can be a virtual index, value or bar that is never stored in the data. It gives the first and the last element the same rule as every other element. Missing Ranges (LC 163) starts with prev = lower - 1 and ends with curr = upper + 1, so the gap before the first number and the gap after the last number use the same check as every gap between two numbers. Largest Rectangle in Histogram (LC 84) adds a bar of height 0 at the end, so the bars still on the stack are handled by the same rule. The trap is returning head: when the first node is removed, head still points at the deleted node, so return dummy.next.

See it run

Remove Linked List Elements: remove_elements([6, 6, 1, 2, 6, 3], val = 6)

prev dummy · prev.next 6

prev dummy · prev.next 6

prev dummy · prev.next 6

prev dummy · prev.next 6

prev dummy · prev.next 1

prev dummy · prev.next 1

prev 1 · prev.next 2

prev 1 · prev.next 2

prev 2 · prev.next 6

prev 2 · prev.next 6

prev 2 · prev.next 3

prev 2 · prev.next 3

prev 3 · prev.next None

prev 3 · prev.next None

prev 3 · prev.next None · return head [6, 6, 1, 2, 3]

result [1, 2, 3]

Each circle is a node, and D is the dummy node. Arrows are next links. A removed node disappears from the picture and appears in the removed list.

  • prev the last node that was kept
  • prev.next the node that is checked now
  • new link the link that was just written
  • dummy.next the first kept node, which the function returns

dummy.next = head, so the first node has a previous node, the dummy D. prev starts at D.

prev.next.val is 6, which equals val = 6. This node must be removed.

prev.next = prev.next.next links dummy to 6. prev stays where it is, so the new prev.next is checked next.

prev.next.val is 6, which equals val = 6. This node must be removed.

prev.next = prev.next.next links dummy to 1. prev stays where it is, so the new prev.next is checked next.

prev.next.val is 1, which differs from val = 6. This node is kept.

prev moves forward to 1.

prev.next.val is 2, which differs from val = 6. This node is kept.

prev moves forward to 2.

prev.next.val is 6, which equals val = 6. This node must be removed.

prev.next = prev.next.next links 2 to 3. prev stays where it is, so the new prev.next is checked next.

prev.next.val is 3, which differs from val = 6. This node is kept.

prev moves forward to 3.

prev.next is None, so the loop stops.

The variable head still points at the first input node, 6, which was removed. Returning head would return a list that starts with 6.

return dummy.next gives the first kept node. The list is [1, 2, 3].

Dprev6prev.next61263
removed
empty
return head
empty
return dummy.next
empty
1 / 16
Gotchas1 of 5

Gotcha 1 of 5

  1. 1
    The trap

    Gotcha 1: Returning head instead of dummy.next

    Mistake
    return head
    Fix
    return dummy.next

    What happens: head still points at the first node of the input. When that node holds val, the loop unlinks it from dummy, but head does not change. Returning head returns a list that starts with a deleted node.

    Example: head = [6, 6, 1, 2, 6, 3], val = 6
    Mistake: returns [6, 6, 1, 2, 3]Fix: returns [1, 2, 3]
  2. 2

    Gotcha 2: Moving prev forward after a deletion

    Mistake
    if prev.next.val == val:
        prev.next = prev.next.next
    prev = prev.next
    Fix
    if prev.next.val == val:
        prev.next = prev.next.next
    else:
        prev = prev.next

    What happens: After a deletion, prev.next is a new node that has not been checked. Moving prev forward skips that check. When two nodes in a row hold val, the second one stays in the list.

    Example: head = [1, 6, 6, 2], val = 6
    Mistake: returns [1, 6, 2]Fix: returns [1, 2]
  3. 3

    Gotcha 3: Starting the two pointers at head instead of dummy

    What happens: Remove Nth Node From End of List (LC 19) moves a leading pointer ahead, then moves both pointers together. The trailing pointer must stop at the node before the one to remove. When the head itself must go, only dummy sits before it. Starting at head leaves no node before the head.

  4. 4

    Gotcha 4: Starting prev at lower instead of lower - 1

    Mistake
    prev = lower
    Fix
    prev = lower - 1

    What happens: prev stands for the number just before the first number that must be covered. That number is lower - 1. When prev is lower, the check curr - prev > 1 treats lower itself as already present. If lower is missing, the gap that starts at lower loses its first number.

    Example: nums = [2, 3], lower = 0, upper = 5
    Mistake: returns [[1, 1], [4, 5]]Fix: returns [[0, 1], [4, 5]]
  5. 5

    Gotcha 5: Using upper instead of upper + 1 as the final curr

    Mistake
    curr = nums[i] if i < len(nums) else upper
    Fix
    curr = nums[i] if i < len(nums) else upper + 1

    What happens: The final curr is a virtual number just after upper. It makes the last gap end at curr - 1, which is upper. When the code uses upper, the last gap ends at upper - 1, so upper is never reported as missing.

    Example: nums = [0, 1, 3, 50, 75], lower = 0, upper = 99
    Mistake: returns [[2, 2], [4, 49], [51, 74], [76, 98]]Fix: returns [[2, 2], [4, 49], [51, 74], [76, 99]]
Not this one if…
  • Every next pointer must point the other way, as in Reverse Linked List (LC 206): the head changes because the whole order flips, not because a node is added or removed at the front. See Linked List Reversal

Deep Dive

Why the head needs a node in front of it

A singly linked list stores only next links. To remove a node, you change the next link of the node before it. Every node has a node before it, except the head. So removing the head is a different operation: it changes the variable head, not a next link.

Without a dummy node, the code needs a separate loop for the head, such as while head and head.val == val: head = head.next, and then the usual loop for the rest. The same split appears when you insert at the front, or when you build a new list and do not know its first node yet.

A dummy node is one extra node placed before the head: dummy = ListNode(0, head). The value 0 is never read. Now every real node, including the head, has a node before it. One loop and one rule, prev.next = prev.next.next, remove any node.

Tracing [6, 6, 1, 2, 6, 3] with val = 6

prev starts at the dummy node. The loop runs while prev.next exists. At each step, prev.next is the node being checked, and prev is the last node that was kept.

The first 6 equals val, so prev.next = prev.next.next links the dummy to the second 6. prev does not move. The second 6 is now prev.next, and it is removed in the same way. The dummy is now linked to 1.

The values 1 and 2 differ from val, so prev moves forward over each. The next 6 is removed by linking 2 to 3. Then 3 is kept, and prev.next is None, so the loop stops. The result is [1, 2, 3]. The same code returns [1, 2, 3, 4, 5] for [1, 2, 6, 3, 4, 5, 6], which is Example 1 of Remove Linked List Elements (LC 203).

Two traps: staying put after a removal, and returning head

After prev.next = prev.next.next, the new prev.next has not been checked yet. So prev stays where it is, and the else branch moves it only when a node is kept. If prev moved after every step, two equal values in a row would leave the second one in the list. For [1, 6, 6, 2] and val = 6, that code returns [1, 6, 2] instead of [1, 2].

The second trap is the return value. After the first node is removed, the variable head still points at the removed node, and the removed node still links to the rest of the list. For the traced input, return head returns [6, 6, 1, 2, 3]. The dummy node always links to the first kept node, so return dummy.next returns [1, 2, 3].

If every node is removed, dummy.next is None, and the function returns an empty list. This case needs no extra code.

The sentinel does not have to be a node

A sentinel is an extra element that is not part of the real data. Its job is to remove a special case at an edge. In a linked list, the sentinel is a node. In an array or a range, it can be a virtual index, a virtual value or a virtual bar. The code never stores it. It only pretends that the element is there.

Missing Ranges (LC 163) gives a sorted array nums, a range lower to upper, and asks for every missing number, written as ranges. A gap before nums[0] and a gap after nums[-1] look like two extra cases. Set prev = lower - 1 before the loop, and let the final value of curr be upper + 1. Then each gap, including the first and the last, is the numbers strictly between prev and curr.

Code / Blueprint
def process_ranges(nums: list[int], lower: int, upper: int) -> list[list[int]]:
"""Missing Ranges: virtual anchors make the first and last gap ordinary.
Time: O(N) | Space: O(1) extra
"""
result = [] # Each item is one missing range [first, last]
prev = lower - 1 # 1. Virtual left anchor: the number just before the range
 
for i in range(len(nums) + 1):
# 2. Virtual right anchor: on the final pass, curr is the number just after the range.
curr = nums[i] if i < len(nums) else upper + 1
 
# 3. Uniform gap check: the numbers strictly between prev and curr are missing.
if curr - prev > 1:
result.append([prev + 1, curr - 1])
 
# 4. Advance: curr becomes the new left anchor.
prev = curr
return result
 

Trace nums = [0, 1, 3, 50, 75], lower = 0, upper = 99. prev starts at -1. curr = 0 gives a difference of 1, so there is no gap. curr = 1 has no gap either. curr = 3 gives [2, 2]. curr = 50 gives [4, 49]. curr = 75 gives [51, 74]. The final curr = 100 gives [76, 99]. The result is [[2, 2], [4, 49], [51, 74], [76, 99]], which is Example 1 of Missing Ranges (LC 163).

An empty nums needs no extra code. The loop runs once with curr = upper + 1, and the result is [[lower, upper]]. The time is O(N) because each number is read once, and the extra space is O(1) besides the result.

Largest Rectangle in Histogram (LC 84) uses the same idea with a bar. After the last bar, the code adds one more bar of height 0. That bar is smaller than every real bar, so it pops all the bars still on the stack, and the code needs no second loop to flush them.

Two traps. First, the offsets: prev = lower or a final curr = upper drops the number at that end of the range, because the gap is computed as prev + 1 to curr - 1. Second, the special-case version: writing separate code for nums[0] and nums[-1] repeats the gap rule three times, and one wrong comparison in any copy gives a wrong answer.

The cost, and where a dummy node helps

The loop looks at each node once, so the time is O(N). The only extra memory is the dummy node and the variable prev, so the space is O(1).

Use a dummy node when the first node of the answer can change. Remove Nth Node From End of List (LC 19) can remove the head. Merge Two Sorted Lists (LC 21) does not know which list gives the first node. Remove Duplicates from Sorted List II (LC 82) removes every copy of a repeated value, including copies at the start.

In Remove Nth Node From End of List (LC 19), start both pointers at dummy, not at head. The trailing pointer must stop at the node before the one to remove. If the head is removed, only dummy is before it.

60-Second Pitch

"The head is the only node with no node before it, so removing it needs a special case. I remove that case with a dummy node: dummy = ListNode(0, head). Then prev starts at the dummy. While prev.next exists, I look at it. If its value equals val, I set prev.next = prev.next.next and keep prev where it is. If not, I move prev forward. At the end I return dummy.next, not head, because head may point at a removed node. The time is O(N) and the space is O(1)."

Common Questions

What is the time and space complexity?

The time is O(N), because the loop looks at each node once. The space is O(1): one dummy node and one pointer. The removal changes links in place.

Can you solve it without a dummy node?

Yes. First move head forward with while head and head.val == val: head = head.next. Then run the same prev loop from head. It gives the same result, but the removal rule is written twice. The dummy node writes it once.

Why return dummy.next and not head?

head is the variable that was passed in, and it never changes. If the first node is removed, head still points at the removed node, and that node still links to later nodes. dummy.next always links to the current first node.

Why does prev stay in place after a removal?

The node that was after the removed node is now prev.next, and it has not been checked. If prev moved forward, the code would skip it. Two equal values in a row would leave the second one in the list.

Could you do it with recursion?

Yes. Set head.next to the result of the same function on head.next. Then return head.next if head.val == val, or head if not. It needs no dummy node, but the call stack uses O(N) space, and a long list can hit the recursion limit in Python.

SWE Reasoning

Spot it

  • The head of the list can be removed, replaced or not known yet.
  • The problem deletes nodes, merges lists, builds a new list or rewires a part of a list that can start at the head.
  • Your first draft has an if that only handles the head.
  • The function returns the new head.

The invariant, step by step

  1. dummy = ListNode(0, head) puts a node before the head. prev = dummy.
  2. while prev.next looks at one node at a time. prev.next is the node being checked.
  3. If prev.next.val == val, prev.next = prev.next.next removes it. prev stays, because the new prev.next is not checked yet.
  4. Otherwise, prev = prev.next keeps the node and moves on.
  5. Every node before prev.next has been checked and kept, and dummy.next is always the current first node.
  6. When prev.next is None, return dummy.next returns the list.

Traps

  • Returning head. If the first node was removed, head points at a node that is not in the list.
  • Moving prev after a removal. The new prev.next is skipped, so two equal values in a row leave one behind.
  • Starting two pointers at head instead of dummy. When the head must go, there is no node before it.
  • Using the dummy value. The value 0 in dummy is not part of the list and must not be compared or returned.

Practice

Canonical LeetCode:Remove Linked List Elements (LC 203)Merge Two Sorted Lists (LC 21)Remove Duplicates from Sorted List II (LC 82)Remove Nth Node From End of List (LC 19)Reverse Linked List II (LC 92)Add Two Numbers (LC 2)Partition List (LC 86)Swap Nodes in Pairs (LC 24)Reverse Nodes in k-Group (LC 25)Odd Even Linked List (LC 328)Missing Ranges (LC 163)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•