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

Conceptual Foundation & Intuitive Analogy

🧭 Conceptual Foundation & Pattern Intuition

When an array is sorted, elements possess a strict monotonic spatial order: moving to the right guarantees larger numbers, while moving to the left guarantees smaller numbers. In an unsorted array, locating a pair that sums to a target requires an O(N)O(N)O(N) hash set. But with sorted data, the space overhead can be eliminated entirely: by placing two pointers at opposite ends, the sum of the boundary elements acts as an infallible guide. If their sum is too small, the left boundary is permanently inadequate and must advance. If their sum is too large, the right boundary must retreat.

🏟️ The Analogy: The Balancing Seesaw

Imagine two people on a giant seesaw trying to balance a target weight. On the left side is a line of light weights arranged from lightest to heaviest; on the right side is a line of heavy weights arranged from heaviest to lightest. If the scale tips too low (sum is less than target), replacing the current light weight with an even lighter weight would make the deficit worse; you must step forward to a heavier light weight. If the scale tips too high, you must step back to a lighter heavy weight. You never need to restart or reconsider discarded weights.

🔑 Why It Works

The proof of correctness is absolute: at step cur_sum < target, numbers[left] + numbers[k] < target for all k≤rightk \le \text{right}k≤right because numbers[k] <= numbers[right]. Therefore, numbers[left] can never participate in any valid solution. Discarding it permanently via left += 1 is mathematically airtight.

Visual Blueprint & State Transition Model
Two Pointers & Sliding Window Mental Model Diagram
Figure 1 • Two Pointers & Sliding Window Core Invariant & State Lifecycle
Core Invariant Specification

The invariant to say out loud: "Everything I have moved past can never be part of a better answer than the one I already recorded — so I never have to look at it again." If you cannot say that sentence about your problem, two pointers is unsound and you need prefix sums, a hash map, or a heap instead.

Foundational Coordination & Boundary Paradigms: Monotonic Pointers vs. Prefix Sum Duality
1. Converging Squeezewhile left < right

Opposite boundaries step inward. Outer pairs are strictly dominated by sortedness or geometric width. Works with negative numbers (Two Sum II, 3Sum, Squares of Sorted Array) because monotonicity relies on sorted positional order (A[i] ≤ A[j]), not element signs.

Archetypes: Two Sum II (LC 167), 3Sum (LC 15), Squares of Sorted Array (LC 977), Container (LC 11).
2. Window & Compactionwhile right < n

Same-direction traversal maintaining contiguous state. FAILS on negative numbers for sum/product: evicting a negative increases sum and adding one decreases it, destroying window monotonicity!

Archetypes: Min Subarray Sum (LC 209, non-negatives only), Longest Substring (LC 3), Remove Duplicates (LC 26).
3. Prefix Sum DualityP[R+1] - P[L] = target

The negative numbers rescue. When negative numbers break sliding window monotonicity, prefix sums transform contiguous range searches into algebraic point lookups: P[L] = P[R+1] - target, recovering strict O(N) via hash map.

Archetypes: Subarray Sum Equals K (LC 560, negative numbers allowed), Shortest Subarray Sum (LC 862).
⚠️ Senior Interview Gotcha: Can Two Pointers Work on Negative Numbers?

YES for Converging / Squeeze: On sorted arrays (e.g. Two Sum II, 3Sum, Squares of Sorted Array), negative numbers work natively because monotonicity is governed by sorted index order (A[i] ≤ A[j]), not signs.

NO for Sliding Window Sums: If elements can be negative, expanding can decrease the sum and contracting can increase it. Monotonicity collapses! Pivot immediately to Prefix Sum + Hash Map (LC 560) or Monotonic Deque (LC 862).

The 15 Mental Models of Two Pointers & Sliding Window

Two Pointers problems divide by how the pointers move, what the window holds, and what a lookup remembers. Name the model before coding.

1Hash Map
items ➔ computed key ➔ dict lookup or count in O(1)

Hashing & Frequency Maps (Group Anagrams, Two Sum)

Spot it when: The question is "have I seen this before?", "how many of each?" or "which items belong together?": duplicates, counts, a partner that completes a target, or items grouped by a key you compute from them.
Mode 1 · Hash Map
Python
1from collections import defaultdict
2 
3 
4def group_anagrams(strs: list[str]) -> list[list[str]]:
5 """Group Anagrams: words with the same sorted letters share one key in a hash map.
6 Time: O(N * K log K) | Space: O(N * K)
7 """
8 groups: dict[str, list[str]] = defaultdict(list) # key -> every word read so far with that key
9 # 1. File Each Word Under Its Key:
10 # Words with the same letters and the same counts produce the same sorted-letters key ("eat", "tea" -> "aet"), so every anagram lands in the same group without comparing any two words.
11 for s in strs:
12 key = "".join(sorted(s))
13 groups[key].append(s)
14 return list(groups.values())
15 
16 
17def two_sum(nums: list[int], target: int) -> list[int]:
18 """Two Sum: remember each value's index, and look up the partner each new value needs.
19 Time: O(N) | Space: O(N)
20 """
21 index_of: dict[int, int] = {} # value -> an index where it was seen
22 # 1. Look Up The Partner Before Storing:
23 # If target - num was already seen, its stored index completes the pair; storing num only after the lookup keeps num from ever pairing with itself.
24 for i, num in enumerate(nums):
25 partner = target - num
26 if partner in index_of:
27 return [index_of[partner], i]
28 index_of[num] = i
29 return [] # LeetCode guarantees exactly one answer, so this line is never reached

A hash map finds a key's entry in one step, so pick the key the question is about and store under it what the answer needs: in group_anagrams the key is a word's sorted letters and the value is every word read so far with that key; in two_sum the key is a number and the value is its index. Items that belong together produce equal keys, so groups[key].append(s) files every anagram in one pass without comparing any two words, and partner in index_of finds an earlier number that completes the pair without a second loop. The trap is a lossy key: it must be equal only for items that truly belong together, so key on "".join(sorted(s)), which keeps repeated letters, never on frozenset(s) or set(s), which drop them and put "aab" and "abb" in one group.

Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Keying on frozenset(s) instead of the sorted letters

    Mistake
    key = frozenset(s)
    Fix
    key = "".join(sorted(s))

    What happens: A set keeps each letter once and drops the repeats. "aab" and "abb" both give the set of a and b. They land in one group, but they are not anagrams. "".join(sorted(s)) keeps every letter, repeats included.

    Example: strs = ["aab", "abb"]
    Mistake: returns [['aab', 'abb']]Fix: returns [['aab'], ['abb']]
  2. 2

    Gotcha 2: Using the list from sorted(s) as the key

    Mistake
    key = sorted(s)
    Fix
    key = "".join(sorted(s))

    What happens: sorted(s) returns a list. A list can change, so Python does not allow it as a dict key. groups[key] raises a TypeError. Join the letters into a string, or wrap the list in tuple(...).

    Example: strs = ["eat", "tea"]
    Mistake: raises TypeErrorFix: returns [['eat', 'tea']]
  3. 3

    Gotcha 3: Storing num before looking up its partner

    Mistake
    partner = target - num
    index_of[num] = i
    if partner in index_of:
        return [index_of[partner], i]
    Fix
    partner = target - num
    if partner in index_of:
        return [index_of[partner], i]
    index_of[num] = i

    What happens: index_of[num] = i makes num visible to the lookup. If it runs first, partner in index_of can find num itself when partner == num. Then two_sum pairs one element with itself.

    Example: nums = [3, 2, 4], target = 6
    Mistake: returns [0, 0]Fix: returns [1, 2]
  4. 4

    Gotcha 4: Joining letter counts into a key without a separator

    Mistake
    key = "".join(map(str, count))
    Fix
    key = tuple(count)

    What happens: A key built from letter counts lists 26 numbers. Joined without a separator, different counts can give the same string. One a with eleven bs gives "111" followed by zeros. Eleven as with one b gives the same string.

    Example: Do "a" + "b" * 11 and "a" * 11 + "b" get the same key?
    Mistake: returns TrueFix: returns False
Canonical LeetCode:Group Anagrams (LC 49)Two Sum (LC 1)Valid Anagram (LC 242)Contains Duplicate (LC 217)Valid Sudoku (LC 36)Ransom Note (LC 383)Isomorphic Strings (LC 205)Detect Squares (LC 2013)Binary Tree Vertical Order Traversal (LC 314)Line Reflection (LC 356)Perfect Rectangle (LC 391)Check If N and Its Double Exist (LC 1346)Random Pick Index (LC 398)Minimum Index of a Valid Split (LC 2780)Copy List with Random Pointer (LC 138)Contains Duplicate II (LC 219)
Open the Hash Map card
2Bucket Sort

Bucket Sort (Top K Frequent, Maximum Gap)

Spot it when: Values or counts fall in a known, bounded range (every count is between 1 and n; n values spread over min to max), and you need their order, the top k, or the largest gap in linear time, without an O(n log n) sort.
Mode 2 · Bucket Sort
Python
1def maximum_gap(nums: list[int]) -> int:
2 """Maximum Gap: pigeonhole buckets, compare neighbours across buckets only.
3 Time: O(N) | Space: O(N)
4 """
5 # 1. Handle Too Few Values:
6 # With fewer than two values there is no neighbour at all, so no gap can be measured.
7 n = len(nums)
8 if n < 2:
9 return 0
10 
11 # 2. Size The Buckets From The Pigeonhole Bound:
12 # The largest gap is at least the average gap (hi - lo) / (n - 1); max(1, ...) keeps every
13 # bucket at least one value wide so the later division never hits zero.
14 lo, hi = min(nums), max(nums)
15 width = max(1, (hi - lo) // (n - 1))
16 count = (hi - lo) // width + 1 # Enough buckets to hold hi as well
17 bucket_min = [float("inf")] * count # Smallest value that fell in each bucket
18 bucket_max = [float("-inf")] * count # Largest value that fell in each bucket
19 
20 # 3. Drop Each Value Into Its Bucket, Keeping Only The Extremes:
21 # No two values are compared here; because width is at most the smallest possible gap, the
22 # answer can never sit inside one bucket, so only its smallest and largest value matter.
23 for x in nums:
24 b = (x - lo) // width
25 bucket_min[b] = min(bucket_min[b], x)
26 bucket_max[b] = max(bucket_max[b], x)
27 
28 # 4. Walk The Buckets In Order And Measure Cross-Bucket Gaps:
29 # Skipping empty buckets and carrying prev_max forward means each gap is measured from the
30 # last non-empty bucket's maximum to the next non-empty bucket's minimum.
31 best = 0
32 prev_max = lo
33 for b in range(count):
34 if bucket_min[b] == float("inf"):
35 continue
36 best = max(best, bucket_min[b] - prev_max)
37 prev_max = bucket_max[b]
38 
39 return best

Bucket Sort drops each value straight into the bucket that covers its range, b = (x - lo) // width, so placing a value needs no comparison with other values and the buckets come out in order. The key can be any bounded number: bucket by value range to find gaps, or by count to order items by frequency. For the largest gap between neighbours, keep only bucket_min[b] and bucket_max[b], walk the buckets once, and measure bucket_min[b] - prev_max, where prev_max is the largest value of the last non-empty bucket. That is enough because of the pigeonhole principle: n values from lo to hi leave n - 1 gaps that add up to hi - lo, so the largest gap is at least their average, and being a whole number it is at least width whenever hi > lo. Two values in the same bucket differ by at most width - 1, so the maximum gap is never inside one bucket: it always runs from one bucket's maximum to a later bucket's minimum, and the values in between never matter. The trap is the floor of 1: when hi - lo < n - 1, for example with repeated values, (hi - lo) // (n - 1) is 0 and (x - lo) // width divides by zero, so write width = max(1, (hi - lo) // (n - 1)).

Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Computing width without the floor of 1

    Mistake
    width = (hi - lo) // (n - 1)
    Fix
    width = max(1, (hi - lo) // (n - 1))

    What happens: With repeated values, hi - lo can be smaller than n - 1. Then (hi - lo) // (n - 1) is 0. The next division, (hi - lo) // width, divides by zero. max(1, ...) keeps every bucket at least one value wide.

    Example: nums = [1, 1, 1, 4, 2]
    Mistake: raises ZeroDivisionErrorFix: returns 2
  2. 2

    Gotcha 2: Leaving out the check if n < 2

    Mistake
    n = len(nums)
    Fix
    n = len(nums)
    if n < 2:
        return 0

    What happens: With one value, there is no neighbour and no gap, so the answer is 0. Without the check, the code goes on to compute the width. n - 1 is 0 there, so (hi - lo) // (n - 1) divides by zero.

    Example: nums = [10]
    Mistake: raises ZeroDivisionErrorFix: returns 0
  3. 3

    Gotcha 3: Rounding the width up with + 1

    Mistake
    width = (hi - lo) // (n - 1) + 1
    Fix
    width = max(1, (hi - lo) // (n - 1))

    What happens: The largest gap is at least (hi - lo) / (n - 1), the average gap. A bucket must be no wider than that, so the largest gap can never be inside one bucket. With + 1, a bucket can be wider. Two values can then share a bucket, and the gap between them is never measured.

    Example: nums = [6, 18]
    Mistake: returns 0Fix: returns 12
  4. 4

    Gotcha 4: Comparing with bucket_max[b - 1] instead of prev_max

    Mistake
    best = max(best, bucket_min[b] - bucket_max[b - 1])
    Fix
    best = max(best, bucket_min[b] - prev_max)
    prev_max = bucket_max[b]

    What happens: The largest gap often runs across empty buckets. The value just below bucket_min[b] is then in an earlier bucket, not in bucket b - 1. An empty bucket_max[b - 1] is -inf, and the difference becomes inf. prev_max keeps the maximum of the last bucket that was not empty.

    Example: nums = [1, 2, 10]
    Mistake: returns infFix: returns 8
Canonical LeetCode:Maximum Gap (LC 164)Sort Characters By Frequency (LC 451)Top K Frequent Elements (LC 347)
Open the Bucket Sort card
3Converging Pointers
left -> <- right (Opposite Boundary Contraction)

Two Pointers (Opposite Ends)

Spot it when: The array is sorted (or can be) and you look for pairs or triples that meet a target, or you compare the two ends (palindromes, container walls).
Mode 3 · Converging Pointers
Python
1from typing import Callable, List, Optional, Tuple
2 
3 
4def converge(n: int, compare: Callable[[int, int], int]) -> Optional[Tuple[int, int]]:
5 """Converging Pointers: squeeze a pair of indices (left, right) inward from both ends of a sorted sequence of length n.
6 compare(left, right) returns 0 to stop at this pair, below 0 to move left, and above 0 to move right.
7 Precondition: each step can rule out the element at left or the element at right for good, and compare says which one.
8 For a pair sum on a sorted sequence, move left when the pair is too small and move right when it is too big.
9 compare may also record an answer and return a nonzero value to keep going, as in Container With Most Water and 3Sum.
10 In Valid Palindrome, a match moves both pointers, so that loop moves left and right together.
11 T = the cost of one compare call.
12 Time: O(N * T) | Space: O(1)
13 """
14 left, right = 0, n - 1 # left and right only ever move toward each other
15 
16 # 1. Squeeze Inward While Not Met:
17 # compare rules out the element at left or at right for good on every step, so only the ruled-out end moves and the pair found, if any, is always still between left and right.
18 while left < right:
19 order = compare(left, right)
20 if order == 0:
21 return (left, right)
22 elif order < 0:
23 left += 1 # The pair is too small: the element at left cannot pair with any remaining element
24 else:
25 right -= 1 # The pair is too big: the element at right cannot pair with any remaining element
26 
27 return None

Start one pointer at each end of a sorted array. If the pair's sum is too small, only moving left right can raise it; if it is too big, only moving right left can lower it.

Each step rules out one element for good: when curr_sum < target, numbers[left] is already paired with the largest value left, so no remaining partner can reach target (and the mirror holds for right). This works with negative numbers too, because it relies on sorted order, not on signs.

The trap is unsorted input: there, moving a pointer no longer raises or lowers the sum, so the squeeze can walk past the answer; sort first, and keep the original indices if the problem asks for them.

Gotchas1 of 5

Gotcha 1 of 5

  1. 1
    The trap

    Gotcha 1: Running the squeeze on an unsorted array

    Mistake
    twoSum(nums, target)
    Fix
    twoSum(sorted(nums), target)

    What happens: When curr_sum < target, the loop moves left one step right to raise the sum. A larger value at left is certain only when nums is sorted. On unsorted input, a move can lower the sum, and the loop walks past the pair. Sorting first finds the pair, but it returns positions in the sorted list. Two Sum (LC 1) wants the original indices, so sort (value, index) pairs instead.

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

    Gotcha 2: Moving both pointers after one comparison

    Mistake
    left += 1
    right -= 1
    Fix
    left += 1

    What happens: When curr_sum < target, only numbers[left] is ruled out. numbers[right] may still pair with a later numbers[left]. Moving right as well drops that value without a check.

    Example: numbers = [1, 2, 4, 6], target = 8
    Mistake: returns []Fix: returns [2, 4]
  3. 3

    Gotcha 3: Skipping 3Sum (LC 15) duplicates before the match check

    What happens: 3Sum (LC 15) picks a first number nums[i] and squeezes the rest of the sorted array for the other two with left and right. The moment total == nums[i] + nums[left] + nums[right] equals zero, record the triplet right away with res.append([nums[i], nums[left], nums[right]]), before skipping anything. Once that triplet is saved, later copies of nums[left] or nums[right] can only rebuild the same triplet of values, never a new one, so moving past them afterward loses nothing. The trap is running the duplicate-skip loops first: while left < right and nums[left] == nums[left + 1]: left += 1 and while left < right and nums[right] == nums[right - 1]: right -= 1, run before the total == 0 check saves the match, throw away the exact left/right pair that produced the zero sum, so that triplet never reaches res at all.

  4. 4

    Gotcha 4: Moving the pointer at the taller wall

    What happens: In Container With Most Water (LC 11), the shorter wall sets the water level. Moving either pointer makes the width smaller. After moving the taller wall, the level is still at most the shorter wall. So the area can never grow. Only moving the shorter wall can find a larger area.

  5. 5

    Gotcha 5: Assuming every two-pointer squeeze can break early

    What happens: Two Sum II - Input Array Is Sorted (LC 167) moves left and right toward each other on every step, and it never breaks out of the loop early: the array is sorted, so raising left can only raise numbers[left] + numbers[right], and lowering right can only lower it, and that holds for negative numbers too. 3Sum (LC 15) adds a different move on top of that squeeze: if nums[i] > 0: break, which stops the whole outer loop over the anchor nums[i]. That break is only correct because the array is sorted ascending and nums[i] is the smallest of the three remaining numbers: once nums[i] is positive, nums[left] and nums[right] (both at or after i) are positive too, so nums[i] + nums[left] + nums[right] can never reach zero, and every later anchor is checked the same way and found to fail. Reading the two moves as the same kind of step is the trap: the LC 167 squeeze needs no sign check and works correctly with negative numbers already, so carrying if nums[i] > 0: break into a squeeze that has no sorted, all-positive-from-here anchor breaks on inputs the squeeze alone would have solved correctly.

Canonical LeetCode:Two Sum II - Input Array Is Sorted (LC 167)3Sum (LC 15)Container With Most Water (LC 11)Trapping Rain Water (LC 42)3Sum Smaller (LC 259)Two Sum Less Than K (LC 1099)Valid Palindrome (LC 125)4Sum (LC 18)Count the Number of Fair Pairs (LC 2563)Strobogrammatic Number (LC 246)
Open the Converging Pointers card
4Dummy Node
list whose head may change ➔ dummy.next = head ➔ rewire through prev, return dummy.next

Sentinel / Dummy Node

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.
Mode 4 · 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

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.

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]]
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)
Open the Dummy Node card
5Boyer-Moore Voting

Boyer-Moore Voting (Majority Element)

Spot it when: A value holds more than a fixed share of the input (more than half, or more than n / k for a small k), and you must find it in one pass with O(1) extra space, so no count per value.
Mode 5 · Boyer-Moore Voting
Python
1def majority_element(nums: list[int]) -> int:
2 """Majority Element: pair off different values; the majority is the value left standing.
3 Time: O(N) | Space: O(1)
4 """
5 # candidate, count: the value currently "standing" and how many of its copies are not yet
6 # paired off against a different value.
7 candidate, count = None, 0
8 
9 # 1. Decide This Value's Role, Then Update The Tally:
10 # Everything read before a count of 0 is already paired off, so x starts a new run;
11 # counting after that check means x always counts as its own first copy, and count never
12 # goes below 0.
13 for x in nums:
14 if count == 0:
15 candidate = x
16 count += 1 if x == candidate else -1
17 
18 # 2. Report The Value Left Standing:
19 # A value that fills more than half of nums has more copies than everything else combined,
20 # so pairing off different values can never cancel it; it is the one still standing at the end.
21 return candidate

Let every value cancel one copy of a different value: a value that fills more than half of nums has more copies than all the others together, so it can't be cancelled out, and the code only has to remember what is still standing, candidate and count. When count == 0 everything read so far is paired off, so x becomes the new candidate; then count += 1 if x == candidate else -1 either adds a copy or removes a pair of two different values, at most one of them the majority, which is why the majority is the candidate left at the end. The trap is trusting that survivor: it is the answer only when a majority is promised (in the middle of [2,2,1,1,1,2,2] the candidate is 1, and on [1, 2, 3] the survivor occurs once), so when the promise is missing, count the survivor again in a second pass.

Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Returning candidate when no majority is promised

    Mistake
    return candidate
    Fix
    if nums.count(candidate) > len(nums) // 2:
        return candidate
    return None

    What happens: The last candidate is the majority only when a majority exists. Majority Element (LC 169) promises one, so the blueprint can return it. Without that promise, the survivor can be any value. Count it in a second pass before returning it.

    Example: nums = [1, 2, 3], with no majority promised
    Mistake: returns 3Fix: returns None
  2. 2

    Gotcha 2: Looping over nums[:-1] and skipping the last value

    Mistake
    for x in nums[:-1]:
    Fix
    for x in nums:

    What happens: Every value must cancel or add one copy. nums[:-1] leaves out the last value. When that value decides the result, the wrong candidate is left standing.

    Example: nums = [1, 2, 2]
    Mistake: returns 1Fix: returns 2
  3. 3

    Gotcha 3: Counting x before testing count == 0

    Mistake
    count += 1 if x == candidate else -1
    if count == 0:
        candidate = x
    Fix
    if count == 0:
        candidate = x
    count += 1 if x == candidate else -1

    What happens: When count is 0, every value read so far is paired off. The new value x must start the new run and count as its first copy. Counting first compares x with the old candidate. At the start, candidate is None, so the first value subtracts 1 and count becomes -1. count then never returns to 0, and candidate stays None.

    Example: nums = [3, 1, 3]
    Mistake: returns NoneFix: returns 3
  4. 4

    Gotcha 4: Returning the value that led with the largest count

    Mistake
    if count > best_count:
        best, best_count = candidate, count
    Fix
    return candidate

    What happens: The lead changes as the pairs cancel. A value can hold the largest count early and still be paired off later. Only the candidate standing after the last value is the majority.

    Example: nums = [1, 1, 2, 2, 2]
    Mistake: returns 1Fix: returns 2
Canonical LeetCode:Majority Element (LC 169)Majority Element II (LC 229)Minimum Index of a Valid Split (LC 2780)
Open the Boyer-Moore Voting card
6KMP

