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

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

13 Core Algorithmic Patterns & 225 Practice Problems

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

4-Stage Deliberate Practice Framework

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

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

Pricing, Access & Commercial Terms

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

Invariant-First Algorithmic Mastery

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

Two Pointers (Opposite Ends)

Grows into
Longest Palindromic SubstringStaircase SearchNext PermutationThree-Reversals Algorithm
Unlocks
Spiral MatrixMerge Sort Counting
The problem

Two Sum II - Input Array Is Sorted (LC 167)(opens leetcode.com)

You're given numbers, a 1-indexed array of integers already sorted in non-decreasing order, plus a target value. Locate two distinct positions index_1 < index_2 whose values add up to target.

Return those two positions, 1-indexed, as [index_1, index_2].

Exactly one valid pair is guaranteed to exist, and you can't reuse the same element twice.

Do this using only constant extra space.

Example 1
Input:numbers = [2,7,11,15], target = 9
Output:[1,2]
2 + 7 = 9, and those values sit at 1-indexed positions 1 and 2, so the answer is [1, 2].

Converging Pointers

left -> <- right (Opposite Boundary Contraction)
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).
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
Invariant: Every pair that uses an index outside [left, right] has been ruled out: it cannot sum to target.

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.

See it run

Converging Pointers, step by step

left 0 · right 5 · curr_sum -

left 0 · right 5 · curr_sum 17

left 0 · right 4 · curr_sum 17

left 0 · right 4 · curr_sum 13

left 1 · right 4 · curr_sum 13

left 1 · right 4 · curr_sum 14

left 2 · right 4 · curr_sum 14

left 2 · right 4 · curr_sum 16

left 2 · right 4 · curr_sum 16 · answer [3, 5]

Each box is one entry of numbers, with its index above it. The answer uses positions that start at 1.

  • left the pointer that only moves right
  • right the pointer that only moves left
  • out a value ruled out for good
  • match a pair whose sum equals the target

Start: left is at index 0 and right is at index 5. The array is sorted, so one comparison can rule out one end.

numbers[0] + numbers[5] = 2 + 15 = 17. That is above the target 16.

Too big, so numbers[5] = 15 is ruled out and right moves to 4.

numbers[0] + numbers[4] = 2 + 11 = 13. That is below the target 16.

Too small, so numbers[0] = 2 is ruled out and only left moves to 1. numbers[4] = 11 stays, because a larger left can still pair with it.

numbers[1] + numbers[4] = 3 + 11 = 14. That is below the target 16.

Too small, so numbers[1] = 3 is ruled out and left moves to 2.

numbers[2] + numbers[4] = 5 + 11 = 16. That equals the target 16.

Done: the pair at positions 3 and 5 sums to 16, so return [3, 5].

02left
13
25
38
411
515right
1 / 9
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.

Not this one if…
  • The answer is a contiguous stretch whose order must be kept, as in Fruit Into Baskets (LC 904), so you can't sort and there are no two ends to pair. See Longest Window
  • The target is the sum of a contiguous subarray, not of two chosen elements, and values can be negative, as in Subarray Sum Equals K (LC 560). See Prefix Sum Map

Theory

The question we build

What we know

numbers is sorted in ascending order. A pair's sum only goes up when its smaller index moves right, and only goes down when its larger index moves left.

What the problem gives us

The input is numbers and target. Return the 1-indexed positions of the two entries that sum to target. LeetCode guarantees exactly one such pair.

The question we build

We ask one question of the pair standing at the two ends: "is numbers[left] + numbers[right] equal to, below, or above target?" When the sum is too small, is there any remaining partner for numbers[left]? numbers[left] is already paired with numbers[right], the largest value not yet ruled out, and the sum is still too small.

Narrowing down to the template

Which element of the current pair can never be part of a better pair? When the sum is too small, numbers[left] paired with the largest remaining value still falls short, so no remaining numbers[right] can save it: drop left. When the sum is too big, the mirror argument drops right. So one comparison always rules out one end for good, and the two pointers only move toward each other. That is exactly Converging Pointers: squeeze [left, right] inward until they meet or the pair is found.

Step 1: Every pair

Check every pair (i, j) with i < j. Correct for any array, sorted or not. States the contract in one listing: a pair whose sum equals target.

