Learn/DSA Patterns
DSA PatternsArrays & Two Pointerseasy16 min read

Two Pointers — Opposite Ends

Your very first algorithm pattern, explained from zero. We build the idea from a real-life story, prove it can never fail, and learn to spot it in five different problems.

#two-pointers#arrays#sorted-array#beginner#interview
Table of contents

Before we start

You have never studied a "pattern" before? Perfect. This is the right place to begin. By the end of this page you will be able to:

  • See the two-pointer idea in your head, like a short movie.
  • Explain out loud why it works — not just memorise code.
  • Recognise it inside five problems that look completely different on the surface.

Read slowly. Where you see a Pause & Think box, actually stop and think. That five seconds of effort is what makes the idea stick.


Picture this first (no code yet)

A real-life story

You are at a small shop. In your hand is a gift card worth exactly ₹12 — and the rule is strange: you must buy exactly two items, and together they must cost exactly ₹12. Not ₹11. Not ₹13.

The items are lined up on a shelf, already arranged cheapest on the left, most expensive on the right:

₹1 ₹3 ₹5 ₹7 ₹9 ₹11

You point your left finger at the cheapest item (₹1) and your right finger at the priciest (₹11). You add them up: 1 + 11 = ₹12. Done on the first try.

That felt easy. But how did you know to start from the two ends? And what would you have done if the total was wrong? Hold that thought — we are about to turn this exact instinct into an algorithm.


The actual problem

Here is the coding version of the shop:

You are given a sorted array of numbers and a target value. Find two numbers in the array that add up exactly to the target.

Example:

array  = [1, 3, 5, 7, 9, 11]
target = 12
answer = (1, 11)   because 1 + 11 = 12

Simple to state. Now let's solve it the obvious way first — and feel why the obvious way is painful.


First, the slow way (so you feel the pain)

The most natural idea: try every possible pair. Pick the first number, then check it against every other number. Then pick the second number, check it against all the rest. And so on.

def two_sum_slow(arr, target):
    n = len(arr)
    for i in range(n):
        for j in range(i + 1, n):
            if arr[i] + arr[j] == target:
                return (arr[i], arr[j])
    return None

This works. But count the effort. For an array of n numbers, the number of pairs you check is roughly:

n = 10        →  about 45 pairs
n = 1,000     →  about 500,000 pairs
n = 100,000   →  about 5,000,000,000 pairs   (five billion!)

At 100 million checks per second, that last case takes almost a minute. The trick we build next finishes the same case in less than a thousandth of a second. Same problem — a hundred-thousand times faster. Let's earn that speed.


The turning point

Go back to the shop. The shelf was sorted. That one word — sorted — is a gift most beginners walk straight past. Let's not.

Pause & think

You put your fingers on ₹1 (left) and ₹11 (right). Suppose their total was too big — say the target was only ₹8, and 1 + 11 = 12 is over.

Which finger should move, and in which direction? And here's the deeper question: is there any pair using the ₹11 that could still work?

Think it through with me. The ₹11 is the most expensive item on the whole shelf. If pairing it with the cheapest item (₹1) already overshoots ₹8, then pairing it with anything else — all of which cost ₹1 or more — will overshoot even harder.

So the ₹11 is useless. It can never be part of a winning pair. We can cross it off forever and slide our right finger left, to ₹9.

The mirror image is just as powerful:

  • If the total is too small, the item under the left finger is the cheapest available. Even paired with the most expensive item on the shelf it falls short — so it can never win. Cross it off and slide the left finger right.

Every single comparison lets us throw away one item for good. That is the whole secret.


The one idea to remember

The entire pattern in one sentence

Start with one finger at each end of a sorted list. Look at the total. Too big? Move the right finger left. Too small? Move the left finger right. Each move permanently removes the one number that cannot possibly help — so the answer, if it exists, is always still trapped between your two fingers.

That's it. That sentence is the algorithm. Everything below is just watching it happen and writing it down.


Watch it happen, frame by frame

Let's hunt for target = 10 in [1, 3, 5, 7, 9, 11]. Read one frame at a time. L is the left finger, R is the right finger.

Frame 1
index:   0    1    2    3    4    5
value: [ 1    3    5    7    9   11 ]
         L                        R
sum = 1 + 11 = 12   →   TOO BIG   →   move R left

The 11 is retired. It can never help. Slide the right finger in:

Frame 2
index:   0    1    2    3    4    5
value: [ 1    3    5    7    9   11 ]
         L                   R
sum = 1 + 9 = 10   →   EXACTLY RIGHT   →   answer is (1, 9)  ✓

Found it in two steps instead of checking every pair. Now watch the other branch — the "too small" case — with target = 16:

Frame 1:  L=1, R=11  →  sum 12  <  16  →  TOO SMALL  →  move L right
Frame 2:  L=3, R=11  →  sum 14  <  16  →  TOO SMALL  →  move L right
Frame 3:  L=5, R=11  →  sum 16  =  16  →  FOUND (5, 11)  ✓