Knuth-Morris-Pratt (KMP) String Matching

Spot it when: You must find an exact pattern in a long text, or a prefix of a string that comes back later in it (a border, a period, a repeated start), in linear time.
Mode 6 · KMP
Python
1def str_str(haystack: str, needle: str) -> int:
2 """KMP: find needle in haystack without ever moving back in haystack.
3 Time: O(N + M) | Space: O(M)
4 """
5 m = len(needle)
6 # lps[i]: the longest proper prefix of needle[:i + 1] that is also its suffix (its longest border).
7 lps = [0] * m
8 k = 0 # Length of the prefix of needle matched so far
9 
10 # 1. Build The Border Table From needle Against Itself:
11 # Building lps runs the same search on needle against itself, so a mismatch falls back to a
12 # shorter border the same way the main search below does.
13 for i in range(1, m):
14 while k > 0 and needle[i] != needle[k]:
15 k = lps[k - 1]
16 if needle[i] == needle[k]:
17 k += 1
18 lps[i] = k
19 
20 # 2. Scan haystack Once, Falling Back Instead Of Restarting:
21 # On a mismatch, the k letters just matched are needle[:k], already known, so k falls back
22 # to the longest border that still fits; the match ends at i, so it starts i - m + 1 letters
23 # earlier.
24 k = 0
25 for i, ch in enumerate(haystack):
26 while k > 0 and ch != needle[k]:
27 k = lps[k - 1]
28 if ch == needle[k]:
29 k += 1
30 if k == m:
31 return i - m + 1
32 return -1

KMP never reads a character of the text twice: when a mismatch comes after k matched letters, those letters are needle[:k], which you already know, so the search keeps their longest border, k = lps[k - 1], and tries the same letter again instead of moving the start forward by one and re-reading. It is correct because a match that is still possible must begin at a suffix of what was read that is also a prefix of needle, and lps[k - 1] is the longest such part; falling back again while k > 0 and ch != needle[k] visits every shorter one in order, and lps itself is built by running the same loop on needle against itself. The trap is resetting k = 0 on a mismatch: on haystack = "abababc", needle = "ababc" it throws away the border "ab" at i = 4, where the only match begins, and returns -1 instead of 2.

Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Resetting k = 0 on a mismatch

    Mistake
    while k > 0 and ch != needle[k]:
        k = 0
    Fix
    while k > 0 and ch != needle[k]:
        k = lps[k - 1]

    What happens: After k matched letters, the text just read is needle[:k]. Its end can also be the start of needle. A match can then begin inside the failed attempt. k = 0 throws that start away, and the search can miss the match. k = lps[k - 1] keeps it.

    Example: haystack = "abababc", needle = "ababc"
    Mistake: returns -1Fix: returns 2
  2. 2

    Gotcha 2: Building lps with range(m) instead of range(1, m)

    Mistake
    for i in range(m):
    Fix
    for i in range(1, m):

    What happens: lps[0] must stay 0, because a single letter has no shorter border. With range(m), the loop compares needle[0] with itself and sets lps[0] = 1. A fall back from k = 1 then sets k = lps[0], which is 1 again. The while loop never ends.

    Example: haystack = "ab", needle = "ac"
    Mistake: the loop never endsFix: returns -1
  3. 3

    Gotcha 3: Falling back once with if instead of while

    Mistake
    if k > 0 and ch != needle[k]:
    Fix
    while k > 0 and ch != needle[k]:

    What happens: After one fall back, the letter may still not match needle[k]. The search must then fall back again, to a shorter border. With if, it falls back once and moves on. k then counts letters that were never matched.

    Example: haystack = "aabaa", needle = "aaa"
    Mistake: returns 2Fix: returns -1
  4. 4

    Gotcha 4: Falling back to lps[k] instead of lps[k - 1]

    Mistake
    while k > 0 and ch != needle[k]:
        k = lps[k]
    Fix
    while k > 0 and ch != needle[k]:
        k = lps[k - 1]

    What happens: After k matched letters, the text read is needle[:k]. Its border is stored at lps[k - 1], because lps[i] describes needle[:i + 1]. lps[k] describes a prefix one letter longer than what was read. In this example, k is 2 at the b, and lps[2] is also 2. k never changes, so the while loop never ends.

    Example: haystack = "aabaaab", needle = "aaab"
    Mistake: the loop never endsFix: returns 3
Canonical LeetCode:Find the Index of the First Occurrence in a String (LC 28)Longest Happy Prefix (LC 1392)Repeated Substring Pattern (LC 459)Shortest Palindrome (LC 214)
Open the KMP card
7Fixed Window
i >= k -> (Absorb Incoming, Evict Outgoing)

Fixed-Size Sliding Window

Spot it when: The window length k is given and you measure every window: averages, counts, anagram matches.
Mode 7 · Fixed Window
Python
1from typing import Callable, List
2 
3 
4def slide_fixed(n: int, k: int, add: Callable[[int], None], remove: Callable[[int], None], record: Callable[[int], None]) -> None:
5 """Fixed Window: visit every window of exactly k indices in 0..n-1, one index in and one out per step.
6 Precondition: n, the number of indices, is known before the loop starts, and k >= 1.
7 record(start) is called once per window, after the window holds indices start..start + k - 1.
8 T = the cost of one add, remove or record call.
9 Time: O(N * T) | Space: O(1) besides the window's state
10 """
11 # 1. Handle Impossible Case:
12 # Fewer than k indices exist, so no window of length k can ever form.
13 if n < k:
14 return
15 
16 # 2. Build First Window:
17 # Add every index from 0 to k - 1 so the window holds its first k indices before any sliding starts.
18 for i in range(k):
19 add(i)
20 record(0)
21 
22 # 3. Slide One Step At A Time:
23 # Add the incoming index i, remove the outgoing index i - k, then record only after both updates land so the window always holds exactly k indices when it is read.
24 for i in range(k, n):
25 add(i)
26 remove(i - k)
27 record(i - k + 1)

Every window has the length k = len(p), so build the first window's counts once, then slide one step at a time: add the letter entering on the right and remove the one leaving on the left. Each slide changes exactly two letters of the window, so updating window_count for s[i] and s[i - k] keeps it equal to the counts of the k characters ending at i without recounting the whole window, and window_count == p_count then says whether that window is an anagram of p. The trap is the index of the leaving letter: when s[i] enters, s[i - k] leaves; an off-by-one there shifts every window, so the windows compared are the wrong ones.

Gotchas1 of 5

Gotcha 1 of 5

  1. 1
    The trap

    Gotcha 1: Removing s[i - k + 1] instead of s[i - k]

    Mistake
    window_count[ord(s[i - k + 1]) - ord('a')] -= 1
    Fix
    window_count[ord(s[i - k]) - ord('a')] -= 1

    What happens: Before the step, the window is s[i - k..i - 1]. When s[i] enters, the letter that leaves is s[i - k]. Removing s[i - k + 1] takes out a letter that is still in the window. The counts then match no real window. The result lists wrong start indices.

    Example: s = "cbaebabacd", p = "abc"
    Mistake: returns [0, 3, 4, 5]Fix: returns [0, 6]
  2. 2

    Gotcha 2: Writing the outgoing index as s[k - i] instead of s[i - k]

    Mistake
    window_count[ord(s[k - i]) - ord('a')] -= 1
    Fix
    window_count[ord(s[i - k]) - ord('a')] -= 1

    What happens: The loop runs i from k upward, and the letter that leaves at step i is s[i - k]. The index i - k grows by 1 at each step, because the window moves right. The index k - i does the opposite: it is 0 at i = k, then -1, -2 and so on. Python reads a negative index from the end of the string and raises no error. With s = "cbaebabacd" and k = 3, the first slide at i = 3 removes s[0], which is correct. At i = 4 the code removes s[-1], the last letter 'd', instead of s[1], the letter 'b'. The count of 'd' becomes -1 and the count of 'b' stays too high. From then on, window_count matches no real window, so no later window can equal p_count.

    Example: s = "cbaebabacd", p = "abc"
    Mistake: returns [0]Fix: returns [0, 6]
  3. 3

    Gotcha 3: Comparing between the add and the remove

    Mistake
    window_count[ord(s[i]) - ord('a')] += 1
    if window_count == p_count:
        result.append(i - k + 1)
    window_count[ord(s[i - k]) - ord('a')] -= 1
    Fix
    window_count[ord(s[i]) - ord('a')] += 1
    window_count[ord(s[i - k]) - ord('a')] -= 1
    if window_count == p_count:
        result.append(i - k + 1)

    What happens: After the add, the window holds k + 1 letters. p_count holds exactly k letters. So the comparison is always False at that point. Only the first window, checked before the loop, can still match.

    Example: s = "cbaebabacd", p = "abc"
    Mistake: returns [0]Fix: returns [0, 6]
  4. 4

    Gotcha 4: Dropping the len(s) < k check

    What happens: When s is shorter than p, no window of length k fits. In Python, s[:k] then returns the whole shorter string without an error. Its counts never equal p_count, so the result is still []. In Java or C++, reading k characters from a shorter string goes past its end. So return [] before the first window is built.

  5. 5

    Gotcha 5: Re-summing every window in Maximum Average Subarray I (LC 643)

    Mistake
    window_sum = sum(nums[i - k + 1:i + 1])
    Fix
    window_sum += nums[i] - nums[i - k]

    What happens: Maximum Average Subarray I (LC 643) slides a sum instead of counts. sum(nums[i - k + 1:i + 1]) reads k values at every step. The whole scan then costs O(N · k). Adding the entering value and subtracting the leaving one costs O(1) per step. Both give the same answer.

Canonical LeetCode:Maximum Average Subarray I (LC 643)Find All Anagrams in a String (LC 438)Maximum Number of Vowels in a Substring of Given Length (LC 1456)Number of Sub-arrays of Size K and Average Greater than or Equal to Threshold (LC 1343)Moving Average from Data Stream (LC 346)Minimum Swaps to Group All 1's Together (LC 1151)Permutation in String (LC 567)Contains Duplicate II (LC 219)
Open the Fixed Window card
8Longest Window
left -> right -> (Check Before Growing, Contract To Repair)

Variable-Size Sliding Window (Longest Window)

Spot it when: You want the longest contiguous subarray or substring that stays within a limit: no repeats, at most k distinct values, at most k zeros flipped.
Mode 8 · Longest Window
Python
1from typing import Callable
2 
3 
4def longest_window(n: int, add: Callable[[int], None], remove: Callable[[int], None], would_be_valid: Callable[[int], bool]) -> int:
5 """Longest Window: the length of the longest valid window [left, right] of indices 0..n-1.
6 Precondition: every shorter window inside a valid window is valid too, and the empty window is valid.
7 add(i) and remove(i) update the window's state for index i.
8 would_be_valid(i) reads whether adding index i to the current window would keep it valid, without changing the window's state.
9 T = the cost of one add, remove or would_be_valid call.
10 Time: O(N * T) | Space: O(1) besides the window's state
11 """
12 left = 0
13 max_len = 0
14 
15 for right in range(n):
16 # 1. Repair By Evicting From left:
17 # would_be_valid(right) asks whether adding index right would still be a valid window, without changing the window's state. Every window [j, right] with j < left contains everything [left, right] contains, so if adding right to [left, right] would still be invalid, no later left can keep right out of it; evicting left is the only way back to a window that right can be added to.
18 while not would_be_valid(right):
19 remove(left)
20 left += 1
21 # 2. Absorb Index right:
22 # Once would_be_valid(right) confirms the window stays valid, right is actually added.
23 add(right)
24 # 3. Record Only After The Add:
25 # The window [left, right] is now valid and fully updated, so its length can be measured.
26 max_len = max(max_len, right - left + 1)
27 
28 return max_len

Before you add right to the window, check whether adding it would break the rule. If it would, move left forward until adding right is safe, then add right and record the window's length.

The window is always valid at the moment you add right, because right is only added once the check says adding it keeps the window valid, so the largest measurement is the answer. Moving left never loses a longer answer: any window that starts before left already contains everything [left, right] contains, so adding right to it would break the rule too.

The trap is answering the check by mutating the window's state to test it: the check must read what adding right would do without changing the window, or the add call right after it double-counts right.

Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Measuring the window before the while loop repairs it

    Mistake
    for right in range(len(s)):
        max_len = max(max_len, right - left + 1)
        while s[right] in seen_chars:
    Fix
    for right in range(len(s)):
        while s[right] in seen_chars:

    What happens: Before the while loop, s[right] may already be in the window. Then the window s[left..right] holds a letter twice. A length measured there counts an invalid window, so max_len can become too large. The while loop moves left until the repeat is gone. After that loop, right - left + 1 is the length of a valid window.

    Example: s = "abcabcbb"
    Mistake: returns 4Fix: returns 3
  2. 2

    Gotcha 2: Repairing the window with if instead of while

    Mistake
    if s[right] in seen_chars:
    Fix
    while s[right] in seen_chars:

    What happens: The earlier copy of s[right] can be several places after left. One step removes only s[left], which may be a different letter. Then the window still holds the repeat when it is measured.

    Example: s = "pwwkew"
    Mistake: returns 4Fix: returns 3
  3. 3

    Gotcha 3: Keeping map keys whose count dropped to 0

    Mistake
    count[fruits[left]] -= 1
    Fix
    count[fruits[left]] -= 1
    if count[fruits[left]] == 0:
        del count[fruits[left]]

    What happens: Fruit Into Baskets (LC 904) allows at most 2 fruit types in the window. It keeps a count for each type and checks len(count). When left moves, the count of fruits[left] goes down by 1. A type whose count reaches 0 is no longer in the window. If its key stays, len(count) still counts it, and the window shrinks more than needed.

  4. 4

    Gotcha 4: Using one window to count exactly k distinct values

    What happens: Subarrays with K Different Integers (LC 992) counts the subarrays with exactly k distinct values. For one right, a whole range of starts gives exactly k values. One window tracks only one start, so it misses the others. Count the subarrays with at most k distinct values, and subtract those with at most k - 1. That is the At-Most Trick.

Canonical LeetCode:Longest Substring Without Repeating Characters (LC 3)Fruit Into Baskets (LC 904)Longest Subarray of 1's After Deleting One Element (LC 1493)Longest Substring with At Most K Distinct Characters (LC 340)Max Consecutive Ones II (LC 487)Minimum Operations to Reduce X to Zero (LC 1658)Longest Substring with At Most Two Distinct Characters (LC 159)Longest Repeating Character Replacement (LC 424)Max Consecutive Ones III (LC 1004)
Open the Longest Window card
9Fast & Slow Pointers
slow (1x) vs fast (2x) (Velocity Differential)