Step 1: Every pairPython
1def two_sum_brute(numbers, target):
2 n = len(numbers)
3 for i in range(n):
4 for j in range(i + 1, n):
5 if numbers[i] + numbers[j] == target:
6 return [i + 1, j + 1] # 1-indexed, as LeetCode 167 asks
7 return []

Cost: O(N^2) time, O(1) extra space.

Step 2: Squeeze from both ends

On sorted input, one comparison at the two ends rules out one end for good. If the sum is too small, numbers[left] cannot pair with anything smaller than numbers[right], so no remaining partner reaches target: move left right. If the sum is too big, the mirror argument moves right left. Run on unsorted input, this reasoning breaks: a pointer move no longer raises or lowers the sum in one direction, so the squeeze can walk straight past the answer.

Step 2: Squeeze from both endsPython
1+New line: def twoSum(numbers: List[int], target: int) -> List[int]:
2+New line: left, right = 0, len(numbers) - 1 # left and right only ever move toward each other
3
4+New line: # 1. Squeeze Inward While Not Met:
5+New line: # numbers is sorted, so moving left right only raises curr_sum and moving right left only lowers it; one comparison always rules out the end that cannot reach target.
6+New line: while left < right:
7+New line: curr_sum = numbers[left] + numbers[right]
8+New line: if curr_sum == target:
9+New line: return [left + 1, right + 1] # LeetCode 167 wants 1-indexed positions
10+New line: elif curr_sum < target:
11+New line: left += 1 # numbers[left] cannot pair with any remaining elements; discard left
12+New line: else:
13+New line: right -= 1 # numbers[right] cannot pair with any remaining elements; discard right
14
15 return []

Cost: O(N) time, O(1) extra space.

Step 3: The named template

The loop does not change between problems. Only compare and what it returns change.

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

Cost: O(N * T), where T is the cost of one compare call.

BeforeAfterWhat changed
curr_sum = numbers[left] + numbers[right]order = compare(left, right)The sum and the comparison with target move inside compare.
if curr_sum == target: return [left + 1, right + 1]if order == 0: return (left, right)compare returns 0 for a match; the caller adds 1 for LeetCode's 1-indexed answer.
elif curr_sum < target: left += 1elif order < 0: left += 1Same move, named by the sign compare returns.
else: right -= 1else: right -= 1Identical.

Practice

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)
Theory Context•Two Pointers & Sliding Window
P0EasyMental model~4 min speedrun

Two Pointers (Opposite Ends)

Grows into
Longest Palindromic SubstringStaircase SearchNext PermutationThree-Reversals Algorithm
Unlocks
Spiral MatrixMerge Sort Counting
The problem

Two Sum II - Input Array Is Sorted (LC 167)(opens leetcode.com)

You're given numbers, a 1-indexed array of integers already sorted in non-decreasing order, plus a target value. Locate two distinct positions index_1 < index_2 whose values add up to target.

Return those two positions, 1-indexed, as [index_1, index_2].

Exactly one valid pair is guaranteed to exist, and you can't reuse the same element twice.

Do this using only constant extra space.

Example 1
Input:numbers = [2,7,11,15], target = 9
Output:[1,2]
2 + 7 = 9, and those values sit at 1-indexed positions 1 and 2, so the answer is [1, 2].

Converging Pointers

left -> <- right (Opposite Boundary Contraction)
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).
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
Invariant: Every pair that uses an index outside [left, right] has been ruled out: it cannot sum to target.

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.

See it run

Converging Pointers, step by step

left 0 · right 5 · curr_sum -

left 0 · right 5 · curr_sum 17

left 0 · right 4 · curr_sum 17

left 0 · right 4 · curr_sum 13

left 1 · right 4 · curr_sum 13

left 1 · right 4 · curr_sum 14

left 2 · right 4 · curr_sum 14

left 2 · right 4 · curr_sum 16

left 2 · right 4 · curr_sum 16 · answer [3, 5]

Each box is one entry of numbers, with its index above it. The answer uses positions that start at 1.

  • left the pointer that only moves right
  • right the pointer that only moves left
  • out a value ruled out for good
  • match a pair whose sum equals the target

Start: left is at index 0 and right is at index 5. The array is sorted, so one comparison can rule out one end.

numbers[0] + numbers[5] = 2 + 15 = 17. That is above the target 16.

Too big, so numbers[5] = 15 is ruled out and right moves to 4.

numbers[0] + numbers[4] = 2 + 11 = 13. That is below the target 16.

