Converging Two Pointers
The first two-pointer motion to learn is the simplest: put one pointer at the start of the array and
one at the end, then walk them toward each other. Each step you look at the pair they point to, make a
decision, and move one (or both) pointers inward. When they meet, you're done. That's a single pass over the
data - O(n) time - and it uses no extra memory beyond the two index variables.
Warm-up: reverse a list in place
The cleanest way to feel the motion is reversing a list. Swap the two ends, step inward, repeat until the pointers meet in the middle.
, = 0, - 1
, = ,
+= 1
-= 1
return
[5, 4, 3, 2, 1]
What just happened: lo starts at the front, hi at the back. Each loop swaps those two elements and
then steps both pointers one place toward the center. The loop condition lo < hi stops them the instant
they cross (or land on the same middle element, which needs no swap). Nothing is ever visited twice.
📝 Terminology. People say "two pointers," but in Python these are just integer indices. The word "pointer" is borrowed from lower-level languages; here it only means "a position we move through the array."
Palindrome check: same motion, different decision
A palindrome reads the same forward and backward - so compare the two ends, and if they ever disagree, it isn't one. Otherwise step inward and keep checking.
, = 0, - 1
return False
+= 1
-= 1
return True
True
False
What just happened: the pointers start at both ends and compare. "racecar" matches at every step until
they meet, so it returns True. "hello" fails immediately - h at the front doesn't match o at the
back - so it returns False without bothering to check the rest.
The real one: pair with a target sum (sorted array)
Here's where converging pointers earn their keep. Given a sorted array, find two elements that add up
to a target. The naive approach checks every pair with two nested loops - O(n²). But because the array is
sorted, the two ends tell you exactly which way to move:
- If the current pair sums to too little, the only way to get a bigger sum is to move the low pointer right (toward larger values).
- If it sums to too much, move the high pointer left (toward smaller values).
- If it's exactly the target, you're done.
, = 0, - 1
= +
return
+= 1
-= 1
return None
(2, 3)
None
What just happened: for target 9, the pointers start at 1 and 11 (sum 12, too big → hi moves
left), then 1 and 7 (sum 8, too small → lo moves right), then 3 and 7 (10, too big → hi
left), then 4 and 5 (9, match → return indices (2, 3)). Target 100 can never be reached; the
pointers cross and the function returns None. One pass, no nested loop.
💡 Key point. This shortcut only works because the array is sorted. Sorted order is what makes "too small → move right, too big → move left" a reliable decision. On an unsorted array this logic silently gives wrong answers - a trap we'll return to in Phase 3.
The same pair-sum, in other languages
The motion is identical in every language: an index at each end, a decision, one pointer steps inward. The typed languages just spell out the array and integer types, and each returns "not found" in its own idiom.
[[codegroup Pair With Target Sum]]
, = 0, - 1
= +
return
+= 1
-= 1
return None
static int[]
// returns {-1, -1} when no pair is found
std::pair<int, int>
func hasPairWithSum(nums []int, target int) (int, int, bool)
[[/codegroup]]
Notice the loop condition is lo < hi, not lo <= hi. Pairing an element with itself isn't a valid pair,
so the pointers must stay strictly apart. That single character is a real bug source - the next phase's
cousin (the sliding window) has its own version of the same off-by-one hazard.
[
{
"q": "In the pair-sum function, why does moving `lo` right when the sum is too small work?",
"choices": ["Because the array is sorted, so larger values are to the right", "Because it's faster to increment than decrement", "Because the target is always positive", "It doesn't matter which pointer moves"],
"answer": 0,
"explain": "Sorted order guarantees values increase to the right. A sum that's too small can only grow by moving the low pointer toward the larger values."
},
{
"q": "Why does the converging loop use `while lo < hi` instead of `while lo <= hi`?",
"choices": ["To run one extra iteration for safety", "So an element is never paired with itself, and the pointers stop when they meet", "Because `<=` is slower", "To handle empty arrays only"],
"answer": 1,
"explain": "A valid pair needs two distinct positions. `lo < hi` keeps them strictly apart and stops the loop the moment they meet or cross."
}
]
Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.
Check your understanding 2 questions
1. In the pair-sum function, why does moving `lo` right when the sum is too small work?
2. Why does the converging loop use `while lo < hi` instead of `while lo <= hi`?