Fast & Slow Pointers (Floyd's Tortoise and Hare)

Spot it when: A linked list (or an array used as next = nums[i]) may contain a loop, or you need its middle in one pass with O(1) extra space.
Mode 9 · Fast & Slow Pointers
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 find_cycle_entrance(head: Optional[ListNode]) -> Optional[ListNode]:
9 """Linked List Cycle II: Floyd's tortoise and hare, then find the cycle's first node.
10 Time: O(N) | Space: O(1)
11 """
12 if not head or not head.next:
13 return None
14 
15 slow = fast = head # both pointers start together; fast will take exactly twice slow's steps
16 has_cycle = False
17 
18 # 1. Phase 1 - Detect Collision:
19 # fast gains exactly one step on slow per move, so inside a cycle it cannot jump over slow: they must meet. Testing fast and fast.next (not just fast) keeps fast.next.next from reading an attribute of None.
20 while fast and fast.next:
21 slow = slow.next # 1 step
22 fast = fast.next.next # 2 steps
23 if slow == fast:
24 has_cycle = True
25 break
26 
27 if not has_cycle:
28 return None # fast fell off the end: no cycle
29 
30 # 2. Phase 2 - Locate Cycle Origin:
31 # head and the meeting point are the same number of steps from the cycle's first node, up to whole laps, so walking both one step at a time lands them on it together.
32 ptr1 = head
33 ptr2 = slow
34 while ptr1 != ptr2:
35 ptr1 = ptr1.next
36 ptr2 = ptr2.next
37 
38 return ptr1

Inside a cycle, fast gains exactly one step on slow per move, so it can't jump over it: if there is a cycle they meet, and if fast runs off the end there is none (and slow is then at the middle).

Walking from the head and walking from the meeting point reach the cycle's first node after the same number of steps (the second walk may add whole laps), so ptr1 from head and ptr2 from the meeting point, one step at a time, both land on it.

The trap is the loop test: write while fast and fast.next, because fast.next.next crashes when fast.next is None; testing only fast crashes on every list without a cycle that has an odd number of nodes.

Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Writing while fast instead of while fast and fast.next

    Mistake
    while fast:
    Fix
    while fast and fast.next:

    What happens: Each step moves fast two nodes with fast.next.next. When fast is the last node, fast.next is None. Then fast.next.next reads an attribute of None, and Python raises an AttributeError. This happens on every list without a cycle that has an odd number of nodes.

    Example: head = [1, 2, 3], pos = -1 (no cycle)
    Mistake: raises AttributeErrorFix: returns None
  2. 2

    Gotcha 2: Moving ptr2 two steps at a time in phase 2

    Mistake
    ptr2 = ptr2.next.next
    Fix
    ptr2 = ptr2.next

    What happens: head and the meeting point are the same number of steps from the cycle's first node, counting whole laps. That holds only when both pointers move one step at a time. With two steps for ptr2, the pointers can meet at another node.

    Example: head = [3, 2, 0, -4], pos = 1 (the cycle starts at the node 2)
    Mistake: returns -4Fix: returns 2
  3. 3

    Gotcha 3: Expecting slow to stop at the first of two middles

    What happens: Without a cycle, slow is at the middle when fast runs off the end. With an even number of nodes, there are two middles. Starting with slow = fast = head stops slow at the second one. Start fast at head.next to stop slow at the first one.

  4. 4

    Gotcha 4: Starting the pointers at an index other than 0

    What happens: Find the Duplicate Number (LC 287) reads the array as a linked list. Index i points to index nums[i]. Every value is between 1 and n, so no index points to index 0. So index 0 is outside the cycle, and the cycle's first node is the duplicate value. From another index, the walk can start inside the cycle and return a value that is not the duplicate.

Canonical LeetCode:Linked List Cycle II (LC 142)Linked List Cycle (LC 141)Find the Duplicate Number (LC 287)Middle of the Linked List (LC 876)Circular Array Loop (LC 457)Reorder List (LC 143)Happy Number (LC 202)
Open the Fast & Slow Pointers card
10Merge Two Sorted
two sorted sequences ➔ take the smaller head, move only its pointer ➔ drain the other sequence ➔ one pass

Two-Sequence Merge Pointers

Spot it when: Two sorted arrays or lists must be read together in one pass: merged into one order, intersected, compared value by value, or joined at the values they share.
Mode 10 · Merge Two Sorted
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 merge_two_lists(list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
9 """Merge Two Sorted Lists: relink the smaller head of the two lists, one node at a time.
10 Time: O(M + N) | Space: O(1) extra
11 """
12 # list1 and list2 are the two pointers: each one is the first unread node of its own list
13 dummy = ListNode(0) # Fake first node, so the first real node is attached like every other
14 tail = dummy # Last node of the merged list built so far
15 
16 # 1. Take The Smaller Head:
17 # Both lists are sorted, so the smaller head is the smallest unread node of both. Attach it, then move only the pointer of the list it came from.
18 while list1 and list2:
19 if list1.val <= list2.val:
20 tail.next = list1
21 list1 = list1.next
22 else:
23 tail.next = list2
24 list2 = list2.next
25 tail = tail.next
26 
27 # 2. Attach The Rest Of The Other List:
28 # The loop stops when one list is empty. The other list is already sorted and already linked, so one assignment attaches all of it.
29 tail.next = list1 if list1 else list2
30 
31 # 3. Return The Node After The Dummy:
32 # dummy only marks the start, so the merged list begins at dummy.next.
33 return dummy.next

Each list is sorted, so its first unread node is its smallest, and the smaller of list1 and list2 is the smallest unread node of both lists: attach that node to tail and move only the pointer of the list it came from. The other pointer stays where it is, because its node is not smaller than the node just attached and still has to be compared with the next node of the other list. Each step attaches the smallest node that is left, so the merged list grows in sorted order and every node is attached exactly once. When list1.val equals list2.val, <= takes the node from list1 first, so equal values keep the order in which the two lists were given. The trap is the end of the loop: while list1 and list2 stops as soon as one list is empty, and the other list still holds unread nodes. Attach them with tail.next = list1 if list1 else list2, or every node after the last comparison is lost. Two arrays have no links to reuse, so they need a drain loop that copies the leftover values one by one.

Gotchas1 of 5

Gotcha 1 of 5

  1. 1
    The trap

    Gotcha 1: Leaving out the attach of the other list after the loop

    Mistake
    # nothing after the loop
    Fix
    tail.next = list1 if list1 else list2

    What happens: while list1 and list2 stops as soon as one list is empty. The other list still holds nodes that were never compared. Without the line tail.next = list1 if list1 else list2, tail.next stays None and those nodes are not in the result.

    Example: list1 = [1, 2, 4], list2 = [1, 3, 4]
    Mistake: returns [1, 1, 2, 3, 4]Fix: returns [1, 1, 2, 3, 4, 4]
  2. 2

    Gotcha 2: Looping while either list has a node

    Mistake
    while list1 or list2:
    Fix
    while list1 and list2:

    What happens: Inside the loop, list1.val <= list2.val reads the val of both heads. When one list is empty, its pointer is None and None.val raises an error. The loop must stop when either list is empty, and the next line attaches the other list.

    Example: list1 = [5], list2 = [1, 2, 6]
    Mistake: raises AttributeErrorFix: returns [1, 2, 5, 6]
  3. 3

    Gotcha 3: Using < so that a tie takes the node from list2

    Mistake
    if list1.val < list2.val:
    Fix
    if list1.val <= list2.val:

    What happens: When both heads hold the same value, < is False, so the else branch attaches the node from list2 first. The values in the result are the same, but equal nodes from list1 now come after equal nodes from list2. This matters when each node carries more data than its value, because a stable merge keeps equal keys in the order the two lists were given.

    Example: list1 = [1] (node a), list2 = [1] (node b)
    Mistake: returns FalseFix: returns True
  4. 4

    Gotcha 4: Forgetting to move tail after an attach

    Mistake
    pass
    Fix
    tail = tail.next

    What happens: tail must always be the last node of the merged list. If it stays on dummy, every attach overwrites dummy.next, so each new node replaces the one attached before it. Only the node from the last assignment survives, together with the nodes that follow it in its own list.

    Example: list1 = [1, 2, 4], list2 = [1, 3, 4]
    Mistake: returns [4]Fix: returns [1, 1, 2, 3, 4, 4]
  5. 5

    Gotcha 5: Returning dummy instead of dummy.next

    Mistake
    return dummy
    Fix
    return dummy.next

    What happens: dummy is the fake node with value 0 that sits before the first real node. Returning dummy returns a list that starts with this extra 0, so the result is one node longer than the merge.

    Example: list1 = [1, 2, 4], list2 = [1, 3, 4]
    Mistake: returns [0, 1, 1, 2, 3, 4, 4]Fix: returns [1, 1, 2, 3, 4, 4]
Canonical LeetCode:Merge Two Sorted Lists (LC 21)Merge Sorted Array (LC 88)Get the Maximum Score (LC 1537)Interval List Intersections (LC 986)Minimum Common Value (LC 2540)Meeting Scheduler (LC 1229)
Open the Merge Two Sorted card
11Linked List Reversal
head ➔ rewire each next pointer ➔ reversed list

In-Place Linked List Reversal (Reverse Linked List II, Reverse Nodes in k-Group)

Spot it when: You must reverse all or part of a singly linked list in place: the whole list, the nodes between two positions, every group of k nodes, or the second half before comparing or merging it with the first.
Mode 11 · Linked List Reversal
Python
1from typing import Optional
2 
3 
4class ListNode:
5 def __init__(self, val: int = 0, next: Optional["ListNode"] = None):
6 self.val = val
7 self.next = next
8 
9 
10def reverse_list(head: Optional[ListNode]) -> Optional[ListNode]:
11 """Reverse Linked List: flip every next pointer in one pass.
12 Time: O(N) | Space: O(1)
13 """
14 prev = None # Head of the reversed part: empty at the start
15 curr = head # First node not flipped yet
16 while curr:
17 nxt = curr.next # Save the rest BEFORE the flip overwrites curr.next
18 curr.next = prev # Flip: curr now points back at the reversed part
19 prev = curr # The reversed part grows by one node
20 curr = nxt # Step into the saved rest
21 return prev # The old tail is the new head
22 
23 
24def reverse_between(head: Optional[ListNode], left: int, right: int) -> Optional[ListNode]:
25 """Reverse Linked List II: reverse positions left..right (1-indexed) in one pass.
26 Time: O(N) | Space: O(1)
27 """
28 dummy = ListNode(0, head) # The segment may start at the head: give it a node before it
29 before = dummy
30 for _ in range(left - 1): # Walk to the node just before position left
31 before = before.next
32 prev = None
33 curr = before.next
34 for _ in range(right - left + 1): # The same flip, exactly right - left + 1 times
35 nxt = curr.next
36 curr.next = prev
37 prev = curr
38 curr = nxt
39 tail = before.next # The segment's old first node is now its last
40 before.next = prev # Reattach the reversed segment...
41 tail.next = curr # ...and hang the untouched rest behind it
42 return dummy.next

Reversing a singly linked list only needs every next pointer to point the other way, so walk the list once with two pointers: prev heads the part already reversed (it starts as None) and curr is the first node not flipped yet; each step points curr.next back at prev, then moves prev and curr one node forward. Each step moves exactly one node from the front of the untouched part to the front of the reversed part, so no node is lost or visited twice, and when curr falls off the end, prev is the new head. reverse_between runs the same steps right - left + 1 times from the node before position left, then reconnects both ends with before.next = prev and tail.next = curr. The trap is the order of the lines: curr.next = prev overwrites the only link to the rest of the list, so save nxt = curr.next first; flip first and every node after curr is lost.

Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Writing curr.next = prev before saving nxt

    Mistake
    while curr:
        curr.next = prev
        nxt = curr.next
    Fix
    while curr:
        nxt = curr.next
        curr.next = prev

    What happens: curr.next is the only link from curr to the rest of the list. curr.next = prev overwrites that link. Then nxt = curr.next saves prev, which is None on the first step. The loop ends after one node, and every node after curr is lost.

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

    Gotcha 2: Returning head instead of prev

    Mistake
    return head
    Fix
    return prev

    What happens: When the loop ends, curr is None, and prev is the old last node. That node is the new head. The old head is now the tail, and its next is None. Returning it gives a list with one node.

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

    Gotcha 3: Starting before at head instead of dummy

    Mistake
    before = head
    Fix
    before = dummy

    What happens: before must be the node just before position left. When left is 1, that is the node before the head. Only dummy can be there. Starting at head puts before one node too far, so the wrong segment is reversed.

    Example: head = [1, 2, 3, 4, 5], left = 1, right = 2
    Mistake: returns [1, 3, 2, 4, 5]Fix: returns [2, 1, 3, 4, 5]
  4. 4

    Gotcha 4: Flipping a last group shorter than k

    Mistake
    while node and count < k:
        node = node.next
        count += 1
    Fix
    while node and count < k:
        node = node.next
        count += 1
    if count < k:
        return dummy.next

    What happens: In Reverse Nodes in k-Group (LC 25), a last group with fewer than k nodes must keep its order. The count loop stops when the list ends, so count is less than k for that group. Without if count < k, the flip loop still reverses those count nodes. The last group comes back reversed.

    Example: head = [1, 2, 3, 4, 5], k = 3
    Mistake: returns [3, 2, 1, 5, 4]Fix: returns [3, 2, 1, 4, 5]
Canonical LeetCode:Reverse Linked List (LC 206)Reverse Linked List II (LC 92)Palindrome Linked List (LC 234)Reorder List (LC 143)Reverse Nodes in k-Group (LC 25)
Open the Linked List Reversal card
12Read-Write Pointers
reader -> writer -> (Compaction Frontier)

Read/Write Pointers (In-Place Array Modification, Dutch National Flag)

Spot it when: You must remove, deduplicate or move elements in place and return the new length (or leave the array partitioned).
Mode 12 · Read-Write Pointers
Python
1from typing import Callable, List, TypeVar
2 
3Item = TypeVar("Item")
4 
5 
6def read_and_write(items: List[Item], keep: Callable[[Item, int], bool]) -> int:
7 """Read-Write Pointers: move every kept item to the front of items, in order, in place.
8 keep(value, writer) returns True to keep value. writer is the number of items kept so far.
9 items[:writer] holds those items, so keep may compare value with items[writer - 1].
10 Precondition: keep reads only value and items[:writer], never a slot at writer or later.
11 Returns writer. The slots from writer on still hold old values. Move Zeroes writes its zeros there afterwards.
12 N = len(items). T = the cost of one keep call.
13 Time: O(N * T) | Space: O(1)
14 """
15 writer = 0 # items[:writer] holds the kept items, in order
16 # 1. Read Every Item, Write Only The Kept Ones:
17 # writer never gets ahead of reader, so items[writer] = items[reader] always lands on a slot whose old value has already been read, never one still waiting to be read.
18 for reader in range(len(items)):
19 if keep(items[reader], writer):
20 items[writer] = items[reader]
21 writer += 1
22 
23 return writer # The number of kept items

writer marks where the next kept element goes; everything before it is final. reader visits every element and copies it to writer only when it should be kept.

Since writer never passes reader, a write never lands on an element that still has to be read, so the array can be rewritten in place. For sorted deduplication, start both at 1, because nums[0] is always kept.

The trap is comparing with the wrong slot: check nums[reader] against nums[writer - 1], the last element kept, not nums[writer], the next free slot, which still holds an old value; on LeetCode Example 2 of Remove Duplicates from Sorted Array (LC 26), [0,0,1,1,1,2,2,3,3,4], the wrong slot keeps 7 values instead of 5.

Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Comparing with items[writer] instead of items[writer - 1]

    Mistake
    if nums[reader] != nums[writer]:
    Fix
    if nums[reader] != nums[writer - 1]:

    What happens: nums[writer - 1] is the last value kept. nums[writer] is the next free slot. It still holds an old value from the input. Comparing with that old value can keep a duplicate. It can also drop a value that is new.

    Example: Remove Duplicates from Sorted Array (LC 26), Example 2: nums = [0, 0, 1, 1, 1, 2, 2, 3, 3, 4]
    Mistake: returns 7Fix: returns 5
  2. 2

    Gotcha 2: Comparing with items[reader - 2] instead of items[writer - 2]

    Mistake
    if nums[reader] != nums[reader - 2]:
    Fix
    if nums[reader] != nums[writer - 2]:

    What happens: Remove Duplicates from Sorted Array II (LC 80) keeps at most two copies of each value. A value is kept when it differs from nums[writer - 2], the second-to-last value kept. nums[reader - 2] may already hold a value that the writer copied there. Then the check compares with the wrong value and drops a copy that should stay.

  3. 3

    Gotcha 3: Leaving old values after writer in Move Zeroes (LC 283)

    Mistake
    nums[writer] = nums[reader]
    Fix
    nums[writer], nums[reader] = nums[reader], nums[writer]

    What happens: Move Zeroes (LC 283) must end with every zero after the non-zero values. Copying nums[reader] to nums[writer] leaves the old values after writer in place. Swap the two values instead. You can also write zeros from writer to the end after the loop. Either way, the non-zero values keep their order.

  4. 4

    Gotcha 4: Moving mid after a swap with high

    Mistake
    nums[mid], nums[high] = nums[high], nums[mid]
    high -= 1
    mid += 1
    Fix
    nums[mid], nums[high] = nums[high], nums[mid]
    high -= 1

    What happens: Sort Colors (LC 75) uses three pointers: low, mid and high. When nums[mid] is 2, it is swapped with nums[high], and high moves left. The value that arrives at mid has not been checked yet. Moving mid past it can leave a 0 or a 2 in the middle.

Canonical LeetCode:Remove Duplicates from Sorted Array (LC 26)Remove Duplicates from Sorted Array II (LC 80)Move Zeroes (LC 283)Sort Colors (LC 75)Partition Array According to Given Pivot (LC 2161)Remove Element (LC 27)Sort Array By Parity (LC 905)Rearrange Array Elements by Sign (LC 2149)
Open the Read-Write Pointers card
13Prefix Sums
nums ➔ prefix with n + 1 slots, prefix[0] = 0 ➔ prefix[right + 1] - prefix[left]

Prefix Sum Array

Spot it when: The array does not change, and you must answer many questions about the sum, count or XOR of a given range left..right. Or you need the running total at each position, counted from the start.
Mode 13 · Prefix Sums
Python
1class NumArray:
2 """Range Sum Query - Immutable: build the prefix array once, then answer each range with one subtraction.
3 Time: O(N) to build, O(1) per sumRange | Space: O(N)
4 """
5 
6 def __init__(self, nums: list[int]) -> None:
7 # prefix[i]: sum of the first i elements of nums; prefix[0] = 0, the sum of no elements
8 self.prefix = [0] * (len(nums) + 1)
9 # 1. Build The Prefix Array Once:
10 # Each next slot adds one more element to the previous running total, so prefix[i + 1] is always one element ahead of prefix[i].
11 for i, num in enumerate(nums):
12 self.prefix[i + 1] = self.prefix[i] + num
13 
14 def sumRange(self, left: int, right: int) -> int:
15 # 1. Subtract The Two Running Totals:
16 # The first right + 1 elements are the first left elements followed by nums[left..right], so removing the first left elements' sum leaves exactly that range.
17 return self.prefix[right + 1] - self.prefix[left]

Build self.prefix once, with one more slot than nums has elements. self.prefix[0] = 0, and each next slot adds one element: self.prefix[i + 1] = self.prefix[i] + num. Then sumRange answers any range with one subtraction, self.prefix[right + 1] - self.prefix[left]. This is correct because the first right + 1 elements are the first left elements followed by nums[left..right]. Subtracting the sum of the first left elements leaves exactly the range. The leading 0 is the sum of no elements, so left = 0 uses the same formula with no special case. The trap is the index shift. self.prefix[i] stops just before nums[i], so the range ends at self.prefix[right + 1]. Writing self.prefix[right] leaves nums[right] out of every answer.

Gotchas1 of 3

Gotcha 1 of 3

  1. 1
    The trap

    Gotcha 1: Writing self.prefix[right] instead of self.prefix[right + 1]

    Mistake
    return self.prefix[right] - self.prefix[left]
    Fix
    return self.prefix[right + 1] - self.prefix[left]

    What happens: self.prefix[i] is the sum of the elements before index i. So self.prefix[right] stops just before nums[right]. The subtraction then returns nums[left..right - 1], one element short. On a one-element range it returns 0.

    Example: nums = [-2, 0, 3, -5, 2, -1], sumRange(0, 2)
    Mistake: returns -2Fix: returns 1
  2. 2

    Gotcha 2: Dropping the leading 0 and reading self.prefix[left - 1]

    Mistake
    self.prefix = []  # prefix[i] = sum of nums[0..i]
    ...
    return self.prefix[right] - self.prefix[left - 1]
    Fix
    self.prefix = [0] * (len(nums) + 1)
    ...
    return self.prefix[right + 1] - self.prefix[left]

    What happens: Without the leading 0, the list has n slots and self.prefix[i] includes nums[i]. A range then needs self.prefix[left - 1]. When left = 0, that is self.prefix[-1]. Python reads the last slot, the sum of the whole array, and raises no error. So a range that starts at index 0 is wrong whenever the whole array does not sum to 0.

    Example: nums = [-2, 0, 3, -5, 2, -1], sumRange(0, 2)
    Mistake: returns 4Fix: returns 1
  3. 3

    Gotcha 3: Adding up the range inside sumRange

    Mistake
    self.nums = nums
    ...
    return sum(self.nums[left : right + 1])
    Fix
    return self.prefix[right + 1] - self.prefix[left]

    What happens: The answers stay correct, but each call reads every element of its range. With 10^4 elements and 10^4 calls, that is up to 10^8 additions. The prefix array does the adding once, in __init__. After that, every call is one subtraction.

Canonical LeetCode:Range Sum Query - Immutable (LC 303)Range Sum Query 2D - Immutable (LC 304)Matrix Block Sum (LC 1314)Find the Highest Altitude (LC 1732)Number of Ways to Split Array (LC 2270)Count Vowel Strings in Ranges (LC 2559)XOR Queries of a Subarray (LC 1310)Special Array II (LC 3152)Find Pivot Index (LC 724)Running Sum of 1d Array (LC 1480)Minimum Value to Get Positive Step by Step Sum (LC 1413)
Open the Prefix Sums card
14Greedy Subsequence Match
i -> s, j -> t (claim s[i] only on a match, i reaches len(s))

Subsequence Matching (Earliest-Match Two Pointers)

Spot it when: You must decide whether every character of one string, in order, can be found somewhere inside a second, usually longer string, and skipping characters of the second string is always allowed.
Mode 14 · Greedy Subsequence Match
Python
1def isSubsequence(s: str, t: str) -> bool:
2 """Is Subsequence: walk t once with j, and advance i in s only when the current characters match.
3 Time: O(len(t)) | Space: O(1)
4 """
5 i = 0 # Characters of s already claimed, in order
6 for j in range(len(t)):
7 if i < len(s) and s[i] == t[j]:
8 i += 1 # The earliest available match for s[i] in t never costs a later match
9 
10 return i == len(s)

i counts how many characters of s are already claimed, in order; walking t once with j, advance i only when t[j] equals s[i], the next character of s still unclaimed. s is a subsequence of t exactly when i reaches len(s) by the time the walk ends. Claiming the earliest copy of s[i] in t is always safe: every character of t after that point is still free for the rest of s, so no earlier choice can ever cost a later match, and a later copy would only leave less of t behind for what comes next. The trap is advancing i on every character of t instead of only on a match: i += 1 outside the if claims characters that were never actually matched, so the final check i == len(s) can pass even when s is not really a subsequence of t.

Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Advancing i on every character of t, not only on a match (the card's trap)

    Mistake
    for j in range(len(t)):
        i += 1
    Fix
    for j in range(len(t)):
        if i < len(s) and s[i] == t[j]:
            i += 1

    What happens: i is supposed to count only the characters of s that were actually matched in t, in order. Moving i += 1 outside the if makes i count every character of t instead. i then reaches len(s) as soon as t is at least as long as s, no matter what characters either string holds, so the final check i == len(s) says True for pairs that are not a real subsequence match.

    Example: s = "abc", t = "ahbgdc"
    Mistake: returns FalseFix: returns True
  2. 2

    Gotcha 2: Writing i == len(s) - 1 instead of i == len(s)

    Mistake
    return i == len(s) - 1
    Fix
    return i == len(s)

    What happens: i reaches len(s) only once every character of s is claimed, since i starts at 0 and the last character claimed brings it to len(s), not len(s) - 1. Comparing against len(s) - 1 rejects a correct full match, one claim short of where the mistake expects it.

    Example: s = "abc", t = "ahbgdc"
    Mistake: returns FalseFix: returns True
  3. 3

    Gotcha 3: Returning True as soon as the scan of t finishes, instead of checking i

    Mistake
    if j == len(t) - 1:
        return True
    Fix
    return i == len(s)

    What happens: Reaching the last index of t only means the walk is over; it says nothing about whether every character of s was claimed. Returning True there confuses "finished scanning t" with "matched all of s", so the function reports a match for every non-empty t, even when most of s was never found.

    Example: s = "axc", t = "ahbgdc"
    Mistake: returns TrueFix: returns False
  4. 4

    Gotcha 4: Checking s[i] in t instead of walking t in order

    Mistake
    for ch in s:
        if ch in t:
            i += 1
    Fix
    for j in range(len(t)):
        if i < len(s) and s[i] == t[j]:
            i += 1

    What happens: ch in t only asks whether the character exists somewhere in t, with no memory of how far through t the previous match reached. Two characters of s can then both match the same early position of t, or match positions of t in the wrong order, and the mistake still counts both as claimed. The order that s must appear in is never checked.

    Example: s = "ba", t = "ab"
    Mistake: returns TrueFix: returns False
Canonical LeetCode:Is Subsequence (LC 392)Longest Word in Dictionary through Deleting (LC 524)Append Characters to String to Make Subsequence (LC 2486)Make String a Subsequence Using Cyclic Increments (LC 2825)
Open the Greedy Subsequence Match card
15Circular Array
circular array ➔ walk indices 0..2n-1 with i % n ➔ see past the end

Circular Array Handling (Virtual Doubling)

Spot it when: The array is circular (the end wraps to the start) and a window, a stack or a scan must see past the end.
Mode 15 · Circular Array
Python
1def next_greater_elements(nums: list[int]) -> list[int]:
2 """Next Greater Element II: scan the virtually doubled array right to left.
3 Time: O(N) | Space: O(N)
4 """
5 n = len(nums)
6 res = [-1] * n
7 stack: list[int] = [] # Candidate values, decreasing from bottom to top
8 
9 # 1. Scan The Doubled Array Right To Left:
10 # i % n reads the circular array twice without copying it, so by the time i reaches a real index, every value that wraps around to its right has already been seen.
11 for i in range(2 * n - 1, -1, -1):
12 val = nums[i % n]
13 # 2. Drop Candidates val Blocks:
14 # A stacked value that is not strictly greater than val can never be the answer for anything further left, since val stands in front of it and is at least as big.
15 while stack and stack[-1] <= val:
16 stack.pop()
17 # 3. Record Only On The Real Pass:
18 # i >= n is only priming the stack with the wrap-around values, so the answer is written exclusively when i < n, a real index.
19 if i < n:
20 res[i] = stack[-1] if stack else -1
21 stack.append(val)
22 
23 return res

A circular array written twice in a row contains every wrap-around, so loop i from 2 * n - 1 down to 0 and read nums[i % n] instead of copying. Scanning right to left, the steps with i >= n fill stack with the values that wrap around, so when i reaches a real index, everything after it on the circle has already been seen; that is why res[i] is written only when i < n. The trap is equal values: pop while stack[-1] <= val, or a value equal to val stays on top and is reported as its next greater element.

Gotchas1 of 3

Gotcha 1 of 3

  1. 1
    The trap

    Gotcha 1: Popping with stack[-1] < val instead of <=

    Mistake
    while stack and stack[-1] < val:
    Fix
    while stack and stack[-1] <= val:

    What happens: A value equal to val is not greater than val. With <, an equal value stays on the stack. It is then on top, and it is reported as the next greater element.

    Example: nums = [5, 5, 5]
    Mistake: returns [5, 5, 5]Fix: returns [-1, -1, -1]
  2. 2

    Gotcha 2: Writing res[i] without the i < n check

    Mistake
    res[i] = stack[-1] if stack else -1
    Fix
    if i < n:
        res[i] = stack[-1] if stack else -1

    What happens: The loop runs i from 2 * n - 1 down to 0, but res has only n slots. For i >= n, res[i] is past the end, and Python raises an IndexError. Those first n steps only fill the stack with the values that wrap around.

    Example: nums = [1, 2, 1]
    Mistake: raises IndexErrorFix: returns [2, -1, 2]
  3. 3

    Gotcha 3: Building nums + nums instead of reading nums[i % n]

    What happens: nums + nums makes a real copy of the array, which costs O(N) extra memory. nums[i % n] reads the same values from the one array. The answers are the same. Circular windows read nums[i % n] in the same way. Two examples are Defuse the Bomb (LC 1652) and Minimum Swaps to Group All 1's Together II (LC 2134).

Canonical LeetCode:Next Greater Element II (LC 503)Minimum Swaps to Group All 1's Together II (LC 2134)Defuse the Bomb (LC 1652)Alternating Groups II (LC 3208)Design Circular Queue (LC 622)Maximum Sum Circular Subarray (LC 918)
Open the Circular Array card
Built on the mental models above
  • Hash-Set Sequence ScanningStart-Element Pruning (Longest Consecutive Sequence)

    Store the values in a set and walk a run only from its first value, so every value is visited once.

  • Counting SortCounting Sort (H-Index, Maximum Ice Cream Bars)

    The values are small whole numbers, so each value gets its own slot instead of a range of values.

  • Longest Palindromic SubstringExpand Around Center (Longest Palindromic Substring)

    Start both pointers at a center and move them outward while the two ends still match.

  • Staircase SearchSaddleback Search (Staircase Search in a Sorted Matrix)

    The pointers are a row and a column in a sorted grid, and each comparison discards a whole row or a whole column.

  • Next PermutationNext Lexicographic Permutation (Narayana Pandita's Algorithm, std::next_permutation)

    Find the pivot from the right, swap it with the next larger value, then reverse the suffix with two pointers.

  • Three-Reversals AlgorithmIn-Place Array Rotation (Rotate Array)

    Reverse the whole range, then reverse each of the two parts, so the items rotate in place.

  • Monotonic DequeMonotonic Deque (Sliding Window Maximum)

    The window state is a deque of candidate indices whose values only decrease, so the window maximum is always at the front.

  • Dual Monotonic DequeDual Monotonic Deque (Longest Subarray Within a Max-Min Limit)

    Keep a second deque for the minimum, and move the left end while the maximum minus the minimum is above the limit.

  • Shortest WindowVariable-Size Sliding Window (Minimum Window)

    Shrink the window while it is still valid and record its length each time, instead of shrinking only when it becomes invalid.

  • At-Most TrickExactly K = At Most K − At Most (K−1)

    Count the windows that end at each right end with an at-most count, then subtract two at-most counts to get an exact count.

  • Fixed-Offset PointersFixed-Offset Pointers (Nth Node From End)

    Start one pointer n steps ahead, then move both one step at a time, so the gap between them stays fixed.

  • Cyclic SortCyclic Sort (Index as Hash, First Missing Positive)

    Swap each value to the index where it belongs, so the array itself records which values are present.

  • Prefix-Suffix ScanPrefix & Suffix Passes (Left-Right Scans)

    The operation has no inverse, so keep one pass from the left and one pass from the right instead of subtracting prefixes.

  • Prefix Sum MapPrefix Sum + Hash Map

    Keep a map from each earlier prefix sum to how often it occurred, so a range with the target sum is found in one pass.

  • Difference ArrayDifference Array (Range Updates)

    Write a mark at each end of a range, then take one prefix sum to read the total at every position.

The Two Pointers Decision Rule (Problem Classification Checklist)

When facing an unseen array, string or linked-list problem, ask in this order:

Step 1.Does the array never change, and are you told the ranges (`left` and `right` are given) and asked for their sum, count or XOR? Or do you only need the running total at each position?
➔ Prefix Sums
Range Sum Query - Immutable (LC 303)Range Sum Query 2D - Immutable (LC 304)

Build prefix with n + 1 slots and prefix[0] = 0, where prefix[i + 1] = prefix[i] + nums[i]; then the sum of nums[left..right] is prefix[right + 1] - prefix[left].

Step 2.Do you count or find subarrays with an exact sum (or a sum divisible by k, or equal 0s and 1s), especially with negative values?
➔ Prefix Sum Map
Subarray Sum Equals K (LC 560)Contiguous Array (LC 525)

Keep a map of prefix sums seen, seeded with {0: 1}; at each prefix p, add count[p - k] before recording p.

Step 3.Is the window length k given, and you measure every window of that length?
➔ Fixed Window
Find All Anagrams in a String (LC 438)Maximum Average Subarray I (LC 643)

Add nums[i]; once i >= k, remove nums[i - k]; read the window after both.

Step 4.Do you want the longest contiguous subarray or substring that stays within a limit (no repeats, at most k distinct, at most k zeros)?
➔ Longest Window
Longest Substring Without Repeating Characters (LC 3)Fruit Into Baskets (LC 904)

Before growing right, check whether adding it would break the limit; while it would, shrink left; once it would not, grow right and record right - left + 1.

Step 5.Do you want the shortest contiguous subarray or substring that meets a condition, where adding elements only makes it easier to meet?
➔ Shortest Window
Minimum Size Subarray Sum (LC 209)Minimum Window Substring (LC 76)

Grow right until the window is valid; while it stays valid, record its length and shrink left.

Step 6.Must you count subarrays with exactly K of something, where adding an element never lowers that amount?
➔ At-Most Trick
Subarrays with K Different Integers (LC 992)Binary Subarrays With Sum (LC 930)

exactly(K) = atMost(K) - atMost(K - 1), where each atMost window adds right - left + 1 per right.

Step 7.Does every position of a sliding window need the window's maximum or minimum?
➔ Monotonic Deque
Sliding Window Maximum (LC 239)Shortest Subarray with Sum at Least K (LC 862)

Keep a deque of indices with decreasing values: pop smaller values from the back, drop the front once it leaves the window; the front is the maximum.

Step 8.Does the window's validity depend on its maximum AND its minimum at once, such as staying valid only while max minus min stays within a limit?
➔ Dual Monotonic Deque
Longest Continuous Subarray With Absolute Diff Less Than or Equal to Limit (LC 1438)Continuous Subarrays (LC 2762)

Run two deques side by side, one decreasing (front = max) and one increasing (front = min); while the gap between the fronts exceeds the limit, shrink from the left, checking both fronts.

Step 9.Is the array sorted (or can it be), and you look for pairs or triples meeting a target, or compare the two ends?
➔ Converging Pointers
Two Sum II - Input Array Is Sorted (LC 167)3Sum (LC 15)

Start left = 0, right = n - 1 and move the pointer whose side can no longer be part of an answer.

Step 10.Must two sorted arrays or lists be read together in one pass: merged, intersected or compared?
➔ Merge Two Sorted
Merge Two Sorted Lists (LC 21)Merge Sorted Array (LC 88)

Keep one pointer per sequence, take the smaller head and advance only that pointer; drain the leftovers.

Step 11.Are the matrix's rows and columns both sorted, and you must find a value or walk the boundary between too small and too big?
➔ Staircase Search
Search a 2D Matrix II (LC 240)Find Positive Integer Solution for a Given Equation (LC 1237)

Start at the top-right corner: a value too big moves left, a value too small moves down.

Step 12.Must elements be removed, deduplicated or moved in place, returning the new length?
➔ Read-Write Pointers
Remove Duplicates from Sorted Array (LC 26)Remove Duplicates from Sorted Array II (LC 80)

The reader scans every element; the writer marks where the next kept element goes; copy on keep.

Step 13.Are the values meant to be 1..n in an array of length n, and you must find what is missing or repeated with O(1) extra space?
➔ Cyclic Sort
First Missing Positive (LC 41)Find All Numbers Disappeared in an Array (LC 448)

Swap each value v into index v - 1 until it is home or a duplicate; then the first i with nums[i] != i + 1 answers.

Step 14.Does each position's answer depend on everything to its left and everything to its right?
➔ Prefix-Suffix Scan
Product of Array Except Self (LC 238)Trapping Rain Water (LC 42)

One pass left to right fills the prefix part, one pass right to left combines the suffix part; neither includes nums[i].

Step 15.Must many range updates be applied before any value is read?
➔ Difference Array
Corporate Flight Bookings (LC 1109)Range Addition (LC 370)

diff[l] += v and diff[r + 1] -= v per update, then one running sum recovers every value.

Step 16.Is the question 'have I seen this before?', 'how many of each?' or 'which items belong together?'?
➔ Hash Map
Group Anagrams (LC 49)Two Sum (LC 1)

Choose the key (the value, its complement, or a canonical form) and read or update dict[key] in O(1) per item.

Step 17.Do you need the longest run of consecutive integers that all appear in an unsorted array, in O(n) time?
➔ Hash-Set Sequence Scanning
Longest Consecutive Sequence (LC 128)Longest Square Streak in an Array (LC 2501)

Put every value in a hash set; for each value whose predecessor is absent, walk upward while the next value is present, and keep the longest walk.

Step 18.Must you decide whether one string's characters all appear, in order, somewhere inside a second, usually longer string, with skipping always allowed?
➔ Greedy Subsequence Match
Is Subsequence (LC 392)Longest Word in Dictionary through Deleting (LC 524)

Walk the second string once with j; advance i in the first string only when the current characters match; the first string is matched once i reaches its length.

Step 19.Is the array circular, so a scan, a window or a stack must see past the end?
➔ Circular Array
Next Greater Element II (LC 503)Minimum Swaps to Group All 1's Together II (LC 2134)

Loop i over 0..2n - 1 and read nums[i % n], recording answers only for i < n.

Step 20.Might a linked list (or next = nums[i]) contain a loop, or do you need its middle in one pass with O(1) space?
➔ Fast & Slow Pointers
Linked List Cycle II (LC 142)Linked List Cycle (LC 141)

slow moves 1 step, fast moves 2; they meet inside a cycle; restart one at head and move both 1 step to meet at the entry.

Step 21.Must you reach or remove the node that is a given number of nodes from the end of a linked list, in one pass?
➔ Fixed-Offset Pointers
Remove Nth Node From End of List (LC 19)Rotate List (LC 61)

Move lead n steps ahead of trail, then move both one step at a time; when lead is the last node, trail is n steps behind it.

Step 22.May the head of a linked list be removed, replaced or not known yet?
➔ Dummy Node
Remove Linked List Elements (LC 203)Merge Two Sorted Lists (LC 21)

Set dummy.next = head, edit through prev starting at dummy, and return dummy.next.

Step 23.Must all or part of a singly linked list be reversed in place?
➔ Linked List Reversal
Reverse Linked List (LC 206)Reverse Linked List II (LC 92)

Save nxt = curr.next, point curr.next = prev, then move prev and curr forward.

Step 24.Do you need the longest palindromic substring, or how many substrings are palindromes?
➔ Longest Palindromic Substring
Longest Palindromic Substring (LC 5)Palindromic Substrings (LC 647)

Expand around each of the 2n - 1 centers (every letter and every gap) while the two ends match.

Step 25.Must an array be rotated or cyclically shifted in place?
➔ Three-Reversals Algorithm
Rotate Array (LC 189)Shift 2D Grid (LC 1260)

k %= n; reverse the whole array, then the first k elements, then the rest.

Step 26.Must one arrangement become the one right after it in dictionary order (the next permutation, the next larger number with the same digits), in place?
➔ Next Permutation
Next Permutation (LC 31)Next Greater Element III (LC 556)

Start at i = n - 2 and move i left while nums[i] >= nums[i + 1]. If i >= 0, swap nums[i] with the rightmost value in nums[i + 1:] that is larger than nums[i]. Then reverse nums[i + 1:]; when there is no such i (i = -1), this reverses the whole array.

Step 27.Are the values whole numbers in a small range (or can every value above n be treated as n), and you need them in order faster than a sort?
➔ Counting Sort
H-Index (LC 274)Maximum Ice Cream Bars (LC 1833)

Make one slot for each value. Add each value with count[v] += 1. Then walk the slots in order of value.

Step 28.Do values or counts fall in a known, bounded range, and you need their order, the top k or the largest gap in linear time?
➔ Bucket Sort
Maximum Gap (LC 164)Sort Characters By Frequency (LC 451)

Put each value into a bucket indexed by its value or count, then read the buckets in order.

Step 29.Must you find a value that holds more than half (or more than n / k) of the input in one pass with O(1) extra space?
➔ Boyer-Moore Voting
Majority Element (LC 169)Majority Element II (LC 229)

Keep k - 1 candidates with counts: a match adds 1, a free slot takes the value, otherwise every count drops by 1; verify in a second pass.

Step 30.Must you find an exact pattern in a long text, or a prefix that comes back later in the string, in linear time?
➔ KMP
Find the Index of the First Occurrence in a String (LC 28)Longest Happy Prefix (LC 1392)

Build the failure table lps; on a mismatch fall back to lps[j - 1] instead of restarting.

Pattern Architecture & Invariant Design Masterclass

The brute-force way to examine every pair or every contiguous stretch of a list is two nested loops: O(n²). Two Pointers is the observation that in many problems you never need to move an index backwards. If you can prove that, then both indices together take at most 2n steps across the entire run, and the whole problem collapses to O(n).

There are two shapes, and confusing them is where most students lose time:

  • Converging pointers. One index starts at the far left, one at the far right, and they walk toward each other. Each comparison lets you eliminate one end permanently. Used when the data is sorted or symmetric.
  • Sliding window. Both indices start at the left. right always moves forward, adding elements into a running "window state" (a sum, a character count, a max). left moves forward only to fix a window that has become invalid or to try shrinking a valid one. The window is always a contiguous stretch.
1.3 — Invariant-First Pointer Boundaries: Deriving < vs <= from Candidate State

A frequent source of off-by-one errors in two-pointer algorithms is guessing whether to write while left < right or while left <= right. The choice is not a matter of memorization—it is dictated by the state semantics of left == right: does the singleton state represent a valid candidate state that must be evaluated, or an invalid/redundant state?

1. When < is Appropriate (Distinct Pair Heuristic)
  • Problem Archetypes: Two Sum II (LC 167), Valid Palindrome (LC 125), Container With Most Water (LC 11).
  • The Candidate Set: The problem specifically seeks an answer formed by a pair of distinct indices (i,j)(i, j)(i,j) with i<ji < ji<j.
  • Why < Holds: When left == right, the two pointers converge on the exact same element. A single element cannot form a two-sum pair with itself (the problem forbids reusing the same element), cannot hold water between two boundary walls, and a single character in a palindrome is symmetrically identical to itself. Because the candidate pair set is completely exhausted when left == right, terminating immediately with while left < right: is mathematically sound and avoids wasted computation.
2. When <= is Required (Critical Counterexamples Where < Fails)
  • Counterexample 1: Three-Way In-Place Partitioning (Dutch National Flag / LC 75 Sort Colors) In 3-way partitioning, we maintain pointers low, mid, and high. The subarray nums[mid..high] represents the unexamined candidate elements. The loop must run while mid <= high:. If you mistakenly used mid < high, the loop would stop one iteration too early, leaving the final element at mid == high completely uninspected and unpartitioned!
  • Counterexample 2: Odd-Length Palindromic Center Expansion (LC 5, LC 647) When expanding outward from prospective centers, odd-length palindromes initialize left = i, right = i (the singleton state left == right). The loop expands while left >= 0 and right < len(s) and s[left] == s[right]. Here, evaluating left == right is not a waste—it is the foundational base case.
  • Counterexample 3: Residual Singleton Evaluation & Hoare Partitioning In quickselect or Hoare-style array partitioning, pointers sweep inward until they cross. The singleton meeting state often holds a pivot element or residual value that must be categorized into one of the two partitions before halting.
3. Framing the Heuristic in Interviews
  • The "Pair vs. Target" Rule of Thumb: It is fine to use the "Pair vs. Target" trick as a quick mental intuition check (distinct pairs stop at <, closed-interval single targets stop at <=). However, it is a memorable heuristic, not a universal law for pointer or binary-search loops. Correct boundary conditions depend on the precise invariant, whether endpoints are inclusive or exclusive, whether a problem needs the first/last valid position, and the update rules for left, right, and mid.
  • The Senior Interview Standard: State the candidate state invariant out loud: "The unexamined candidates are in the range [mid..high]. Because the candidate at mid == high has not yet been inspected, my loop condition must be while mid <= high:."

3.1 — Understanding Invariants & The 5 Invariant Shapes

Before tackling specific problems, the single most critical idea to master is the loop invariant:

An invariant is a statement about your program's state that is True before the loop starts, is temporarily broken during internal state updates, and is strictly restored before the next iteration begins.

The Box Transfer Analogy

Imagine transferring items from Box A to Box B one by one:

  1. Held (Pre-condition): A+B=TotalA + B = \text{Total}A+B=Total.
  2. Broken (In-flight): You pick up an item in your hand. Visible A+B=Total−1A + B = \text{Total} - 1A+B=Total−1.
  3. Restored (Post-condition): You place the item into Box B. A+B=TotalA + B = \text{Total}A+B=Total is true again.
Code / Blueprint
total = len(A) + len(B)
while A:
item = A.pop() # Invariant temporarily broken (item in hand)
B.append(item) # Invariant restored
assert len(A) + len(B) == total
 
The 5 Canonical Invariant Shapes in Two Pointers
Interactive Architecture Diagram
Synthesizing vector architecture diagram...

The Kept-Prefix Boundary Partition (Reader-Writer Shape):

  • nums[0..writer-1]: The Kept Frontier ("Kept-Prefix Boundary") — Contains only verified, deduplicated elements in finalized order.
  • nums[writer..reader-1]: Discardable Debris — Elements already scanned that were duplicates or filtered out.
  • nums[reader..n-1]: Untrusted Raw Stream — Unprocessed elements waiting to be scanned.
  • Key Invariant Rule: Every candidate decision nums[reader] is evaluated against the trusted boundary of truth (nums[writer - 1] or nums[writer - k]), never against the raw stream.

The Prefix Sum Rescue (when values can be negative):

  • The Prefix Sum Algebraic Rescue: By shifting from contiguous physical pointers to prefix accumulators P[i]=∑j=0i−1nums[j]P[i] = \sum_{j=0}^{i-1} nums[j]P[i]=∑j=0i−1​nums[j], any contiguous subarray sum nums[L…R]nums[L \dots R]nums[L…R] equals P[R+1]−P[L]P[R+1] - P[L]P[R+1]−P[L]. We convert the window search into the algebraic identity P[L]=P[R+1]−targetP[L] = P[R+1] - \text{target}P[L]=P[R+1]−target. Storing prefix occurrences in a hash map solves the problem in single-pass O(N)O(N)O(N) time and O(N)O(N)O(N) space, completely immune to negative values.
The "Negative Numbers" Gotcha Matrix: When Two Pointers Works vs. When It Fails
Pointer ArchetypeAllows Negative Numbers?Why? Underlying Invariant MechanismCanonical Archetypes
Converging Squeeze (Opposite Direction)YESMonotonicity comes from sorted positional order (A[i] <= A[j]), not element signs. Advancing left increases the pair sum; advancing right decreases the pair sum.Two Sum II (LC 167), 3Sum (LC 15), 4Sum (LC 18), Container With Most Water (LC 11)
Symmetrical Extremes (Squares)YESLargest absolute values reside at outer boundaries. Squaring negative extremes yields the largest positive values at the far-left boundary.Squares of a Sorted Array (LC 977)
Partitioning & Compaction (Reader-Writer)YESInvariant evaluates an element-wise predicate (nums[i] < 0 or nums[i] == pivot), completely independent of cumulative arithmetic.Move Zeroes (LC 283), Sort Colors / Dutch Flag (LC 75), Rearrange Array Elements by Sign (LC 2149)
Sliding Window (Sum / Product)NO (Fatal Trap!)Adding negative numbers causes window sum to decrease; shrinking can cause sum to increase. Window monotonicity broken.Fails on LC 209 with negatives; MUST pivot to LC 560 (Prefix Sum) or LC 862 (Monotonic Deque)
Sliding Window (Frequency / Strings)YESWindow state tracks character frequencies/sets, which are purely combinatorial and unaffected by numerical arithmetic.Longest Substring (LC 3), Minimum Window Substring (LC 76), Character Replacement (LC 424)
Fast & Slow (Cycle / Midpoint)YESTraversal depends on node pointers / relative step speeds (1x vs 2x), completely independent of node payload values.Linked List Cycle (LC 141), Middle of Linked List (LC 876)

3.2 — The Two Pointers Decision Rule (Problem Classification Checklist)

When facing an unseen problem in an interview, use this systematic checklist to classify the exact sub-family before writing code:

Interactive Architecture Diagram
Synthesizing vector architecture diagram...

The diagram asks six questions in order, and the first "yes" names the family. Question 1 comes first because its problems are the easiest to spot: the array never changes, and each question names its range. Subarray Sum Equals K (LC 560) answers "no" there, because its ranges are not given. It answers "yes" at question 2. The later questions separate the pointer families by what they keep: a window, two ends of sorted data, a write position, or a fast and a slow pointer. When every answer is "no", the problem is not a two-pointers shape.

3.1 — Understanding Invariants: Monotonic Convergence & Window Validity

Why the invariant holds: pointers only ever move forward, so a position that has already been passed can never be revisited under a different pairing. Each pointer move is a proof step, not a guess — converging pointers eliminate a boundary because the discarded element is strictly dominated by the recorded answer, and a sliding window's left only advances when the window it would leave behind is provably no better than one still ahead.

Two Pointers collapses O(N²) pair comparisons into O(N) by exploiting monotonicity: converging pointers eliminate one boundary per step because outer pairs are strictly dominated, sliding windows advance right to accumulate state and contract left to restore constraints, and prefix sums transform non-monotonic windows into algebraic O(1) difference lookups.

1. Opposite-Direction Monotonic Squeeze (Converging Pointers)
Condition: While left < right, evaluate sum vs target (or wall height vs capacity)
Action: if sum < target: left += 1; elif sum > target: right -= 1

In a sorted array, if nums[left] + nums[right] < target, no element paired with nums[left] can reach target, eliminating left permanently.

2. Sliding Window Validity Restoration & Duplicate Skipping
Condition: While window violates constraint or nums[i] == nums[i-1] during multi-pointer scans
Action: evict left element state with left += 1 (or advance pointer past duplicate values)

Shrinking left restores the feasible window invariant in O(1) amortized steps; skipping duplicate values prevents generating identical combinations.

Common Questions
  • "Why does the sliding window pattern stop working the moment the array can contain negative numbers?"

    Because the whole technique is built on one assumption — that expanding the window can only make things "more," and shrinking it can only make things "less" — and negative numbers break that assumption in both directions at once.

    With all non-negative elements, adding nums[right] into the window sum can only increase it (or leave it unchanged at zero), and removing nums[left] can only decrease it. That monotonic relationship is exactly what lets you shrink left with confidence: if the window is currently valid, you know shrinking it further will only make it less valid, never suddenly valid again after briefly getting worse — so you never have to backtrack and re-check positions you've already moved past.

    Once negative numbers are allowed, expanding right can decrease the running sum (if the new element is negative), and shrinking left can increase it (if the element you're removing was negative). The sum stops moving in one predictable direction, so "the window is currently too big, shrink left" is no longer a sound conclusion — the answer you're looking for could be sitting on either side of your current window, in a way two pointers has no way to discover without checking every possibility again. That's why Subarray Sum Equals K (LC 560, which allows negatives) abandons the shrinking window entirely and switches to a prefix-sum hash map instead — trading the two-pointer's "never look back" guarantee for one that doesn't depend on monotonicity at all.

Complexity analysis

Time: O(n). The right index takes exactly n steps. The left index also takes at most n steps in total across the whole run — it only ever increases and can never exceed right. Total pointer movement is bounded by 2n, so the cost is linear even though the code contains a loop inside a loop. This is the amortised argument the interviewer wants to hear: "the inner while is not O(n) per outer step — it is O(n) for the entire algorithm."

Space: O(1) for scalar-state windows (4.1, 4.2, 4.4). O(k) for set/map-state windows (4.3), where k is the number of distinct values that can be in the window at once — O(min(n, alphabet)) for strings.

The alternative worth naming: with negative numbers allowed, sliding window is invalid, and the correct approach is prefix sums plus a monotonic deque or a hash map — O(n) time but O(n) space. Knowing when the pattern does not apply is a Staff-level signal.

Production Systems Deployment & Motivation

In streaming telemetry and rate limiters, monitoring rolling time windows — for example, finding the shortest consecutive burst of seconds that accounts for a traffic spike — uses two pointers moving in the same direction to maintain window state in one linear pass, instead of re-checking every candidate window from scratch.

Mastery Roadmap & Prerequisite Graph

💡 Don't worry if pointer movement rules feel subtle right now! Two Pointers is a universal state-elimination technique that turns O(N2)O(N^2)O(N2) pair searches into single-pass O(N)O(N)O(N) scans. As you progress through the Archetype Variants (from Container With Most Water and 3Sum to Dynamic Sliding Window and Trapping Rain Water), each hands-on challenge will cement this intuition until advancing left += 1 or shrinking right -= 1 based on monotonicity becomes second nature.

  • Applying two-pointer convergence to unsorted data: Two-pointer convergence strictly requires monotonicity (A[i]+A[j]A[i] + A[j]A[i]+A[j] increases when advancing iii, decreases when decrementing jjj). If the array is unsorted, pointer convergence skips potential pairs unpredictably. Always sort first or use a hash map.

  • Using < vs <= in loop condition: When finding pairs of distinct elements (index1 < index2), the loop condition must be while left < right:. Using left <= right allows an element to pair with itself when 2 * nums[left] == target.

  • 32-bit signed integer overflow on sum: In typed languages (Java, C++, C#), adding two large integers (nums[left] + nums[right]) can overflow 231−12^{31}-1231−1. Cast to 64-bit int (long long) before comparing.

Staff+ Engineering Perspective

Staff+ Systems Architecture & Hardware Considerations

ARCHITECTURAL SIGNALS & INTERVIEW TRIGGERS

Triggers: "sorted array pair search", "subarray / substring satisfying condition", "sliding window max/min", "in-place partitioned reordering". Transforms O(N2)O(N^2)O(N2) brute-force nested iterations into optimal O(N)O(N)O(N) linear scans by tracking coordinated index cursors across contiguous memory.

CONSTRAINTS & BOUNDS

Array lengths N≤107N \le 10^7N≤107. A quadratic O(N2)O(N^2)O(N2) double loop times out when N>104N > 10^4N>104. Two pointers ensure each element is visited at most a constant number of times (typically 2), guaranteeing strict O(N)O(N)O(N) runtime with O(1)O(1)O(1) auxiliary space.

FAANG PRODUCTION TRAPS & EDGE CASES

Index out-of-bounds on empty inputs; off-by-one window length calculations (right−left+1right - left + 1right−left+1 vs right−leftright - leftright−left); failing to update auxiliary window state (hash maps or frequency counts) symmetrically upon both expansion and contraction.

Theory Context•Two Pointers & Sliding Window

Conceptual Foundation & Intuitive Analogy

🧭 Conceptual Foundation & Pattern Intuition

When an array is sorted, elements possess a strict monotonic spatial order: moving to the right guarantees larger numbers, while moving to the left guarantees smaller numbers. In an unsorted array, locating a pair that sums to a target requires an O(N)O(N)O(N) hash set. But with sorted data, the space overhead can be eliminated entirely: by placing two pointers at opposite ends, the sum of the boundary elements acts as an infallible guide. If their sum is too small, the left boundary is permanently inadequate and must advance. If their sum is too large, the right boundary must retreat.

🏟️ The Analogy: The Balancing Seesaw

Imagine two people on a giant seesaw trying to balance a target weight. On the left side is a line of light weights arranged from lightest to heaviest; on the right side is a line of heavy weights arranged from heaviest to lightest. If the scale tips too low (sum is less than target), replacing the current light weight with an even lighter weight would make the deficit worse; you must step forward to a heavier light weight. If the scale tips too high, you must step back to a lighter heavy weight. You never need to restart or reconsider discarded weights.

🔑 Why It Works

The proof of correctness is absolute: at step cur_sum < target, numbers[left] + numbers[k] < target for all k≤rightk \le \text{right}k≤right because numbers[k] <= numbers[right]. Therefore, numbers[left] can never participate in any valid solution. Discarding it permanently via left += 1 is mathematically airtight.

Visual Blueprint & State Transition Model
Two Pointers & Sliding Window Mental Model Diagram
Figure 1 • Two Pointers & Sliding Window Core Invariant & State Lifecycle
Core Invariant Specification

The invariant to say out loud: "Everything I have moved past can never be part of a better answer than the one I already recorded — so I never have to look at it again." If you cannot say that sentence about your problem, two pointers is unsound and you need prefix sums, a hash map, or a heap instead.

Foundational Coordination & Boundary Paradigms: Monotonic Pointers vs. Prefix Sum Duality
1. Converging Squeezewhile left < right

Opposite boundaries step inward. Outer pairs are strictly dominated by sortedness or geometric width. Works with negative numbers (Two Sum II, 3Sum, Squares of Sorted Array) because monotonicity relies on sorted positional order (A[i] ≤ A[j]), not element signs.

Archetypes: Two Sum II (LC 167), 3Sum (LC 15), Squares of Sorted Array (LC 977), Container (LC 11).
2. Window & Compactionwhile right < n

Same-direction traversal maintaining contiguous state. FAILS on negative numbers for sum/product: evicting a negative increases sum and adding one decreases it, destroying window monotonicity!

Archetypes: Min Subarray Sum (LC 209, non-negatives only), Longest Substring (LC 3), Remove Duplicates (LC 26).
3. Prefix Sum DualityP[R+1] - P[L] = target

The negative numbers rescue. When negative numbers break sliding window monotonicity, prefix sums transform contiguous range searches into algebraic point lookups: P[L] = P[R+1] - target, recovering strict O(N) via hash map.

Archetypes: Subarray Sum Equals K (LC 560, negative numbers allowed), Shortest Subarray Sum (LC 862).
⚠️ Senior Interview Gotcha: Can Two Pointers Work on Negative Numbers?

YES for Converging / Squeeze: On sorted arrays (e.g. Two Sum II, 3Sum, Squares of Sorted Array), negative numbers work natively because monotonicity is governed by sorted index order (A[i] ≤ A[j]), not signs.

NO for Sliding Window Sums: If elements can be negative, expanding can decrease the sum and contracting can increase it. Monotonicity collapses! Pivot immediately to Prefix Sum + Hash Map (LC 560) or Monotonic Deque (LC 862).

The 15 Mental Models of Two Pointers & Sliding Window

Two Pointers problems divide by how the pointers move, what the window holds, and what a lookup remembers. Name the model before coding.

1Hash Map
items ➔ computed key ➔ dict lookup or count in O(1)

Hashing & Frequency Maps (Group Anagrams, Two Sum)

Spot it when: The question is "have I seen this before?", "how many of each?" or "which items belong together?": duplicates, counts, a partner that completes a target, or items grouped by a key you compute from them.
Mode 1 · Hash Map
Python
1from collections import defaultdict
2 
3 
4def group_anagrams(strs: list[str]) -> list[list[str]]:
5 """Group Anagrams: words with the same sorted letters share one key in a hash map.
6 Time: O(N * K log K) | Space: O(N * K)
7 """
8 groups: dict[str, list[str]] = defaultdict(list) # key -> every word read so far with that key
9 # 1. File Each Word Under Its Key:
10 # Words with the same letters and the same counts produce the same sorted-letters key ("eat", "tea" -> "aet"), so every anagram lands in the same group without comparing any two words.
11 for s in strs:
12 key = "".join(sorted(s))
13 groups[key].append(s)
14 return list(groups.values())
15 
16 
17def two_sum(nums: list[int], target: int) -> list[int]:
18 """Two Sum: remember each value's index, and look up the partner each new value needs.
19 Time: O(N) | Space: O(N)
20 """
21 index_of: dict[int, int] = {} # value -> an index where it was seen
22 # 1. Look Up The Partner Before Storing:
23 # If target - num was already seen, its stored index completes the pair; storing num only after the lookup keeps num from ever pairing with itself.
24 for i, num in enumerate(nums):
25 partner = target - num
26 if partner in index_of:
27 return [index_of[partner], i]
28 index_of[num] = i
29 return [] # LeetCode guarantees exactly one answer, so this line is never reached

A hash map finds a key's entry in one step, so pick the key the question is about and store under it what the answer needs: in group_anagrams the key is a word's sorted letters and the value is every word read so far with that key; in two_sum the key is a number and the value is its index. Items that belong together produce equal keys, so groups[key].append(s) files every anagram in one pass without comparing any two words, and partner in index_of finds an earlier number that completes the pair without a second loop. The trap is a lossy key: it must be equal only for items that truly belong together, so key on "".join(sorted(s)), which keeps repeated letters, never on frozenset(s) or set(s), which drop them and put "aab" and "abb" in one group.

Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Keying on frozenset(s) instead of the sorted letters

    Mistake
    key = frozenset(s)
    Fix
    key = "".join(sorted(s))

    What happens: A set keeps each letter once and drops the repeats. "aab" and "abb" both give the set of a and b. They land in one group, but they are not anagrams. "".join(sorted(s)) keeps every letter, repeats included.

    Example: strs = ["aab", "abb"]
    Mistake: returns [['aab', 'abb']]Fix: returns [['aab'], ['abb']]
  2. 2

    Gotcha 2: Using the list from sorted(s) as the key

    Mistake
    key = sorted(s)
    Fix
    key = "".join(sorted(s))

    What happens: sorted(s) returns a list. A list can change, so Python does not allow it as a dict key. groups[key] raises a TypeError. Join the letters into a string, or wrap the list in tuple(...).

    Example: strs = ["eat", "tea"]
    Mistake: raises TypeErrorFix: returns [['eat', 'tea']]
  3. 3

    Gotcha 3: Storing num before looking up its partner

    Mistake
    partner = target - num
    index_of[num] = i
    if partner in index_of:
        return [index_of[partner], i]
    Fix
    partner = target - num
    if partner in index_of:
        return [index_of[partner], i]
    index_of[num] = i

    What happens: index_of[num] = i makes num visible to the lookup. If it runs first, partner in index_of can find num itself when partner == num. Then two_sum pairs one element with itself.

    Example: nums = [3, 2, 4], target = 6
    Mistake: returns [0, 0]Fix: returns [1, 2]
  4. 4

    Gotcha 4: Joining letter counts into a key without a separator

    Mistake
    key = "".join(map(str, count))
    Fix
    key = tuple(count)

    What happens: A key built from letter counts lists 26 numbers. Joined without a separator, different counts can give the same string. One a with eleven bs gives "111" followed by zeros. Eleven as with one b gives the same string.

    Example: Do "a" + "b" * 11 and "a" * 11 + "b" get the same key?
    Mistake: returns TrueFix: returns False
Canonical LeetCode:Group Anagrams (LC 49)Two Sum (LC 1)Valid Anagram (LC 242)Contains Duplicate (LC 217)Valid Sudoku (LC 36)Ransom Note (LC 383)Isomorphic Strings (LC 205)Detect Squares (LC 2013)Binary Tree Vertical Order Traversal (LC 314)Line Reflection (LC 356)Perfect Rectangle (LC 391)Check If N and Its Double Exist (LC 1346)Random Pick Index (LC 398)Minimum Index of a Valid Split (LC 2780)Copy List with Random Pointer (LC 138)Contains Duplicate II (LC 219)
Open the Hash Map card
2Bucket Sort

Bucket Sort (Top K Frequent, Maximum Gap)

Spot it when: Values or counts fall in a known, bounded range (every count is between 1 and n; n values spread over min to max), and you need their order, the top k, or the largest gap in linear time, without an O(n log n) sort.
Mode 2 · Bucket Sort
Python
1def maximum_gap(nums: list[int]) -> int:
2 """Maximum Gap: pigeonhole buckets, compare neighbours across buckets only.
3 Time: O(N) | Space: O(N)
4 """
5 # 1. Handle Too Few Values:
6 # With fewer than two values there is no neighbour at all, so no gap can be measured.
7 n = len(nums)
8 if n < 2:
9 return 0
10 
11 # 2. Size The Buckets From The Pigeonhole Bound:
12 # The largest gap is at least the average gap (hi - lo) / (n - 1); max(1, ...) keeps every
13 # bucket at least one value wide so the later division never hits zero.
14 lo, hi = min(nums), max(nums)
15 width = max(1, (hi - lo) // (n - 1))
16 count = (hi - lo) // width + 1 # Enough buckets to hold hi as well
17 bucket_min = [float("inf")] * count # Smallest value that fell in each bucket
18 bucket_max = [float("-inf")] * count # Largest value that fell in each bucket
19 
20 # 3. Drop Each Value Into Its Bucket, Keeping Only The Extremes:
21 # No two values are compared here; because width is at most the smallest possible gap, the
22 # answer can never sit inside one bucket, so only its smallest and largest value matter.
23 for x in nums:
24 b = (x - lo) // width
25 bucket_min[b] = min(bucket_min[b], x)
26 bucket_max[b] = max(bucket_max[b], x)
27 
28 # 4. Walk The Buckets In Order And Measure Cross-Bucket Gaps:
29 # Skipping empty buckets and carrying prev_max forward means each gap is measured from the
30 # last non-empty bucket's maximum to the next non-empty bucket's minimum.
31 best = 0
32 prev_max = lo
33 for b in range(count):
34 if bucket_min[b] == float("inf"):
35 continue
36 best = max(best, bucket_min[b] - prev_max)
37 prev_max = bucket_max[b]
38 
39 return best

Bucket Sort drops each value straight into the bucket that covers its range, b = (x - lo) // width, so placing a value needs no comparison with other values and the buckets come out in order. The key can be any bounded number: bucket by value range to find gaps, or by count to order items by frequency. For the largest gap between neighbours, keep only bucket_min[b] and bucket_max[b], walk the buckets once, and measure bucket_min[b] - prev_max, where prev_max is the largest value of the last non-empty bucket. That is enough because of the pigeonhole principle: n values from lo to hi leave n - 1 gaps that add up to hi - lo, so the largest gap is at least their average, and being a whole number it is at least width whenever hi > lo. Two values in the same bucket differ by at most width - 1, so the maximum gap is never inside one bucket: it always runs from one bucket's maximum to a later bucket's minimum, and the values in between never matter. The trap is the floor of 1: when hi - lo < n - 1, for example with repeated values, (hi - lo) // (n - 1) is 0 and (x - lo) // width divides by zero, so write width = max(1, (hi - lo) // (n - 1)).

Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Computing width without the floor of 1

    Mistake
    width = (hi - lo) // (n - 1)
    Fix
    width = max(1, (hi - lo) // (n - 1))

    What happens: With repeated values, hi - lo can be smaller than n - 1. Then (hi - lo) // (n - 1) is 0. The next division, (hi - lo) // width, divides by zero. max(1, ...) keeps every bucket at least one value wide.

    Example: nums = [1, 1, 1, 4, 2]
    Mistake: raises ZeroDivisionErrorFix: returns 2
  2. 2

    Gotcha 2: Leaving out the check if n < 2

    Mistake
    n = len(nums)
    Fix
    n = len(nums)
    if n < 2:
        return 0

    What happens: With one value, there is no neighbour and no gap, so the answer is 0. Without the check, the code goes on to compute the width. n - 1 is 0 there, so (hi - lo) // (n - 1) divides by zero.

    Example: nums = [10]
    Mistake: raises ZeroDivisionErrorFix: returns 0
  3. 3

    Gotcha 3: Rounding the width up with + 1

    Mistake
    width = (hi - lo) // (n - 1) + 1
    Fix
    width = max(1, (hi - lo) // (n - 1))

    What happens: The largest gap is at least (hi - lo) / (n - 1), the average gap. A bucket must be no wider than that, so the largest gap can never be inside one bucket. With + 1, a bucket can be wider. Two values can then share a bucket, and the gap between them is never measured.

    Example: nums = [6, 18]
    Mistake: returns 0Fix: returns 12
  4. 4

    Gotcha 4: Comparing with bucket_max[b - 1] instead of prev_max

    Mistake
    best = max(best, bucket_min[b] - bucket_max[b - 1])
    Fix
    best = max(best, bucket_min[b] - prev_max)
    prev_max = bucket_max[b]

    What happens: The largest gap often runs across empty buckets. The value just below bucket_min[b] is then in an earlier bucket, not in bucket b - 1. An empty bucket_max[b - 1] is -inf, and the difference becomes inf. prev_max keeps the maximum of the last bucket that was not empty.

    Example: nums = [1, 2, 10]
    Mistake: returns infFix: returns 8
Canonical LeetCode:Maximum Gap (LC 164)Sort Characters By Frequency (LC 451)Top K Frequent Elements (LC 347)
Open the Bucket Sort card
3Converging Pointers
left -> <- right (Opposite Boundary Contraction)

Two Pointers (Opposite Ends)

Spot it when: The array is sorted (or can be) and you look for pairs or triples that meet a target, or you compare the two ends (palindromes, container walls).
Mode 3 · Converging Pointers
Python
1from typing import Callable, List, Optional, Tuple
2 
3 
4def converge(n: int, compare: Callable[[int, int], int]) -> Optional[Tuple[int, int]]:
5 """Converging Pointers: squeeze a pair of indices (left, right) inward from both ends of a sorted sequence of length n.
6 compare(left, right) returns 0 to stop at this pair, below 0 to move left, and above 0 to move right.
7 Precondition: each step can rule out the element at left or the element at right for good, and compare says which one.
8 For a pair sum on a sorted sequence, move left when the pair is too small and move right when it is too big.
9 compare may also record an answer and return a nonzero value to keep going, as in Container With Most Water and 3Sum.
10 In Valid Palindrome, a match moves both pointers, so that loop moves left and right together.
11 T = the cost of one compare call.
12 Time: O(N * T) | Space: O(1)
13 """
14 left, right = 0, n - 1 # left and right only ever move toward each other
15 
16 # 1. Squeeze Inward While Not Met:
17 # compare rules out the element at left or at right for good on every step, so only the ruled-out end moves and the pair found, if any, is always still between left and right.
18 while left < right:
19 order = compare(left, right)
20 if order == 0:
21 return (left, right)
22 elif order < 0:
23 left += 1 # The pair is too small: the element at left cannot pair with any remaining element
24 else:
25 right -= 1 # The pair is too big: the element at right cannot pair with any remaining element
26 
27 return None

Start one pointer at each end of a sorted array. If the pair's sum is too small, only moving left right can raise it; if it is too big, only moving right left can lower it.

Each step rules out one element for good: when curr_sum < target, numbers[left] is already paired with the largest value left, so no remaining partner can reach target (and the mirror holds for right). This works with negative numbers too, because it relies on sorted order, not on signs.

The trap is unsorted input: there, moving a pointer no longer raises or lowers the sum, so the squeeze can walk past the answer; sort first, and keep the original indices if the problem asks for them.

Gotchas1 of 5

Gotcha 1 of 5

  1. 1
    The trap

    Gotcha 1: Running the squeeze on an unsorted array

    Mistake
    twoSum(nums, target)
    Fix
    twoSum(sorted(nums), target)

    What happens: When curr_sum < target, the loop moves left one step right to raise the sum. A larger value at left is certain only when nums is sorted. On unsorted input, a move can lower the sum, and the loop walks past the pair. Sorting first finds the pair, but it returns positions in the sorted list. Two Sum (LC 1) wants the original indices, so sort (value, index) pairs instead.

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

    Gotcha 2: Moving both pointers after one comparison

    Mistake
    left += 1
    right -= 1
    Fix
    left += 1

    What happens: When curr_sum < target, only numbers[left] is ruled out. numbers[right] may still pair with a later numbers[left]. Moving right as well drops that value without a check.

    Example: numbers = [1, 2, 4, 6], target = 8
    Mistake: returns []Fix: returns [2, 4]
  3. 3

    Gotcha 3: Skipping 3Sum (LC 15) duplicates before the match check

    What happens: 3Sum (LC 15) picks a first number nums[i] and squeezes the rest of the sorted array for the other two with left and right. The moment total == nums[i] + nums[left] + nums[right] equals zero, record the triplet right away with res.append([nums[i], nums[left], nums[right]]), before skipping anything. Once that triplet is saved, later copies of nums[left] or nums[right] can only rebuild the same triplet of values, never a new one, so moving past them afterward loses nothing. The trap is running the duplicate-skip loops first: while left < right and nums[left] == nums[left + 1]: left += 1 and while left < right and nums[right] == nums[right - 1]: right -= 1, run before the total == 0 check saves the match, throw away the exact left/right pair that produced the zero sum, so that triplet never reaches res at all.

  4. 4

    Gotcha 4: Moving the pointer at the taller wall

    What happens: In Container With Most Water (LC 11), the shorter wall sets the water level. Moving either pointer makes the width smaller. After moving the taller wall, the level is still at most the shorter wall. So the area can never grow. Only moving the shorter wall can find a larger area.

  5. 5

    Gotcha 5: Assuming every two-pointer squeeze can break early

    What happens: Two Sum II - Input Array Is Sorted (LC 167) moves left and right toward each other on every step, and it never breaks out of the loop early: the array is sorted, so raising left can only raise numbers[left] + numbers[right], and lowering right can only lower it, and that holds for negative numbers too. 3Sum (LC 15) adds a different move on top of that squeeze: if nums[i] > 0: break, which stops the whole outer loop over the anchor nums[i]. That break is only correct because the array is sorted ascending and nums[i] is the smallest of the three remaining numbers: once nums[i] is positive, nums[left] and nums[right] (both at or after i) are positive too, so nums[i] + nums[left] + nums[right] can never reach zero, and every later anchor is checked the same way and found to fail. Reading the two moves as the same kind of step is the trap: the LC 167 squeeze needs no sign check and works correctly with negative numbers already, so carrying if nums[i] > 0: break into a squeeze that has no sorted, all-positive-from-here anchor breaks on inputs the squeeze alone would have solved correctly.

Canonical LeetCode:Two Sum II - Input Array Is Sorted (LC 167)3Sum (LC 15)Container With Most Water (LC 11)Trapping Rain Water (LC 42)3Sum Smaller (LC 259)Two Sum Less Than K (LC 1099)Valid Palindrome (LC 125)4Sum (LC 18)Count the Number of Fair Pairs (LC 2563)Strobogrammatic Number (LC 246)
Open the Converging Pointers card
4Dummy Node
list whose head may change ➔ dummy.next = head ➔ rewire through prev, return dummy.next

Sentinel / Dummy Node

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.
Mode 4 · 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

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.

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]]
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)
Open the Dummy Node card
5Boyer-Moore Voting

Boyer-Moore Voting (Majority Element)

Spot it when: A value holds more than a fixed share of the input (more than half, or more than n / k for a small k), and you must find it in one pass with O(1) extra space, so no count per value.
Mode 5 · Boyer-Moore Voting
Python
1def majority_element(nums: list[int]) -> int:
2 """Majority Element: pair off different values; the majority is the value left standing.
3 Time: O(N) | Space: O(1)
4 """
5 # candidate, count: the value currently "standing" and how many of its copies are not yet
6 # paired off against a different value.
7 candidate, count = None, 0
8 
9 # 1. Decide This Value's Role, Then Update The Tally:
10 # Everything read before a count of 0 is already paired off, so x starts a new run;
11 # counting after that check means x always counts as its own first copy, and count never
12 # goes below 0.
13 for x in nums:
14 if count == 0:
15 candidate = x
16 count += 1 if x == candidate else -1
17 
18 # 2. Report The Value Left Standing:
19 # A value that fills more than half of nums has more copies than everything else combined,
20 # so pairing off different values can never cancel it; it is the one still standing at the end.
21 return candidate

Let every value cancel one copy of a different value: a value that fills more than half of nums has more copies than all the others together, so it can't be cancelled out, and the code only has to remember what is still standing, candidate and count. When count == 0 everything read so far is paired off, so x becomes the new candidate; then count += 1 if x == candidate else -1 either adds a copy or removes a pair of two different values, at most one of them the majority, which is why the majority is the candidate left at the end. The trap is trusting that survivor: it is the answer only when a majority is promised (in the middle of [2,2,1,1,1,2,2] the candidate is 1, and on [1, 2, 3] the survivor occurs once), so when the promise is missing, count the survivor again in a second pass.

Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Returning candidate when no majority is promised

    Mistake
    return candidate
    Fix
    if nums.count(candidate) > len(nums) // 2:
        return candidate
    return None

    What happens: The last candidate is the majority only when a majority exists. Majority Element (LC 169) promises one, so the blueprint can return it. Without that promise, the survivor can be any value. Count it in a second pass before returning it.

    Example: nums = [1, 2, 3], with no majority promised
    Mistake: returns 3Fix: returns None
  2. 2

    Gotcha 2: Looping over nums[:-1] and skipping the last value

    Mistake
    for x in nums[:-1]:
    Fix
    for x in nums:

    What happens: Every value must cancel or add one copy. nums[:-1] leaves out the last value. When that value decides the result, the wrong candidate is left standing.

    Example: nums = [1, 2, 2]
    Mistake: returns 1Fix: returns 2
  3. 3

    Gotcha 3: Counting x before testing count == 0

    Mistake
    count += 1 if x == candidate else -1
    if count == 0:
        candidate = x
    Fix
    if count == 0:
        candidate = x
    count += 1 if x == candidate else -1

    What happens: When count is 0, every value read so far is paired off. The new value x must start the new run and count as its first copy. Counting first compares x with the old candidate. At the start, candidate is None, so the first value subtracts 1 and count becomes -1. count then never returns to 0, and candidate stays None.

    Example: nums = [3, 1, 3]
    Mistake: returns NoneFix: returns 3
  4. 4

    Gotcha 4: Returning the value that led with the largest count

    Mistake
    if count > best_count:
        best, best_count = candidate, count
    Fix
    return candidate

    What happens: The lead changes as the pairs cancel. A value can hold the largest count early and still be paired off later. Only the candidate standing after the last value is the majority.

    Example: nums = [1, 1, 2, 2, 2]
    Mistake: returns 1Fix: returns 2
Canonical LeetCode:Majority Element (LC 169)Majority Element II (LC 229)Minimum Index of a Valid Split (LC 2780)
Open the Boyer-Moore Voting card
6KMP

Knuth-Morris-Pratt (KMP) String Matching

Spot it when: You must find an exact pattern in a long text, or a prefix of a string that comes back later in it (a border, a period, a repeated start), in linear time.
Mode 6 · KMP
Python
1def str_str(haystack: str, needle: str) -> int:
2 """KMP: find needle in haystack without ever moving back in haystack.
3 Time: O(N + M) | Space: O(M)
4 """
5 m = len(needle)
6 # lps[i]: the longest proper prefix of needle[:i + 1] that is also its suffix (its longest border).
7 lps = [0] * m
8 k = 0 # Length of the prefix of needle matched so far
9 
10 # 1. Build The Border Table From needle Against Itself:
11 # Building lps runs the same search on needle against itself, so a mismatch falls back to a
12 # shorter border the same way the main search below does.
13 for i in range(1, m):
14 while k > 0 and needle[i] != needle[k]:
15 k = lps[k - 1]
16 if needle[i] == needle[k]:
17 k += 1
18 lps[i] = k
19 
20 # 2. Scan haystack Once, Falling Back Instead Of Restarting:
21 # On a mismatch, the k letters just matched are needle[:k], already known, so k falls back
22 # to the longest border that still fits; the match ends at i, so it starts i - m + 1 letters
23 # earlier.
24 k = 0
25 for i, ch in enumerate(haystack):
26 while k > 0 and ch != needle[k]:
27 k = lps[k - 1]
28 if ch == needle[k]:
29 k += 1
30 if k == m:
31 return i - m + 1
32 return -1

KMP never reads a character of the text twice: when a mismatch comes after k matched letters, those letters are needle[:k], which you already know, so the search keeps their longest border, k = lps[k - 1], and tries the same letter again instead of moving the start forward by one and re-reading. It is correct because a match that is still possible must begin at a suffix of what was read that is also a prefix of needle, and lps[k - 1] is the longest such part; falling back again while k > 0 and ch != needle[k] visits every shorter one in order, and lps itself is built by running the same loop on needle against itself. The trap is resetting k = 0 on a mismatch: on haystack = "abababc", needle = "ababc" it throws away the border "ab" at i = 4, where the only match begins, and returns -1 instead of 2.

Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Resetting k = 0 on a mismatch

    Mistake
    while k > 0 and ch != needle[k]:
        k = 0
    Fix
    while k > 0 and ch != needle[k]:
        k = lps[k - 1]

    What happens: After k matched letters, the text just read is needle[:k]. Its end can also be the start of needle. A match can then begin inside the failed attempt. k = 0 throws that start away, and the search can miss the match. k = lps[k - 1] keeps it.

    Example: haystack = "abababc", needle = "ababc"
    Mistake: returns -1Fix: returns 2
  2. 2

    Gotcha 2: Building lps with range(m) instead of range(1, m)

    Mistake
    for i in range(m):
    Fix
    for i in range(1, m):

    What happens: lps[0] must stay 0, because a single letter has no shorter border. With range(m), the loop compares needle[0] with itself and sets lps[0] = 1. A fall back from k = 1 then sets k = lps[0], which is 1 again. The while loop never ends.

    Example: haystack = "ab", needle = "ac"
    Mistake: the loop never endsFix: returns -1
  3. 3

    Gotcha 3: Falling back once with if instead of while

    Mistake
    if k > 0 and ch != needle[k]:
    Fix
    while k > 0 and ch != needle[k]:

    What happens: After one fall back, the letter may still not match needle[k]. The search must then fall back again, to a shorter border. With if, it falls back once and moves on. k then counts letters that were never matched.

    Example: haystack = "aabaa", needle = "aaa"
    Mistake: returns 2Fix: returns -1
  4. 4

    Gotcha 4: Falling back to lps[k] instead of lps[k - 1]

    Mistake
    while k > 0 and ch != needle[k]:
        k = lps[k]
    Fix
    while k > 0 and ch != needle[k]:
        k = lps[k - 1]

    What happens: After k matched letters, the text read is needle[:k]. Its border is stored at lps[k - 1], because lps[i] describes needle[:i + 1]. lps[k] describes a prefix one letter longer than what was read. In this example, k is 2 at the b, and lps[2] is also 2. k never changes, so the while loop never ends.

    Example: haystack = "aabaaab", needle = "aaab"
    Mistake: the loop never endsFix: returns 3
Canonical LeetCode:Find the Index of the First Occurrence in a String (LC 28)Longest Happy Prefix (LC 1392)Repeated Substring Pattern (LC 459)Shortest Palindrome (LC 214)
Open the KMP card
7Fixed Window
i >= k -> (Absorb Incoming, Evict Outgoing)

Fixed-Size Sliding Window

Spot it when: The window length k is given and you measure every window: averages, counts, anagram matches.
Mode 7 · Fixed Window
Python
1from typing import Callable, List
2 
3 
4def slide_fixed(n: int, k: int, add: Callable[[int], None], remove: Callable[[int], None], record: Callable[[int], None]) -> None:
5 """Fixed Window: visit every window of exactly k indices in 0..n-1, one index in and one out per step.
6 Precondition: n, the number of indices, is known before the loop starts, and k >= 1.
7 record(start) is called once per window, after the window holds indices start..start + k - 1.
8 T = the cost of one add, remove or record call.
9 Time: O(N * T) | Space: O(1) besides the window's state
10 """
11 # 1. Handle Impossible Case:
12 # Fewer than k indices exist, so no window of length k can ever form.
13 if n < k:
14 return
15 
16 # 2. Build First Window:
17 # Add every index from 0 to k - 1 so the window holds its first k indices before any sliding starts.
18 for i in range(k):
19 add(i)
20 record(0)
21 
22 # 3. Slide One Step At A Time:
23 # Add the incoming index i, remove the outgoing index i - k, then record only after both updates land so the window always holds exactly k indices when it is read.
24 for i in range(k, n):
25 add(i)
26 remove(i - k)
27 record(i - k + 1)

Every window has the length k = len(p), so build the first window's counts once, then slide one step at a time: add the letter entering on the right and remove the one leaving on the left. Each slide changes exactly two letters of the window, so updating window_count for s[i] and s[i - k] keeps it equal to the counts of the k characters ending at i without recounting the whole window, and window_count == p_count then says whether that window is an anagram of p. The trap is the index of the leaving letter: when s[i] enters, s[i - k] leaves; an off-by-one there shifts every window, so the windows compared are the wrong ones.

Gotchas1 of 5

Gotcha 1 of 5

  1. 1
    The trap

    Gotcha 1: Removing s[i - k + 1] instead of s[i - k]

    Mistake
    window_count[ord(s[i - k + 1]) - ord('a')] -= 1
    Fix
    window_count[ord(s[i - k]) - ord('a')] -= 1

    What happens: Before the step, the window is s[i - k..i - 1]. When s[i] enters, the letter that leaves is s[i - k]. Removing s[i - k + 1] takes out a letter that is still in the window. The counts then match no real window. The result lists wrong start indices.

    Example: s = "cbaebabacd", p = "abc"
    Mistake: returns [0, 3, 4, 5]Fix: returns [0, 6]
  2. 2

    Gotcha 2: Writing the outgoing index as s[k - i] instead of s[i - k]

    Mistake
    window_count[ord(s[k - i]) - ord('a')] -= 1
    Fix
    window_count[ord(s[i - k]) - ord('a')] -= 1

    What happens: The loop runs i from k upward, and the letter that leaves at step i is s[i - k]. The index i - k grows by 1 at each step, because the window moves right. The index k - i does the opposite: it is 0 at i = k, then -1, -2 and so on. Python reads a negative index from the end of the string and raises no error. With s = "cbaebabacd" and k = 3, the first slide at i = 3 removes s[0], which is correct. At i = 4 the code removes s[-1], the last letter 'd', instead of s[1], the letter 'b'. The count of 'd' becomes -1 and the count of 'b' stays too high. From then on, window_count matches no real window, so no later window can equal p_count.

    Example: s = "cbaebabacd", p = "abc"
    Mistake: returns [0]Fix: returns [0, 6]
  3. 3

    Gotcha 3: Comparing between the add and the remove

    Mistake
    window_count[ord(s[i]) - ord('a')] += 1
    if window_count == p_count:
        result.append(i - k + 1)
    window_count[ord(s[i - k]) - ord('a')] -= 1
    Fix
    window_count[ord(s[i]) - ord('a')] += 1
    window_count[ord(s[i - k]) - ord('a')] -= 1
    if window_count == p_count:
        result.append(i - k + 1)

    What happens: After the add, the window holds k + 1 letters. p_count holds exactly k letters. So the comparison is always False at that point. Only the first window, checked before the loop, can still match.

    Example: s = "cbaebabacd", p = "abc"
    Mistake: returns [0]Fix: returns [0, 6]
  4. 4

    Gotcha 4: Dropping the len(s) < k check

    What happens: When s is shorter than p, no window of length k fits. In Python, s[:k] then returns the whole shorter string without an error. Its counts never equal p_count, so the result is still []. In Java or C++, reading k characters from a shorter string goes past its end. So return [] before the first window is built.

  5. 5

    Gotcha 5: Re-summing every window in Maximum Average Subarray I (LC 643)

    Mistake
    window_sum = sum(nums[i - k + 1:i + 1])
    Fix
    window_sum += nums[i] - nums[i - k]

    What happens: Maximum Average Subarray I (LC 643) slides a sum instead of counts. sum(nums[i - k + 1:i + 1]) reads k values at every step. The whole scan then costs O(N · k). Adding the entering value and subtracting the leaving one costs O(1) per step. Both give the same answer.

Canonical LeetCode:Maximum Average Subarray I (LC 643)Find All Anagrams in a String (LC 438)Maximum Number of Vowels in a Substring of Given Length (LC 1456)Number of Sub-arrays of Size K and Average Greater than or Equal to Threshold (LC 1343)Moving Average from Data Stream (LC 346)Minimum Swaps to Group All 1's Together (LC 1151)Permutation in String (LC 567)Contains Duplicate II (LC 219)
Open the Fixed Window card
8Longest Window
left -> right -> (Check Before Growing, Contract To Repair)

Variable-Size Sliding Window (Longest Window)

Spot it when: You want the longest contiguous subarray or substring that stays within a limit: no repeats, at most k distinct values, at most k zeros flipped.
Mode 8 · Longest Window
Python
1from typing import Callable
2 
3 
4def longest_window(n: int, add: Callable[[int], None], remove: Callable[[int], None], would_be_valid: Callable[[int], bool]) -> int:
5 """Longest Window: the length of the longest valid window [left, right] of indices 0..n-1.
6 Precondition: every shorter window inside a valid window is valid too, and the empty window is valid.
7 add(i) and remove(i) update the window's state for index i.
8 would_be_valid(i) reads whether adding index i to the current window would keep it valid, without changing the window's state.
9 T = the cost of one add, remove or would_be_valid call.
10 Time: O(N * T) | Space: O(1) besides the window's state
11 """
12 left = 0
13 max_len = 0
14 
15 for right in range(n):
16 # 1. Repair By Evicting From left:
17 # would_be_valid(right) asks whether adding index right would still be a valid window, without changing the window's state. Every window [j, right] with j < left contains everything [left, right] contains, so if adding right to [left, right] would still be invalid, no later left can keep right out of it; evicting left is the only way back to a window that right can be added to.
18 while not would_be_valid(right):
19 remove(left)
20 left += 1
21 # 2. Absorb Index right:
22 # Once would_be_valid(right) confirms the window stays valid, right is actually added.
23 add(right)
24 # 3. Record Only After The Add:
25 # The window [left, right] is now valid and fully updated, so its length can be measured.
26 max_len = max(max_len, right - left + 1)
27 
28 return max_len

Before you add right to the window, check whether adding it would break the rule. If it would, move left forward until adding right is safe, then add right and record the window's length.

The window is always valid at the moment you add right, because right is only added once the check says adding it keeps the window valid, so the largest measurement is the answer. Moving left never loses a longer answer: any window that starts before left already contains everything [left, right] contains, so adding right to it would break the rule too.

The trap is answering the check by mutating the window's state to test it: the check must read what adding right would do without changing the window, or the add call right after it double-counts right.

Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Measuring the window before the while loop repairs it

    Mistake
    for right in range(len(s)):
        max_len = max(max_len, right - left + 1)
        while s[right] in seen_chars:
    Fix
    for right in range(len(s)):
        while s[right] in seen_chars:

    What happens: Before the while loop, s[right] may already be in the window. Then the window s[left..right] holds a letter twice. A length measured there counts an invalid window, so max_len can become too large. The while loop moves left until the repeat is gone. After that loop, right - left + 1 is the length of a valid window.

    Example: s = "abcabcbb"
    Mistake: returns 4Fix: returns 3
  2. 2

    Gotcha 2: Repairing the window with if instead of while

    Mistake
    if s[right] in seen_chars:
    Fix
    while s[right] in seen_chars:

    What happens: The earlier copy of s[right] can be several places after left. One step removes only s[left], which may be a different letter. Then the window still holds the repeat when it is measured.

    Example: s = "pwwkew"
    Mistake: returns 4Fix: returns 3
  3. 3

    Gotcha 3: Keeping map keys whose count dropped to 0

    Mistake
    count[fruits[left]] -= 1
    Fix
    count[fruits[left]] -= 1
    if count[fruits[left]] == 0:
        del count[fruits[left]]

    What happens: Fruit Into Baskets (LC 904) allows at most 2 fruit types in the window. It keeps a count for each type and checks len(count). When left moves, the count of fruits[left] goes down by 1. A type whose count reaches 0 is no longer in the window. If its key stays, len(count) still counts it, and the window shrinks more than needed.

  4. 4

    Gotcha 4: Using one window to count exactly k distinct values

    What happens: Subarrays with K Different Integers (LC 992) counts the subarrays with exactly k distinct values. For one right, a whole range of starts gives exactly k values. One window tracks only one start, so it misses the others. Count the subarrays with at most k distinct values, and subtract those with at most k - 1. That is the At-Most Trick.

Canonical LeetCode:Longest Substring Without Repeating Characters (LC 3)Fruit Into Baskets (LC 904)Longest Subarray of 1's After Deleting One Element (LC 1493)Longest Substring with At Most K Distinct Characters (LC 340)Max Consecutive Ones II (LC 487)Minimum Operations to Reduce X to Zero (LC 1658)Longest Substring with At Most Two Distinct Characters (LC 159)Longest Repeating Character Replacement (LC 424)Max Consecutive Ones III (LC 1004)
Open the Longest Window card
9Fast & Slow Pointers
slow (1x) vs fast (2x) (Velocity Differential)

Fast & Slow Pointers (Floyd's Tortoise and Hare)

Spot it when: A linked list (or an array used as next = nums[i]) may contain a loop, or you need its middle in one pass with O(1) extra space.
Mode 9 · Fast & Slow Pointers
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 find_cycle_entrance(head: Optional[ListNode]) -> Optional[ListNode]:
9 """Linked List Cycle II: Floyd's tortoise and hare, then find the cycle's first node.
10 Time: O(N) | Space: O(1)
11 """
12 if not head or not head.next:
13 return None
14 
15 slow = fast = head # both pointers start together; fast will take exactly twice slow's steps
16 has_cycle = False
17 
18 # 1. Phase 1 - Detect Collision:
19 # fast gains exactly one step on slow per move, so inside a cycle it cannot jump over slow: they must meet. Testing fast and fast.next (not just fast) keeps fast.next.next from reading an attribute of None.
20 while fast and fast.next:
21 slow = slow.next # 1 step
22 fast = fast.next.next # 2 steps
23 if slow == fast:
24 has_cycle = True
25 break
26 
27 if not has_cycle:
28 return None # fast fell off the end: no cycle
29 
30 # 2. Phase 2 - Locate Cycle Origin:
31 # head and the meeting point are the same number of steps from the cycle's first node, up to whole laps, so walking both one step at a time lands them on it together.
32 ptr1 = head
33 ptr2 = slow
34 while ptr1 != ptr2:
35 ptr1 = ptr1.next
36 ptr2 = ptr2.next
37 
38 return ptr1

Inside a cycle, fast gains exactly one step on slow per move, so it can't jump over it: if there is a cycle they meet, and if fast runs off the end there is none (and slow is then at the middle).

Walking from the head and walking from the meeting point reach the cycle's first node after the same number of steps (the second walk may add whole laps), so ptr1 from head and ptr2 from the meeting point, one step at a time, both land on it.

The trap is the loop test: write while fast and fast.next, because fast.next.next crashes when fast.next is None; testing only fast crashes on every list without a cycle that has an odd number of nodes.

Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Writing while fast instead of while fast and fast.next

    Mistake
    while fast:
    Fix
    while fast and fast.next:

    What happens: Each step moves fast two nodes with fast.next.next. When fast is the last node, fast.next is None. Then fast.next.next reads an attribute of None, and Python raises an AttributeError. This happens on every list without a cycle that has an odd number of nodes.

    Example: head = [1, 2, 3], pos = -1 (no cycle)
    Mistake: raises AttributeErrorFix: returns None
  2. 2

    Gotcha 2: Moving ptr2 two steps at a time in phase 2

    Mistake
    ptr2 = ptr2.next.next
    Fix
    ptr2 = ptr2.next

    What happens: head and the meeting point are the same number of steps from the cycle's first node, counting whole laps. That holds only when both pointers move one step at a time. With two steps for ptr2, the pointers can meet at another node.

    Example: head = [3, 2, 0, -4], pos = 1 (the cycle starts at the node 2)
    Mistake: returns -4Fix: returns 2
  3. 3

    Gotcha 3: Expecting slow to stop at the first of two middles

    What happens: Without a cycle, slow is at the middle when fast runs off the end. With an even number of nodes, there are two middles. Starting with slow = fast = head stops slow at the second one. Start fast at head.next to stop slow at the first one.

  4. 4

    Gotcha 4: Starting the pointers at an index other than 0

    What happens: Find the Duplicate Number (LC 287) reads the array as a linked list. Index i points to index nums[i]. Every value is between 1 and n, so no index points to index 0. So index 0 is outside the cycle, and the cycle's first node is the duplicate value. From another index, the walk can start inside the cycle and return a value that is not the duplicate.

Canonical LeetCode:Linked List Cycle II (LC 142)Linked List Cycle (LC 141)Find the Duplicate Number (LC 287)Middle of the Linked List (LC 876)Circular Array Loop (LC 457)Reorder List (LC 143)Happy Number (LC 202)
Open the Fast & Slow Pointers card
10Merge Two Sorted
two sorted sequences ➔ take the smaller head, move only its pointer ➔ drain the other sequence ➔ one pass

Two-Sequence Merge Pointers

Spot it when: Two sorted arrays or lists must be read together in one pass: merged into one order, intersected, compared value by value, or joined at the values they share.
Mode 10 · Merge Two Sorted
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 merge_two_lists(list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
9 """Merge Two Sorted Lists: relink the smaller head of the two lists, one node at a time.
10 Time: O(M + N) | Space: O(1) extra
11 """
12 # list1 and list2 are the two pointers: each one is the first unread node of its own list
13 dummy = ListNode(0) # Fake first node, so the first real node is attached like every other
14 tail = dummy # Last node of the merged list built so far
15 
16 # 1. Take The Smaller Head:
17 # Both lists are sorted, so the smaller head is the smallest unread node of both. Attach it, then move only the pointer of the list it came from.
18 while list1 and list2:
19 if list1.val <= list2.val:
20 tail.next = list1
21 list1 = list1.next
22 else:
23 tail.next = list2
24 list2 = list2.next
25 tail = tail.next
26 
27 # 2. Attach The Rest Of The Other List:
28 # The loop stops when one list is empty. The other list is already sorted and already linked, so one assignment attaches all of it.
29 tail.next = list1 if list1 else list2
30 
31 # 3. Return The Node After The Dummy:
32 # dummy only marks the start, so the merged list begins at dummy.next.
33 return dummy.next

Each list is sorted, so its first unread node is its smallest, and the smaller of list1 and list2 is the smallest unread node of both lists: attach that node to tail and move only the pointer of the list it came from. The other pointer stays where it is, because its node is not smaller than the node just attached and still has to be compared with the next node of the other list. Each step attaches the smallest node that is left, so the merged list grows in sorted order and every node is attached exactly once. When list1.val equals list2.val, <= takes the node from list1 first, so equal values keep the order in which the two lists were given. The trap is the end of the loop: while list1 and list2 stops as soon as one list is empty, and the other list still holds unread nodes. Attach them with tail.next = list1 if list1 else list2, or every node after the last comparison is lost. Two arrays have no links to reuse, so they need a drain loop that copies the leftover values one by one.

Gotchas1 of 5

Gotcha 1 of 5

  1. 1
    The trap

    Gotcha 1: Leaving out the attach of the other list after the loop

    Mistake
    # nothing after the loop
    Fix
    tail.next = list1 if list1 else list2

    What happens: while list1 and list2 stops as soon as one list is empty. The other list still holds nodes that were never compared. Without the line tail.next = list1 if list1 else list2, tail.next stays None and those nodes are not in the result.

    Example: list1 = [1, 2, 4], list2 = [1, 3, 4]
    Mistake: returns [1, 1, 2, 3, 4]Fix: returns [1, 1, 2, 3, 4, 4]
  2. 2

    Gotcha 2: Looping while either list has a node

    Mistake
    while list1 or list2:
    Fix
    while list1 and list2:

    What happens: Inside the loop, list1.val <= list2.val reads the val of both heads. When one list is empty, its pointer is None and None.val raises an error. The loop must stop when either list is empty, and the next line attaches the other list.

    Example: list1 = [5], list2 = [1, 2, 6]
    Mistake: raises AttributeErrorFix: returns [1, 2, 5, 6]
  3. 3

    Gotcha 3: Using < so that a tie takes the node from list2

    Mistake
    if list1.val < list2.val:
    Fix
    if list1.val <= list2.val:

    What happens: When both heads hold the same value, < is False, so the else branch attaches the node from list2 first. The values in the result are the same, but equal nodes from list1 now come after equal nodes from list2. This matters when each node carries more data than its value, because a stable merge keeps equal keys in the order the two lists were given.

    Example: list1 = [1] (node a), list2 = [1] (node b)
    Mistake: returns FalseFix: returns True
  4. 4

    Gotcha 4: Forgetting to move tail after an attach

    Mistake
    pass
    Fix
    tail = tail.next

    What happens: tail must always be the last node of the merged list. If it stays on dummy, every attach overwrites dummy.next, so each new node replaces the one attached before it. Only the node from the last assignment survives, together with the nodes that follow it in its own list.

    Example: list1 = [1, 2, 4], list2 = [1, 3, 4]
    Mistake: returns [4]Fix: returns [1, 1, 2, 3, 4, 4]
  5. 5

    Gotcha 5: Returning dummy instead of dummy.next

    Mistake
    return dummy
    Fix
    return dummy.next

    What happens: dummy is the fake node with value 0 that sits before the first real node. Returning dummy returns a list that starts with this extra 0, so the result is one node longer than the merge.

    Example: list1 = [1, 2, 4], list2 = [1, 3, 4]
    Mistake: returns [0, 1, 1, 2, 3, 4, 4]Fix: returns [1, 1, 2, 3, 4, 4]
Canonical LeetCode:Merge Two Sorted Lists (LC 21)Merge Sorted Array (LC 88)Get the Maximum Score (LC 1537)Interval List Intersections (LC 986)Minimum Common Value (LC 2540)Meeting Scheduler (LC 1229)
Open the Merge Two Sorted card
11Linked List Reversal
head ➔ rewire each next pointer ➔ reversed list

In-Place Linked List Reversal (Reverse Linked List II, Reverse Nodes in k-Group)

Spot it when: You must reverse all or part of a singly linked list in place: the whole list, the nodes between two positions, every group of k nodes, or the second half before comparing or merging it with the first.
Mode 11 · Linked List Reversal
Python
1from typing import Optional
2 
3 
4class ListNode:
5 def __init__(self, val: int = 0, next: Optional["ListNode"] = None):
6 self.val = val
7 self.next = next
8 
9 
10def reverse_list(head: Optional[ListNode]) -> Optional[ListNode]:
11 """Reverse Linked List: flip every next pointer in one pass.
12 Time: O(N) | Space: O(1)
13 """
14 prev = None # Head of the reversed part: empty at the start
15 curr = head # First node not flipped yet
16 while curr:
17 nxt = curr.next # Save the rest BEFORE the flip overwrites curr.next
18 curr.next = prev # Flip: curr now points back at the reversed part
19 prev = curr # The reversed part grows by one node
20 curr = nxt # Step into the saved rest
21 return prev # The old tail is the new head
22 
23 
24def reverse_between(head: Optional[ListNode], left: int, right: int) -> Optional[ListNode]:
25 """Reverse Linked List II: reverse positions left..right (1-indexed) in one pass.
26 Time: O(N) | Space: O(1)
27 """
28 dummy = ListNode(0, head) # The segment may start at the head: give it a node before it
29 before = dummy
30 for _ in range(left - 1): # Walk to the node just before position left
31 before = before.next
32 prev = None
33 curr = before.next
34 for _ in range(right - left + 1): # The same flip, exactly right - left + 1 times
35 nxt = curr.next
36 curr.next = prev
37 prev = curr
38 curr = nxt
39 tail = before.next # The segment's old first node is now its last
40 before.next = prev # Reattach the reversed segment...
41 tail.next = curr # ...and hang the untouched rest behind it
42 return dummy.next

Reversing a singly linked list only needs every next pointer to point the other way, so walk the list once with two pointers: prev heads the part already reversed (it starts as None) and curr is the first node not flipped yet; each step points curr.next back at prev, then moves prev and curr one node forward. Each step moves exactly one node from the front of the untouched part to the front of the reversed part, so no node is lost or visited twice, and when curr falls off the end, prev is the new head. reverse_between runs the same steps right - left + 1 times from the node before position left, then reconnects both ends with before.next = prev and tail.next = curr. The trap is the order of the lines: curr.next = prev overwrites the only link to the rest of the list, so save nxt = curr.next first; flip first and every node after curr is lost.

Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Writing curr.next = prev before saving nxt

    Mistake
    while curr:
        curr.next = prev
        nxt = curr.next
    Fix
    while curr:
        nxt = curr.next
        curr.next = prev

    What happens: curr.next is the only link from curr to the rest of the list. curr.next = prev overwrites that link. Then nxt = curr.next saves prev, which is None on the first step. The loop ends after one node, and every node after curr is lost.

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

    Gotcha 2: Returning head instead of prev

    Mistake
    return head
    Fix
    return prev

    What happens: When the loop ends, curr is None, and prev is the old last node. That node is the new head. The old head is now the tail, and its next is None. Returning it gives a list with one node.

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

    Gotcha 3: Starting before at head instead of dummy

    Mistake
    before = head
    Fix
    before = dummy

    What happens: before must be the node just before position left. When left is 1, that is the node before the head. Only dummy can be there. Starting at head puts before one node too far, so the wrong segment is reversed.

    Example: head = [1, 2, 3, 4, 5], left = 1, right = 2
    Mistake: returns [1, 3, 2, 4, 5]Fix: returns [2, 1, 3, 4, 5]
  4. 4

    Gotcha 4: Flipping a last group shorter than k

    Mistake
    while node and count < k:
        node = node.next
        count += 1
    Fix
    while node and count < k:
        node = node.next
        count += 1
    if count < k:
        return dummy.next

    What happens: In Reverse Nodes in k-Group (LC 25), a last group with fewer than k nodes must keep its order. The count loop stops when the list ends, so count is less than k for that group. Without if count < k, the flip loop still reverses those count nodes. The last group comes back reversed.

    Example: head = [1, 2, 3, 4, 5], k = 3
    Mistake: returns [3, 2, 1, 5, 4]Fix: returns [3, 2, 1, 4, 5]
Canonical LeetCode:Reverse Linked List (LC 206)Reverse Linked List II (LC 92)Palindrome Linked List (LC 234)Reorder List (LC 143)Reverse Nodes in k-Group (LC 25)
Open the Linked List Reversal card
12Read-Write Pointers
reader -> writer -> (Compaction Frontier)

Read/Write Pointers (In-Place Array Modification, Dutch National Flag)

Spot it when: You must remove, deduplicate or move elements in place and return the new length (or leave the array partitioned).
Mode 12 · Read-Write Pointers
Python
1from typing import Callable, List, TypeVar
2 
3Item = TypeVar("Item")
4 
5 
6def read_and_write(items: List[Item], keep: Callable[[Item, int], bool]) -> int:
7 """Read-Write Pointers: move every kept item to the front of items, in order, in place.
8 keep(value, writer) returns True to keep value. writer is the number of items kept so far.
9 items[:writer] holds those items, so keep may compare value with items[writer - 1].
10 Precondition: keep reads only value and items[:writer], never a slot at writer or later.
11 Returns writer. The slots from writer on still hold old values. Move Zeroes writes its zeros there afterwards.
12 N = len(items). T = the cost of one keep call.
13 Time: O(N * T) | Space: O(1)
14 """
15 writer = 0 # items[:writer] holds the kept items, in order
16 # 1. Read Every Item, Write Only The Kept Ones:
17 # writer never gets ahead of reader, so items[writer] = items[reader] always lands on a slot whose old value has already been read, never one still waiting to be read.
18 for reader in range(len(items)):
19 if keep(items[reader], writer):
20 items[writer] = items[reader]
21 writer += 1
22 
23 return writer # The number of kept items

writer marks where the next kept element goes; everything before it is final. reader visits every element and copies it to writer only when it should be kept.

Since writer never passes reader, a write never lands on an element that still has to be read, so the array can be rewritten in place. For sorted deduplication, start both at 1, because nums[0] is always kept.

The trap is comparing with the wrong slot: check nums[reader] against nums[writer - 1], the last element kept, not nums[writer], the next free slot, which still holds an old value; on LeetCode Example 2 of Remove Duplicates from Sorted Array (LC 26), [0,0,1,1,1,2,2,3,3,4], the wrong slot keeps 7 values instead of 5.

Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Comparing with items[writer] instead of items[writer - 1]

    Mistake
    if nums[reader] != nums[writer]:
    Fix
    if nums[reader] != nums[writer - 1]:

    What happens: nums[writer - 1] is the last value kept. nums[writer] is the next free slot. It still holds an old value from the input. Comparing with that old value can keep a duplicate. It can also drop a value that is new.

    Example: Remove Duplicates from Sorted Array (LC 26), Example 2: nums = [0, 0, 1, 1, 1, 2, 2, 3, 3, 4]
    Mistake: returns 7Fix: returns 5
  2. 2

    Gotcha 2: Comparing with items[reader - 2] instead of items[writer - 2]

    Mistake
    if nums[reader] != nums[reader - 2]:
    Fix
    if nums[reader] != nums[writer - 2]:

    What happens: Remove Duplicates from Sorted Array II (LC 80) keeps at most two copies of each value. A value is kept when it differs from nums[writer - 2], the second-to-last value kept. nums[reader - 2] may already hold a value that the writer copied there. Then the check compares with the wrong value and drops a copy that should stay.

  3. 3

    Gotcha 3: Leaving old values after writer in Move Zeroes (LC 283)

    Mistake
    nums[writer] = nums[reader]
    Fix
    nums[writer], nums[reader] = nums[reader], nums[writer]

    What happens: Move Zeroes (LC 283) must end with every zero after the non-zero values. Copying nums[reader] to nums[writer] leaves the old values after writer in place. Swap the two values instead. You can also write zeros from writer to the end after the loop. Either way, the non-zero values keep their order.

  4. 4

    Gotcha 4: Moving mid after a swap with high

    Mistake
    nums[mid], nums[high] = nums[high], nums[mid]
    high -= 1
    mid += 1
    Fix
    nums[mid], nums[high] = nums[high], nums[mid]
    high -= 1

    What happens: Sort Colors (LC 75) uses three pointers: low, mid and high. When nums[mid] is 2, it is swapped with nums[high], and high moves left. The value that arrives at mid has not been checked yet. Moving mid past it can leave a 0 or a 2 in the middle.

Canonical LeetCode:Remove Duplicates from Sorted Array (LC 26)Remove Duplicates from Sorted Array II (LC 80)Move Zeroes (LC 283)Sort Colors (LC 75)Partition Array According to Given Pivot (LC 2161)Remove Element (LC 27)Sort Array By Parity (LC 905)Rearrange Array Elements by Sign (LC 2149)
Open the Read-Write Pointers card
13Prefix Sums
nums ➔ prefix with n + 1 slots, prefix[0] = 0 ➔ prefix[right + 1] - prefix[left]

Prefix Sum Array

Spot it when: The array does not change, and you must answer many questions about the sum, count or XOR of a given range left..right. Or you need the running total at each position, counted from the start.
Mode 13 · Prefix Sums
Python
1class NumArray:
2 """Range Sum Query - Immutable: build the prefix array once, then answer each range with one subtraction.
3 Time: O(N) to build, O(1) per sumRange | Space: O(N)
4 """
5 
6 def __init__(self, nums: list[int]) -> None:
7 # prefix[i]: sum of the first i elements of nums; prefix[0] = 0, the sum of no elements
8 self.prefix = [0] * (len(nums) + 1)
9 # 1. Build The Prefix Array Once:
10 # Each next slot adds one more element to the previous running total, so prefix[i + 1] is always one element ahead of prefix[i].
11 for i, num in enumerate(nums):
12 self.prefix[i + 1] = self.prefix[i] + num
13 
14 def sumRange(self, left: int, right: int) -> int:
15 # 1. Subtract The Two Running Totals:
16 # The first right + 1 elements are the first left elements followed by nums[left..right], so removing the first left elements' sum leaves exactly that range.
17 return self.prefix[right + 1] - self.prefix[left]

Build self.prefix once, with one more slot than nums has elements. self.prefix[0] = 0, and each next slot adds one element: self.prefix[i + 1] = self.prefix[i] + num. Then sumRange answers any range with one subtraction, self.prefix[right + 1] - self.prefix[left]. This is correct because the first right + 1 elements are the first left elements followed by nums[left..right]. Subtracting the sum of the first left elements leaves exactly the range. The leading 0 is the sum of no elements, so left = 0 uses the same formula with no special case. The trap is the index shift. self.prefix[i] stops just before nums[i], so the range ends at self.prefix[right + 1]. Writing self.prefix[right] leaves nums[right] out of every answer.

Gotchas1 of 3

Gotcha 1 of 3

  1. 1
    The trap

    Gotcha 1: Writing self.prefix[right] instead of self.prefix[right + 1]

    Mistake
    return self.prefix[right] - self.prefix[left]
    Fix
    return self.prefix[right + 1] - self.prefix[left]

    What happens: self.prefix[i] is the sum of the elements before index i. So self.prefix[right] stops just before nums[right]. The subtraction then returns nums[left..right - 1], one element short. On a one-element range it returns 0.

    Example: nums = [-2, 0, 3, -5, 2, -1], sumRange(0, 2)
    Mistake: returns -2Fix: returns 1
  2. 2

    Gotcha 2: Dropping the leading 0 and reading self.prefix[left - 1]

    Mistake
    self.prefix = []  # prefix[i] = sum of nums[0..i]
    ...
    return self.prefix[right] - self.prefix[left - 1]
    Fix
    self.prefix = [0] * (len(nums) + 1)
    ...
    return self.prefix[right + 1] - self.prefix[left]

    What happens: Without the leading 0, the list has n slots and self.prefix[i] includes nums[i]. A range then needs self.prefix[left - 1]. When left = 0, that is self.prefix[-1]. Python reads the last slot, the sum of the whole array, and raises no error. So a range that starts at index 0 is wrong whenever the whole array does not sum to 0.

    Example: nums = [-2, 0, 3, -5, 2, -1], sumRange(0, 2)
    Mistake: returns 4Fix: returns 1
  3. 3

    Gotcha 3: Adding up the range inside sumRange

    Mistake
    self.nums = nums
    ...
    return sum(self.nums[left : right + 1])
    Fix
    return self.prefix[right + 1] - self.prefix[left]

    What happens: The answers stay correct, but each call reads every element of its range. With 10^4 elements and 10^4 calls, that is up to 10^8 additions. The prefix array does the adding once, in __init__. After that, every call is one subtraction.

Canonical LeetCode:Range Sum Query - Immutable (LC 303)Range Sum Query 2D - Immutable (LC 304)Matrix Block Sum (LC 1314)Find the Highest Altitude (LC 1732)Number of Ways to Split Array (LC 2270)Count Vowel Strings in Ranges (LC 2559)XOR Queries of a Subarray (LC 1310)Special Array II (LC 3152)Find Pivot Index (LC 724)Running Sum of 1d Array (LC 1480)Minimum Value to Get Positive Step by Step Sum (LC 1413)
Open the Prefix Sums card
14Greedy Subsequence Match
i -> s, j -> t (claim s[i] only on a match, i reaches len(s))

Subsequence Matching (Earliest-Match Two Pointers)

Spot it when: You must decide whether every character of one string, in order, can be found somewhere inside a second, usually longer string, and skipping characters of the second string is always allowed.
Mode 14 · Greedy Subsequence Match
Python
1def isSubsequence(s: str, t: str) -> bool:
2 """Is Subsequence: walk t once with j, and advance i in s only when the current characters match.
3 Time: O(len(t)) | Space: O(1)
4 """
5 i = 0 # Characters of s already claimed, in order
6 for j in range(len(t)):
7 if i < len(s) and s[i] == t[j]:
8 i += 1 # The earliest available match for s[i] in t never costs a later match
9 
10 return i == len(s)

i counts how many characters of s are already claimed, in order; walking t once with j, advance i only when t[j] equals s[i], the next character of s still unclaimed. s is a subsequence of t exactly when i reaches len(s) by the time the walk ends. Claiming the earliest copy of s[i] in t is always safe: every character of t after that point is still free for the rest of s, so no earlier choice can ever cost a later match, and a later copy would only leave less of t behind for what comes next. The trap is advancing i on every character of t instead of only on a match: i += 1 outside the if claims characters that were never actually matched, so the final check i == len(s) can pass even when s is not really a subsequence of t.

Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Advancing i on every character of t, not only on a match (the card's trap)

    Mistake
    for j in range(len(t)):
        i += 1
    Fix
    for j in range(len(t)):
        if i < len(s) and s[i] == t[j]:
            i += 1

    What happens: i is supposed to count only the characters of s that were actually matched in t, in order. Moving i += 1 outside the if makes i count every character of t instead. i then reaches len(s) as soon as t is at least as long as s, no matter what characters either string holds, so the final check i == len(s) says True for pairs that are not a real subsequence match.

    Example: s = "abc", t = "ahbgdc"
    Mistake: returns FalseFix: returns True
  2. 2

    Gotcha 2: Writing i == len(s) - 1 instead of i == len(s)

    Mistake
    return i == len(s) - 1
    Fix
    return i == len(s)

    What happens: i reaches len(s) only once every character of s is claimed, since i starts at 0 and the last character claimed brings it to len(s), not len(s) - 1. Comparing against len(s) - 1 rejects a correct full match, one claim short of where the mistake expects it.

    Example: s = "abc", t = "ahbgdc"
    Mistake: returns FalseFix: returns True
  3. 3

    Gotcha 3: Returning True as soon as the scan of t finishes, instead of checking i

    Mistake
    if j == len(t) - 1:
        return True
    Fix
    return i == len(s)

    What happens: Reaching the last index of t only means the walk is over; it says nothing about whether every character of s was claimed. Returning True there confuses "finished scanning t" with "matched all of s", so the function reports a match for every non-empty t, even when most of s was never found.

    Example: s = "axc", t = "ahbgdc"
    Mistake: returns TrueFix: returns False
  4. 4

    Gotcha 4: Checking s[i] in t instead of walking t in order

    Mistake
    for ch in s:
        if ch in t:
            i += 1
    Fix
    for j in range(len(t)):
        if i < len(s) and s[i] == t[j]:
            i += 1

    What happens: ch in t only asks whether the character exists somewhere in t, with no memory of how far through t the previous match reached. Two characters of s can then both match the same early position of t, or match positions of t in the wrong order, and the mistake still counts both as claimed. The order that s must appear in is never checked.

    Example: s = "ba", t = "ab"
    Mistake: returns TrueFix: returns False
Canonical LeetCode:Is Subsequence (LC 392)Longest Word in Dictionary through Deleting (LC 524)Append Characters to String to Make Subsequence (LC 2486)Make String a Subsequence Using Cyclic Increments (LC 2825)
Open the Greedy Subsequence Match card
15Circular Array
circular array ➔ walk indices 0..2n-1 with i % n ➔ see past the end

Circular Array Handling (Virtual Doubling)

Spot it when: The array is circular (the end wraps to the start) and a window, a stack or a scan must see past the end.
Mode 15 · Circular Array
Python
1def next_greater_elements(nums: list[int]) -> list[int]:
2 """Next Greater Element II: scan the virtually doubled array right to left.
3 Time: O(N) | Space: O(N)
4 """
5 n = len(nums)
6 res = [-1] * n
7 stack: list[int] = [] # Candidate values, decreasing from bottom to top
8 
9 # 1. Scan The Doubled Array Right To Left:
10 # i % n reads the circular array twice without copying it, so by the time i reaches a real index, every value that wraps around to its right has already been seen.
11 for i in range(2 * n - 1, -1, -1):
12 val = nums[i % n]
13 # 2. Drop Candidates val Blocks:
14 # A stacked value that is not strictly greater than val can never be the answer for anything further left, since val stands in front of it and is at least as big.
15 while stack and stack[-1] <= val:
16 stack.pop()
17 # 3. Record Only On The Real Pass:
18 # i >= n is only priming the stack with the wrap-around values, so the answer is written exclusively when i < n, a real index.
19 if i < n:
20 res[i] = stack[-1] if stack else -1
21 stack.append(val)
22 
23 return res

A circular array written twice in a row contains every wrap-around, so loop i from 2 * n - 1 down to 0 and read nums[i % n] instead of copying. Scanning right to left, the steps with i >= n fill stack with the values that wrap around, so when i reaches a real index, everything after it on the circle has already been seen; that is why res[i] is written only when i < n. The trap is equal values: pop while stack[-1] <= val, or a value equal to val stays on top and is reported as its next greater element.

Gotchas1 of 3

Gotcha 1 of 3

  1. 1
    The trap

    Gotcha 1: Popping with stack[-1] < val instead of <=

    Mistake
    while stack and stack[-1] < val:
    Fix
    while stack and stack[-1] <= val:

    What happens: A value equal to val is not greater than val. With <, an equal value stays on the stack. It is then on top, and it is reported as the next greater element.

    Example: nums = [5, 5, 5]
    Mistake: returns [5, 5, 5]Fix: returns [-1, -1, -1]
  2. 2

    Gotcha 2: Writing res[i] without the i < n check

    Mistake
    res[i] = stack[-1] if stack else -1
    Fix
    if i < n:
        res[i] = stack[-1] if stack else -1

    What happens: The loop runs i from 2 * n - 1 down to 0, but res has only n slots. For i >= n, res[i] is past the end, and Python raises an IndexError. Those first n steps only fill the stack with the values that wrap around.

    Example: nums = [1, 2, 1]
    Mistake: raises IndexErrorFix: returns [2, -1, 2]
  3. 3

    Gotcha 3: Building nums + nums instead of reading nums[i % n]

    What happens: nums + nums makes a real copy of the array, which costs O(N) extra memory. nums[i % n] reads the same values from the one array. The answers are the same. Circular windows read nums[i % n] in the same way. Two examples are Defuse the Bomb (LC 1652) and Minimum Swaps to Group All 1's Together II (LC 2134).

Canonical LeetCode:Next Greater Element II (LC 503)Minimum Swaps to Group All 1's Together II (LC 2134)Defuse the Bomb (LC 1652)Alternating Groups II (LC 3208)Design Circular Queue (LC 622)Maximum Sum Circular Subarray (LC 918)
Open the Circular Array card
Built on the mental models above
  • Hash-Set Sequence ScanningStart-Element Pruning (Longest Consecutive Sequence)

    Store the values in a set and walk a run only from its first value, so every value is visited once.

  • Counting SortCounting Sort (H-Index, Maximum Ice Cream Bars)

    The values are small whole numbers, so each value gets its own slot instead of a range of values.

  • Longest Palindromic SubstringExpand Around Center (Longest Palindromic Substring)

    Start both pointers at a center and move them outward while the two ends still match.

  • Staircase SearchSaddleback Search (Staircase Search in a Sorted Matrix)

    The pointers are a row and a column in a sorted grid, and each comparison discards a whole row or a whole column.

  • Next PermutationNext Lexicographic Permutation (Narayana Pandita's Algorithm, std::next_permutation)

    Find the pivot from the right, swap it with the next larger value, then reverse the suffix with two pointers.

  • Three-Reversals AlgorithmIn-Place Array Rotation (Rotate Array)

    Reverse the whole range, then reverse each of the two parts, so the items rotate in place.

  • Monotonic DequeMonotonic Deque (Sliding Window Maximum)

    The window state is a deque of candidate indices whose values only decrease, so the window maximum is always at the front.

  • Dual Monotonic DequeDual Monotonic Deque (Longest Subarray Within a Max-Min Limit)

    Keep a second deque for the minimum, and move the left end while the maximum minus the minimum is above the limit.

  • Shortest WindowVariable-Size Sliding Window (Minimum Window)

    Shrink the window while it is still valid and record its length each time, instead of shrinking only when it becomes invalid.

  • At-Most TrickExactly K = At Most K − At Most (K−1)

    Count the windows that end at each right end with an at-most count, then subtract two at-most counts to get an exact count.

  • Fixed-Offset PointersFixed-Offset Pointers (Nth Node From End)

    Start one pointer n steps ahead, then move both one step at a time, so the gap between them stays fixed.

  • Cyclic SortCyclic Sort (Index as Hash, First Missing Positive)

    Swap each value to the index where it belongs, so the array itself records which values are present.

  • Prefix-Suffix ScanPrefix & Suffix Passes (Left-Right Scans)

    The operation has no inverse, so keep one pass from the left and one pass from the right instead of subtracting prefixes.

  • Prefix Sum MapPrefix Sum + Hash Map

    Keep a map from each earlier prefix sum to how often it occurred, so a range with the target sum is found in one pass.

  • Difference ArrayDifference Array (Range Updates)

    Write a mark at each end of a range, then take one prefix sum to read the total at every position.

The Two Pointers Decision Rule (Problem Classification Checklist)

When facing an unseen array, string or linked-list problem, ask in this order:

Step 1.Does the array never change, and are you told the ranges (`left` and `right` are given) and asked for their sum, count or XOR? Or do you only need the running total at each position?
➔ Prefix Sums
Range Sum Query - Immutable (LC 303)Range Sum Query 2D - Immutable (LC 304)

Build prefix with n + 1 slots and prefix[0] = 0, where prefix[i + 1] = prefix[i] + nums[i]; then the sum of nums[left..right] is prefix[right + 1] - prefix[left].

Step 2.Do you count or find subarrays with an exact sum (or a sum divisible by k, or equal 0s and 1s), especially with negative values?
➔ Prefix Sum Map
Subarray Sum Equals K (LC 560)Contiguous Array (LC 525)

Keep a map of prefix sums seen, seeded with {0: 1}; at each prefix p, add count[p - k] before recording p.

Step 3.Is the window length k given, and you measure every window of that length?
➔ Fixed Window
Find All Anagrams in a String (LC 438)Maximum Average Subarray I (LC 643)

Add nums[i]; once i >= k, remove nums[i - k]; read the window after both.

Step 4.Do you want the longest contiguous subarray or substring that stays within a limit (no repeats, at most k distinct, at most k zeros)?
➔ Longest Window
Longest Substring Without Repeating Characters (LC 3)Fruit Into Baskets (LC 904)

Before growing right, check whether adding it would break the limit; while it would, shrink left; once it would not, grow right and record right - left + 1.

Step 5.Do you want the shortest contiguous subarray or substring that meets a condition, where adding elements only makes it easier to meet?
➔ Shortest Window
Minimum Size Subarray Sum (LC 209)Minimum Window Substring (LC 76)

Grow right until the window is valid; while it stays valid, record its length and shrink left.

Step 6.Must you count subarrays with exactly K of something, where adding an element never lowers that amount?
➔ At-Most Trick
Subarrays with K Different Integers (LC 992)Binary Subarrays With Sum (LC 930)

exactly(K) = atMost(K) - atMost(K - 1), where each atMost window adds right - left + 1 per right.

Step 7.Does every position of a sliding window need the window's maximum or minimum?
➔ Monotonic Deque
Sliding Window Maximum (LC 239)Shortest Subarray with Sum at Least K (LC 862)

Keep a deque of indices with decreasing values: pop smaller values from the back, drop the front once it leaves the window; the front is the maximum.

Step 8.Does the window's validity depend on its maximum AND its minimum at once, such as staying valid only while max minus min stays within a limit?
➔ Dual Monotonic Deque
Longest Continuous Subarray With Absolute Diff Less Than or Equal to Limit (LC 1438)Continuous Subarrays (LC 2762)

Run two deques side by side, one decreasing (front = max) and one increasing (front = min); while the gap between the fronts exceeds the limit, shrink from the left, checking both fronts.

Step 9.Is the array sorted (or can it be), and you look for pairs or triples meeting a target, or compare the two ends?
➔ Converging Pointers
Two Sum II - Input Array Is Sorted (LC 167)3Sum (LC 15)

Start left = 0, right = n - 1 and move the pointer whose side can no longer be part of an answer.

Step 10.Must two sorted arrays or lists be read together in one pass: merged, intersected or compared?
➔ Merge Two Sorted
Merge Two Sorted Lists (LC 21)Merge Sorted Array (LC 88)

Keep one pointer per sequence, take the smaller head and advance only that pointer; drain the leftovers.

Step 11.Are the matrix's rows and columns both sorted, and you must find a value or walk the boundary between too small and too big?
➔ Staircase Search
Search a 2D Matrix II (LC 240)Find Positive Integer Solution for a Given Equation (LC 1237)

Start at the top-right corner: a value too big moves left, a value too small moves down.

Step 12.Must elements be removed, deduplicated or moved in place, returning the new length?
➔ Read-Write Pointers
Remove Duplicates from Sorted Array (LC 26)Remove Duplicates from Sorted Array II (LC 80)

The reader scans every element; the writer marks where the next kept element goes; copy on keep.

Step 13.Are the values meant to be 1..n in an array of length n, and you must find what is missing or repeated with O(1) extra space?
➔ Cyclic Sort
First Missing Positive (LC 41)Find All Numbers Disappeared in an Array (LC 448)

Swap each value v into index v - 1 until it is home or a duplicate; then the first i with nums[i] != i + 1 answers.

Step 14.Does each position's answer depend on everything to its left and everything to its right?
➔ Prefix-Suffix Scan
Product of Array Except Self (LC 238)Trapping Rain Water (LC 42)

One pass left to right fills the prefix part, one pass right to left combines the suffix part; neither includes nums[i].

Step 15.Must many range updates be applied before any value is read?
➔ Difference Array
Corporate Flight Bookings (LC 1109)Range Addition (LC 370)

diff[l] += v and diff[r + 1] -= v per update, then one running sum recovers every value.

Step 16.Is the question 'have I seen this before?', 'how many of each?' or 'which items belong together?'?
➔ Hash Map
Group Anagrams (LC 49)Two Sum (LC 1)

Choose the key (the value, its complement, or a canonical form) and read or update dict[key] in O(1) per item.

Step 17.Do you need the longest run of consecutive integers that all appear in an unsorted array, in O(n) time?
➔ Hash-Set Sequence Scanning
Longest Consecutive Sequence (LC 128)Longest Square Streak in an Array (LC 2501)

Put every value in a hash set; for each value whose predecessor is absent, walk upward while the next value is present, and keep the longest walk.

Step 18.Must you decide whether one string's characters all appear, in order, somewhere inside a second, usually longer string, with skipping always allowed?
➔ Greedy Subsequence Match
Is Subsequence (LC 392)Longest Word in Dictionary through Deleting (LC 524)

Walk the second string once with j; advance i in the first string only when the current characters match; the first string is matched once i reaches its length.

Step 19.Is the array circular, so a scan, a window or a stack must see past the end?
➔ Circular Array
Next Greater Element II (LC 503)Minimum Swaps to Group All 1's Together II (LC 2134)

Loop i over 0..2n - 1 and read nums[i % n], recording answers only for i < n.

Step 20.Might a linked list (or next = nums[i]) contain a loop, or do you need its middle in one pass with O(1) space?
➔ Fast & Slow Pointers
Linked List Cycle II (LC 142)Linked List Cycle (LC 141)

slow moves 1 step, fast moves 2; they meet inside a cycle; restart one at head and move both 1 step to meet at the entry.

Step 21.Must you reach or remove the node that is a given number of nodes from the end of a linked list, in one pass?
➔ Fixed-Offset Pointers
Remove Nth Node From End of List (LC 19)Rotate List (LC 61)

Move lead n steps ahead of trail, then move both one step at a time; when lead is the last node, trail is n steps behind it.

Step 22.May the head of a linked list be removed, replaced or not known yet?
➔ Dummy Node
Remove Linked List Elements (LC 203)Merge Two Sorted Lists (LC 21)

Set dummy.next = head, edit through prev starting at dummy, and return dummy.next.

Step 23.Must all or part of a singly linked list be reversed in place?
➔ Linked List Reversal
Reverse Linked List (LC 206)Reverse Linked List II (LC 92)

Save nxt = curr.next, point curr.next = prev, then move prev and curr forward.

Step 24.Do you need the longest palindromic substring, or how many substrings are palindromes?
➔ Longest Palindromic Substring
Longest Palindromic Substring (LC 5)Palindromic Substrings (LC 647)

Expand around each of the 2n - 1 centers (every letter and every gap) while the two ends match.

Step 25.Must an array be rotated or cyclically shifted in place?
➔ Three-Reversals Algorithm
Rotate Array (LC 189)Shift 2D Grid (LC 1260)

k %= n; reverse the whole array, then the first k elements, then the rest.

Step 26.Must one arrangement become the one right after it in dictionary order (the next permutation, the next larger number with the same digits), in place?
➔ Next Permutation
Next Permutation (LC 31)Next Greater Element III (LC 556)

Start at i = n - 2 and move i left while nums[i] >= nums[i + 1]. If i >= 0, swap nums[i] with the rightmost value in nums[i + 1:] that is larger than nums[i]. Then reverse nums[i + 1:]; when there is no such i (i = -1), this reverses the whole array.

Step 27.Are the values whole numbers in a small range (or can every value above n be treated as n), and you need them in order faster than a sort?
➔ Counting Sort
H-Index (LC 274)Maximum Ice Cream Bars (LC 1833)

Make one slot for each value. Add each value with count[v] += 1. Then walk the slots in order of value.

Step 28.Do values or counts fall in a known, bounded range, and you need their order, the top k or the largest gap in linear time?
➔ Bucket Sort
Maximum Gap (LC 164)Sort Characters By Frequency (LC 451)

Put each value into a bucket indexed by its value or count, then read the buckets in order.

Step 29.Must you find a value that holds more than half (or more than n / k) of the input in one pass with O(1) extra space?
➔ Boyer-Moore Voting
Majority Element (LC 169)Majority Element II (LC 229)

Keep k - 1 candidates with counts: a match adds 1, a free slot takes the value, otherwise every count drops by 1; verify in a second pass.

Step 30.Must you find an exact pattern in a long text, or a prefix that comes back later in the string, in linear time?
➔ KMP
Find the Index of the First Occurrence in a String (LC 28)Longest Happy Prefix (LC 1392)

Build the failure table lps; on a mismatch fall back to lps[j - 1] instead of restarting.

Pattern Architecture & Invariant Design Masterclass

The brute-force way to examine every pair or every contiguous stretch of a list is two nested loops: O(n²). Two Pointers is the observation that in many problems you never need to move an index backwards. If you can prove that, then both indices together take at most 2n steps across the entire run, and the whole problem collapses to O(n).

There are two shapes, and confusing them is where most students lose time:

  • Converging pointers. One index starts at the far left, one at the far right, and they walk toward each other. Each comparison lets you eliminate one end permanently. Used when the data is sorted or symmetric.
  • Sliding window. Both indices start at the left. right always moves forward, adding elements into a running "window state" (a sum, a character count, a max). left moves forward only to fix a window that has become invalid or to try shrinking a valid one. The window is always a contiguous stretch.
1.3 — Invariant-First Pointer Boundaries: Deriving < vs <= from Candidate State

A frequent source of off-by-one errors in two-pointer algorithms is guessing whether to write while left < right or while left <= right. The choice is not a matter of memorization—it is dictated by the state semantics of left == right: does the singleton state represent a valid candidate state that must be evaluated, or an invalid/redundant state?

1. When < is Appropriate (Distinct Pair Heuristic)
  • Problem Archetypes: Two Sum II (LC 167), Valid Palindrome (LC 125), Container With Most Water (LC 11).
  • The Candidate Set: The problem specifically seeks an answer formed by a pair of distinct indices (i,j)(i, j)(i,j) with i<ji < ji<j.
  • Why < Holds: When left == right, the two pointers converge on the exact same element. A single element cannot form a two-sum pair with itself (the problem forbids reusing the same element), cannot hold water between two boundary walls, and a single character in a palindrome is symmetrically identical to itself. Because the candidate pair set is completely exhausted when left == right, terminating immediately with while left < right: is mathematically sound and avoids wasted computation.
2. When <= is Required (Critical Counterexamples Where < Fails)
  • Counterexample 1: Three-Way In-Place Partitioning (Dutch National Flag / LC 75 Sort Colors) In 3-way partitioning, we maintain pointers low, mid, and high. The subarray nums[mid..high] represents the unexamined candidate elements. The loop must run while mid <= high:. If you mistakenly used mid < high, the loop would stop one iteration too early, leaving the final element at mid == high completely uninspected and unpartitioned!
  • Counterexample 2: Odd-Length Palindromic Center Expansion (LC 5, LC 647) When expanding outward from prospective centers, odd-length palindromes initialize left = i, right = i (the singleton state left == right). The loop expands while left >= 0 and right < len(s) and s[left] == s[right]. Here, evaluating left == right is not a waste—it is the foundational base case.
  • Counterexample 3: Residual Singleton Evaluation & Hoare Partitioning In quickselect or Hoare-style array partitioning, pointers sweep inward until they cross. The singleton meeting state often holds a pivot element or residual value that must be categorized into one of the two partitions before halting.
3. Framing the Heuristic in Interviews
  • The "Pair vs. Target" Rule of Thumb: It is fine to use the "Pair vs. Target" trick as a quick mental intuition check (distinct pairs stop at <, closed-interval single targets stop at <=). However, it is a memorable heuristic, not a universal law for pointer or binary-search loops. Correct boundary conditions depend on the precise invariant, whether endpoints are inclusive or exclusive, whether a problem needs the first/last valid position, and the update rules for left, right, and mid.
  • The Senior Interview Standard: State the candidate state invariant out loud: "The unexamined candidates are in the range [mid..high]. Because the candidate at mid == high has not yet been inspected, my loop condition must be while mid <= high:."

3.1 — Understanding Invariants & The 5 Invariant Shapes

Before tackling specific problems, the single most critical idea to master is the loop invariant:

An invariant is a statement about your program's state that is True before the loop starts, is temporarily broken during internal state updates, and is strictly restored before the next iteration begins.

The Box Transfer Analogy

Imagine transferring items from Box A to Box B one by one:

  1. Held (Pre-condition): A+B=TotalA + B = \text{Total}A+B=Total.
  2. Broken (In-flight): You pick up an item in your hand. Visible A+B=Total−1A + B = \text{Total} - 1A+B=Total−1.
  3. Restored (Post-condition): You place the item into Box B. A+B=TotalA + B = \text{Total}A+B=Total is true again.
Code / Blueprint
total = len(A) + len(B)
while A:
item = A.pop() # Invariant temporarily broken (item in hand)
B.append(item) # Invariant restored
assert len(A) + len(B) == total
 
The 5 Canonical Invariant Shapes in Two Pointers
Interactive Architecture Diagram
Synthesizing vector architecture diagram...

The Kept-Prefix Boundary Partition (Reader-Writer Shape):

  • nums[0..writer-1]: The Kept Frontier ("Kept-Prefix Boundary") — Contains only verified, deduplicated elements in finalized order.
  • nums[writer..reader-1]: Discardable Debris — Elements already scanned that were duplicates or filtered out.
  • nums[reader..n-1]: Untrusted Raw Stream — Unprocessed elements waiting to be scanned.
  • Key Invariant Rule: Every candidate decision nums[reader] is evaluated against the trusted boundary of truth (nums[writer - 1] or nums[writer - k]), never against the raw stream.

The Prefix Sum Rescue (when values can be negative):

  • The Prefix Sum Algebraic Rescue: By shifting from contiguous physical pointers to prefix accumulators P[i]=∑j=0i−1nums[j]P[i] = \sum_{j=0}^{i-1} nums[j]P[i]=∑j=0i−1​nums[j], any contiguous subarray sum nums[L…R]nums[L \dots R]nums[L…R] equals P[R+1]−P[L]P[R+1] - P[L]P[R+1]−P[L]. We convert the window search into the algebraic identity P[L]=P[R+1]−targetP[L] = P[R+1] - \text{target}P[L]=P[R+1]−target. Storing prefix occurrences in a hash map solves the problem in single-pass O(N)O(N)O(N) time and O(N)O(N)O(N) space, completely immune to negative values.
The "Negative Numbers" Gotcha Matrix: When Two Pointers Works vs. When It Fails
Pointer ArchetypeAllows Negative Numbers?Why? Underlying Invariant MechanismCanonical Archetypes
Converging Squeeze (Opposite Direction)YESMonotonicity comes from sorted positional order (A[i] <= A[j]), not element signs. Advancing left increases the pair sum; advancing right decreases the pair sum.Two Sum II (LC 167), 3Sum (LC 15), 4Sum (LC 18), Container With Most Water (LC 11)
Symmetrical Extremes (Squares)YESLargest absolute values reside at outer boundaries. Squaring negative extremes yields the largest positive values at the far-left boundary.Squares of a Sorted Array (LC 977)
Partitioning & Compaction (Reader-Writer)YESInvariant evaluates an element-wise predicate (nums[i] < 0 or nums[i] == pivot), completely independent of cumulative arithmetic.Move Zeroes (LC 283), Sort Colors / Dutch Flag (LC 75), Rearrange Array Elements by Sign (LC 2149)
Sliding Window (Sum / Product)NO (Fatal Trap!)Adding negative numbers causes window sum to decrease; shrinking can cause sum to increase. Window monotonicity broken.Fails on LC 209 with negatives; MUST pivot to LC 560 (Prefix Sum) or LC 862 (Monotonic Deque)
Sliding Window (Frequency / Strings)YESWindow state tracks character frequencies/sets, which are purely combinatorial and unaffected by numerical arithmetic.Longest Substring (LC 3), Minimum Window Substring (LC 76), Character Replacement (LC 424)
Fast & Slow (Cycle / Midpoint)YESTraversal depends on node pointers / relative step speeds (1x vs 2x), completely independent of node payload values.Linked List Cycle (LC 141), Middle of Linked List (LC 876)

3.2 — The Two Pointers Decision Rule (Problem Classification Checklist)

When facing an unseen problem in an interview, use this systematic checklist to classify the exact sub-family before writing code:

Interactive Architecture Diagram
Synthesizing vector architecture diagram...

The diagram asks six questions in order, and the first "yes" names the family. Question 1 comes first because its problems are the easiest to spot: the array never changes, and each question names its range. Subarray Sum Equals K (LC 560) answers "no" there, because its ranges are not given. It answers "yes" at question 2. The later questions separate the pointer families by what they keep: a window, two ends of sorted data, a write position, or a fast and a slow pointer. When every answer is "no", the problem is not a two-pointers shape.

3.1 — Understanding Invariants: Monotonic Convergence & Window Validity

Why the invariant holds: pointers only ever move forward, so a position that has already been passed can never be revisited under a different pairing. Each pointer move is a proof step, not a guess — converging pointers eliminate a boundary because the discarded element is strictly dominated by the recorded answer, and a sliding window's left only advances when the window it would leave behind is provably no better than one still ahead.

Two Pointers collapses O(N²) pair comparisons into O(N) by exploiting monotonicity: converging pointers eliminate one boundary per step because outer pairs are strictly dominated, sliding windows advance right to accumulate state and contract left to restore constraints, and prefix sums transform non-monotonic windows into algebraic O(1) difference lookups.

1. Opposite-Direction Monotonic Squeeze (Converging Pointers)
Condition: While left < right, evaluate sum vs target (or wall height vs capacity)
Action: if sum < target: left += 1; elif sum > target: right -= 1

In a sorted array, if nums[left] + nums[right] < target, no element paired with nums[left] can reach target, eliminating left permanently.

2. Sliding Window Validity Restoration & Duplicate Skipping
Condition: While window violates constraint or nums[i] == nums[i-1] during multi-pointer scans
Action: evict left element state with left += 1 (or advance pointer past duplicate values)

Shrinking left restores the feasible window invariant in O(1) amortized steps; skipping duplicate values prevents generating identical combinations.

Common Questions
  • "Why does the sliding window pattern stop working the moment the array can contain negative numbers?"

    Because the whole technique is built on one assumption — that expanding the window can only make things "more," and shrinking it can only make things "less" — and negative numbers break that assumption in both directions at once.

    With all non-negative elements, adding nums[right] into the window sum can only increase it (or leave it unchanged at zero), and removing nums[left] can only decrease it. That monotonic relationship is exactly what lets you shrink left with confidence: if the window is currently valid, you know shrinking it further will only make it less valid, never suddenly valid again after briefly getting worse — so you never have to backtrack and re-check positions you've already moved past.

    Once negative numbers are allowed, expanding right can decrease the running sum (if the new element is negative), and shrinking left can increase it (if the element you're removing was negative). The sum stops moving in one predictable direction, so "the window is currently too big, shrink left" is no longer a sound conclusion — the answer you're looking for could be sitting on either side of your current window, in a way two pointers has no way to discover without checking every possibility again. That's why Subarray Sum Equals K (LC 560, which allows negatives) abandons the shrinking window entirely and switches to a prefix-sum hash map instead — trading the two-pointer's "never look back" guarantee for one that doesn't depend on monotonicity at all.

Complexity analysis

Time: O(n). The right index takes exactly n steps. The left index also takes at most n steps in total across the whole run — it only ever increases and can never exceed right. Total pointer movement is bounded by 2n, so the cost is linear even though the code contains a loop inside a loop. This is the amortised argument the interviewer wants to hear: "the inner while is not O(n) per outer step — it is O(n) for the entire algorithm."

Space: O(1) for scalar-state windows (4.1, 4.2, 4.4). O(k) for set/map-state windows (4.3), where k is the number of distinct values that can be in the window at once — O(min(n, alphabet)) for strings.

The alternative worth naming: with negative numbers allowed, sliding window is invalid, and the correct approach is prefix sums plus a monotonic deque or a hash map — O(n) time but O(n) space. Knowing when the pattern does not apply is a Staff-level signal.

Production Systems Deployment & Motivation

In streaming telemetry and rate limiters, monitoring rolling time windows — for example, finding the shortest consecutive burst of seconds that accounts for a traffic spike — uses two pointers moving in the same direction to maintain window state in one linear pass, instead of re-checking every candidate window from scratch.

Mastery Roadmap & Prerequisite Graph

💡 Don't worry if pointer movement rules feel subtle right now! Two Pointers is a universal state-elimination technique that turns O(N2)O(N^2)O(N2) pair searches into single-pass O(N)O(N)O(N) scans. As you progress through the Archetype Variants (from Container With Most Water and 3Sum to Dynamic Sliding Window and Trapping Rain Water), each hands-on challenge will cement this intuition until advancing left += 1 or shrinking right -= 1 based on monotonicity becomes second nature.

  • Applying two-pointer convergence to unsorted data: Two-pointer convergence strictly requires monotonicity (A[i]+A[j]A[i] + A[j]A[i]+A[j] increases when advancing iii, decreases when decrementing jjj). If the array is unsorted, pointer convergence skips potential pairs unpredictably. Always sort first or use a hash map.

  • Using < vs <= in loop condition: When finding pairs of distinct elements (index1 < index2), the loop condition must be while left < right:. Using left <= right allows an element to pair with itself when 2 * nums[left] == target.

  • 32-bit signed integer overflow on sum: In typed languages (Java, C++, C#), adding two large integers (nums[left] + nums[right]) can overflow 231−12^{31}-1231−1. Cast to 64-bit int (long long) before comparing.

Staff+ Engineering Perspective

Staff+ Systems Architecture & Hardware Considerations

ARCHITECTURAL SIGNALS & INTERVIEW TRIGGERS

Triggers: "sorted array pair search", "subarray / substring satisfying condition", "sliding window max/min", "in-place partitioned reordering". Transforms O(N2)O(N^2)O(N2) brute-force nested iterations into optimal O(N)O(N)O(N) linear scans by tracking coordinated index cursors across contiguous memory.

CONSTRAINTS & BOUNDS

Array lengths N≤107N \le 10^7N≤107. A quadratic O(N2)O(N^2)O(N2) double loop times out when N>104N > 10^4N>104. Two pointers ensure each element is visited at most a constant number of times (typically 2), guaranteeing strict O(N)O(N)O(N) runtime with O(1)O(1)O(1) auxiliary space.

FAANG PRODUCTION TRAPS & EDGE CASES

Index out-of-bounds on empty inputs; off-by-one window length calculations (right−left+1right - left + 1right−left+1 vs right−leftright - leftright−left); failing to update auxiliary window state (hash maps or frequency counts) symmetrically upon both expansion and contraction.

© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•