Too small, so numbers[0] = 2 is ruled out and only left moves to 1. numbers[4] = 11 stays, because a larger left can still pair with it.

numbers[1] + numbers[4] = 3 + 11 = 14. That is below the target 16.

Too small, so numbers[1] = 3 is ruled out and left moves to 2.

numbers[2] + numbers[4] = 5 + 11 = 16. That equals the target 16.

Done: the pair at positions 3 and 5 sums to 16, so return [3, 5].

02left
13
25
38
411
515right
1 / 9
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.

Not this one if…
  • The answer is a contiguous stretch whose order must be kept, as in Fruit Into Baskets (LC 904), so you can't sort and there are no two ends to pair. See Longest Window
  • The target is the sum of a contiguous subarray, not of two chosen elements, and values can be negative, as in Subarray Sum Equals K (LC 560). See Prefix Sum Map

Theory

The question we build

What we know

numbers is sorted in ascending order. A pair's sum only goes up when its smaller index moves right, and only goes down when its larger index moves left.

What the problem gives us

The input is numbers and target. Return the 1-indexed positions of the two entries that sum to target. LeetCode guarantees exactly one such pair.

The question we build

We ask one question of the pair standing at the two ends: "is numbers[left] + numbers[right] equal to, below, or above target?" When the sum is too small, is there any remaining partner for numbers[left]? numbers[left] is already paired with numbers[right], the largest value not yet ruled out, and the sum is still too small.

Narrowing down to the template

Which element of the current pair can never be part of a better pair? When the sum is too small, numbers[left] paired with the largest remaining value still falls short, so no remaining numbers[right] can save it: drop left. When the sum is too big, the mirror argument drops right. So one comparison always rules out one end for good, and the two pointers only move toward each other. That is exactly Converging Pointers: squeeze [left, right] inward until they meet or the pair is found.

Step 1: Every pair

Check every pair (i, j) with i < j. Correct for any array, sorted or not. States the contract in one listing: a pair whose sum equals target.

Step 1: Every pairPython
1def two_sum_brute(numbers, target):
2 n = len(numbers)
3 for i in range(n):
4 for j in range(i + 1, n):
5 if numbers[i] + numbers[j] == target:
6 return [i + 1, j + 1] # 1-indexed, as LeetCode 167 asks
7 return []

Cost: O(N^2) time, O(1) extra space.

Step 2: Squeeze from both ends

On sorted input, one comparison at the two ends rules out one end for good. If the sum is too small, numbers[left] cannot pair with anything smaller than numbers[right], so no remaining partner reaches target: move left right. If the sum is too big, the mirror argument moves right left. Run on unsorted input, this reasoning breaks: a pointer move no longer raises or lowers the sum in one direction, so the squeeze can walk straight past the answer.

Step 2: Squeeze from both endsPython
1+New line: def twoSum(numbers: List[int], target: int) -> List[int]:
2+New line: left, right = 0, len(numbers) - 1 # left and right only ever move toward each other
3
4+New line: # 1. Squeeze Inward While Not Met:
5+New line: # numbers is sorted, so moving left right only raises curr_sum and moving right left only lowers it; one comparison always rules out the end that cannot reach target.
6+New line: while left < right:
7+New line: curr_sum = numbers[left] + numbers[right]
8+New line: if curr_sum == target:
9+New line: return [left + 1, right + 1] # LeetCode 167 wants 1-indexed positions
10+New line: elif curr_sum < target:
11+New line: left += 1 # numbers[left] cannot pair with any remaining elements; discard left
12+New line: else:
13+New line: right -= 1 # numbers[right] cannot pair with any remaining elements; discard right
14
15 return []

Cost: O(N) time, O(1) extra space.

Step 3: The named template

The loop does not change between problems. Only compare and what it returns change.

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

Cost: O(N * T), where T is the cost of one compare call.

BeforeAfterWhat changed
curr_sum = numbers[left] + numbers[right]order = compare(left, right)The sum and the comparison with target move inside compare.
if curr_sum == target: return [left + 1, right + 1]if order == 0: return (left, right)compare returns 0 for a match; the caller adds 1 for LeetCode's 1-indexed answer.
elif curr_sum < target: left += 1elif order < 0: left += 1Same move, named by the sign compare returns.
else: right -= 1else: right -= 1Identical.

Practice

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)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•