And if the two fingers ever bump into each other without finding the target? Then you have tried every number that could possibly matter — there is no such pair, and you stop.

Pause & think

Cover the answer below. In [2, 4, 6, 8, 10] with target = 14, walk the fingers yourself. Which pair do they land on, and how many steps did it take?

Check your trace
L=2, R=10  →  sum 12  <  14  →  too small, move L
L=4, R=10  →  sum 14  =  14  →  FOUND (4, 10)

Two steps. If you got (4, 10), you understand the mechanism.


Now, the code — line by line

We had two "fingers." In code, a finger is just an index — a number that says which position we are pointing at. left starts at position 0 (the first item). right starts at the last position.

def two_sum_sorted(arr, target):
    left = 0                    # left finger: first index
    right = len(arr) - 1        # right finger: last index

    while left < right:         # keep going while the fingers haven't met
        total = arr[left] + arr[right]

        if total == target:     # exactly right — we're done
            return (arr[left], arr[right])
        elif total < target:    # too small — the left item is too weak
            left += 1           # move left finger one step right
        else:                   # too big — the right item is too strong
            right -= 1          # move right finger one step left

    return None                 # fingers met, nothing found

Read it against the story, line by line:

  • left = 0 and right = len(arr) - 1 — put a finger on each end.
  • while left < right: — keep working only while the fingers have a gap between them. The moment they meet, every useful number has been tried. (We use <, not <=, because a number can't pair with itself.)
  • total < targetleft += 1 — total too small, so retire the weakest number and move the left finger inward.
  • total > targetright -= 1 — total too big, so retire the strongest number and move the right finger inward.

That's the complete algorithm. Nine real lines.


Why does it never miss the answer?

This is the question that separates memorising from understanding. Let's make it airtight, in plain words.

Think of the two fingers as the walls of a shrinking box. At the start, the box [left … right] holds the entire array, so if an answer exists, it is obviously inside the box.

Now, the only way we ever move a finger is by retiring a number we have proven cannot be in any answer:

  • We move left right only after proving the left number is too small to ever reach the target — even with the largest partner available.
  • We move right left only after proving the right number is too big to ever reach the target — even with the smallest partner available.

So every move throws away a number that was guaranteed useless. We never throw away a number that could have been part of the answer. Therefore the answer — if it exists — is always still inside the box. The box only shrinks by removing junk. When we finally look at the right pair, it's still there waiting. It cannot be skipped.

Want the formal version? (optional — for the curious)

Claim. If a valid pair (i, j) with i < j exists, the loop returns it.

Invariant. At the top of every loop iteration, left ≤ i and j ≤ right.

  • Start: left = 0 ≤ i and j ≤ n−1 = right. ✓
  • When we do left += 1: we had arr[left] + arr[right] < target. Since the array is sorted, arr[right] ≥ arr[j], so arr[left] + arr[j] ≤ arr[left] + arr[right] < target = arr[i] + arr[j]. That forces arr[left] < arr[i], hence left < i, so after left += 1 we still have left ≤ i. ✓
  • When we do right -= 1: symmetric argument gives right > j, so right − 1 ≥ j. ✓

The invariant is never broken, so the pointers can never step over the answer. When they meet, either we already returned it or it never existed. ∎


Why is it so fast?

Look at the loop again. On every iteration, exactly one finger moves inward by one step. left only ever goes right; right only ever goes left. Together they can move at most n steps before they meet.

So the whole thing does about n units of work — written O(n), "grows in a straight line with the input." Compare that to the slow version's :

n = 100,000
slow  (n²)  →  ~5,000,000,000 steps
fast  (n)   →  ~100,000 steps

And we used only two integer variables of extra memory (left and right), no matter how huge the array — that's O(1) space, the best possible.

ApproachTimeExtra memory
Check every pairO(n²)O(1)
Two pointersO(n)O(1)

When should I reach for this? (the trigger list)

You won't be told "use two pointers." You have to recognise the smell of the problem. Reach for opposite-end two pointers when you notice:

  • The array (or string) is sorted, or you're allowed to sort it first.
  • You're asked to find a pair (or decide something about the two ends).
  • The brute-force answer is "check every pair," i.e. O(n²), and you want faster.
  • You're working with something that has two ends that can move toward each other — like a string you read from both sides.

See two or more of those together? Put a finger on each end and start moving inward.


The same trick in five disguises

Here is the real test of understanding: the same skeletontwo fingers at the ends, move inward by a rule — solves problems that look nothing alike. Notice the skeleton each time.

Disguise 1 — Reverse a string in place

To reverse "hello", swap the two ends, then move inward:

def reverse(s):
    left, right = 0, len(s) - 1
    while left < right:
        s[left], s[right] = s[right], s[left]   # swap the ends
        left += 1
        right -= 1

Same fingers, same "move inward until they meet." The rule is just "swap" instead of "compare a sum."

Disguise 2 — Is it a palindrome?

A palindrome reads the same forwards and backwards ("racecar"). So compare the two ends; if they ever disagree, it's not a palindrome:

def is_palindrome(s):
    left, right = 0, len(s) - 1
    while left < right:
        if s[left] != s[right]:
            return False
        left += 1
        right -= 1
    return True

Disguise 3 — Container with the most water (LeetCode 11)

Vertical lines of different heights; pick two to hold the most water. Water held = width × shorter of the two heights. Start as wide as possible and always move the shorter wall inward — the shorter wall is the bottleneck, so the taller one can't help.

def max_water(height):
    left, right = 0, len(height) - 1
    best = 0
    while left < right:
        area = (right - left) * min(height[left], height[right])
        best = max(best, area)
        if height[left] < height[right]:
            left += 1        # move the shorter (limiting) side
        else:
            right -= 1
    return best

Different question, same instinct: each step retires the wall that can't improve things.

Level up — 3Sum (three numbers that sum to zero)

Once the two-finger idea is comfortable, most "3-number" problems are just: fix one number, then run two pointers on the rest.

def three_sum(nums):
    nums.sort()
    result = []
    for i in range(len(nums) - 2):
        if i > 0 and nums[i] == nums[i - 1]:
            continue                      # skip duplicate anchors
        left, right = i + 1, len(nums) - 1
        need = -nums[i]                   # we want the other two to sum to this
        while left < right:
            total = nums[left] + nums[right]
            if total == need:
                result.append([nums[i], nums[left], nums[right]])
                while left < right and nums[left] == nums[left + 1]:
                    left += 1             # skip duplicate answers
                while left < right and nums[right] == nums[right - 1]:
                    right -= 1
                left += 1
                right -= 1
            elif total < need:
                left += 1
            else:
                right -= 1
    return result

The inner while loop is exactly the pattern from this page. You already know it.


Traps that catch beginners

Watch out for these

  • Forgetting the array must be sorted. The whole "retire the useless number" logic depends on it. If the input isn't sorted, sort it first (or use a different pattern).
  • Writing while left <= right instead of left < right. With <=, the two fingers can land on the same element and you'd "pair a number with itself." Use <.
  • Moving the wrong finger. Too big → move right down. Too small → move left up. Say it out loud until it's automatic.
  • Forgetting the empty case. An array with fewer than two elements has no pair — the while simply never runs, which is correct, but always keep it in mind.
BugFix
while left <= right:Use left < right — two distinct items
Moving left when the sum is too bigToo big → move right (down); too small → move left (up)
Assuming sorted inputCheck it; call arr.sort() if you're allowed

Say it like a pro (interview one-liner)

"The array is sorted, so I'll use two pointers from opposite ends. If the pair's sum is too big I move the right pointer down; if it's too small I move the left pointer up. Each move eliminates a number that can't be part of any answer, so it's O(n) time and O(1) space."

Deliver that in an interview and you've shown you understand the why, not just the what.


Remember this forever

Two Pointers — Opposite Ends

One finger at each end of a sorted list. Look at the total:

  • Too big → move the right finger left.
  • Too small → move the left finger right.
  • Just right → done.

Each move retires a number that can't possibly help, so the answer stays trapped between your fingers.


Cost: O(n) time, O(1) space · Trigger: sorted list + find a pair · Skeleton: while left < right: move inward by a rule


Check yourself

Answer these from memory before moving on. Retrieving the idea is what burns it in.

Why does the array have to be sorted?

Because our decision to retire a number relies on it being the smallest or largest still in play. If the sum is too big, we retire the right number knowing it's the biggest available — only true if the list is sorted. Without order, "too big" tells us nothing about which number to drop.

The sum is too small. Which pointer moves, and why?

The left pointer moves right. The left number is the smallest still available; even paired with the largest number it falls short, so it can never reach the target. Retire it and try the next-smallest.

Why is the loop condition `left < right` and not `left <= right`?

Because we need two different elements. If left == right, both fingers point at the same number, and a number can't pair with itself. < stops us exactly one step before that.

What makes it O(n) instead of O(n²)?

Each iteration moves exactly one finger inward by one step, and the fingers only travel toward each other. So there are at most n iterations total — a single pass — instead of checking all ~n²/2 pairs.


Practice problems

Do them in this order. The first three lock in the core idea; the last two are "boss levels" that reuse it.

ProblemDifficultyWhat to noticeLink
Two Sum II (sorted input)EasyThe exact pattern from this pageLC 167
Reverse StringEasySame fingers, rule = "swap"LC 344
Valid PalindromeEasyCompare ends, skip non-lettersLC 125
Container With Most WaterMediumAlways move the shorter wallLC 11
3SumMediumFix one number, two-point the restLC 15
Trapping Rain WaterHardTwo pointers + track each side's maxLC 42

When you can solve Two Sum II without looking, and explain out loud why each pointer moves, you have truly learned this pattern. Next up: Two Pointers — Same Direction, where both fingers move the same way to clean up an array in a single pass.