Two Pointers from Both Ends: Pair Sum, Palindrome and Container
24 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)Many problems ask about pairs. Checking every pair is slow. When the data is sorted, or the rule is symmetric, two pointers that start at the two ends and close in solve it in one pass.
Two pointers from both ends
Two pointers is a technique that keeps two positions in a sequence and moves them by a rule, so one pass can replace nested loops. Opposite-end pointers start at the two ends and close in.
A shop has prices sorted low to high and a budget. Is there a pair of prices that adds up to it? Put one pointer at each end and move them toward each other.
Example 1 · Brute force: try every pair
def pair_sum_slow(nums, target): for i in range(len(nums)): for j in range(i + 1, len(nums)): if nums[i] + nums[j] == target: return i, j return None
print(pair_sum_slow([3, 8, 12, 19, 25, 40], 31))(2, 3)
What this code does
- Two nested loops check every pair, so the time is O(n squared) and the space is O(1).
- It is correct but slow for long lists. Sorted order lets two pointers do the same job in O(n).
Example 2 · Two pointers from both ends
def pair_sum(nums, target): left, right = 0, len(nums) - 1 while left < right: total = nums[left] + nums[right] if total == target: return left, right if total < target: left += 1 else: right -= 1 return None
prices = [3, 8, 12, 19, 25, 40]print(pair_sum(prices, 31))print(pair_sum(prices, 5))(2, 3) None
What this code does
- Each step looks at one pair and moves one pointer, so the loop runs at most n times.
- Python lists are zero-based, so
len(nums) - 1is the last index.
Why moving a pointer is safe
If the sum is too big, the right value is too big for every partner still available, because the left value is already the smallest one left. So the right value can go. The same logic works when the sum is too small.
def count_pairs_below(nums, target): left, right = 0, len(nums) - 1 count = 0 while left < right: if nums[left] + nums[right] < target: count += right - left left += 1 else: right -= 1 return count
print(count_pairs_below([1, 3, 5, 7, 9], 10))Example 3 · Trace the pointers
prices = [3, 8, 12, 19, 25, 40]target = 31left, right = 0, len(prices) - 1while left < right: total = prices[left] + prices[right] print(left, right, total) if total == target: break if total < target: left += 1 else: right -= 10 5 43 0 4 28 1 4 33 1 3 27 2 3 31
What this code does
- Printing the pointers each round shows the search closing in.
- Five rounds are enough here, while brute force may need up to 15 pair checks.
When a pair is below the target, every pair from left to right - 1 is below it too, so right - left pairs are counted at once.
Valid palindrome
A palindrome reads the same from both ends. Compare the first and last letters, skip anything that is not a letter or digit, and move inward.
Example 4 · Palindrome with a built-in view
def is_palindrome_short(text): clean = [ch.lower() for ch in text if ch.isalnum()] return clean == clean[::-1]
print(is_palindrome_short("A man, a plan, a canal: Panama"))print(is_palindrome_short("Order 66 #"))True False
What this code does
- This is the shortest Python version: filter, then compare with the reversed copy.
- It builds two extra lists, so space is O(n). Interviews often ask for the two-pointer version with O(1) space.
Example 5 · Palindrome, two pointers
def is_palindrome(text): left, right = 0, len(text) - 1 while left < right: if not text[left].isalnum(): left += 1 elif not text[right].isalnum(): right -= 1 elif text[left].lower() != text[right].lower(): return False else: left += 1 right -= 1 return True
for message in ["Was it a car or a cat I saw?", "Python"]: print(message, is_palindrome(message))Was it a car or a cat I saw? True Python False
What this code does
- Spaces and punctuation are skipped by moving only one pointer.
- The loop stops early on the first mismatch.
Example 6 · Valid palindrome
def is_palindrome(text): left, right = 0, len(text) - 1 while left < right: if not text[left].isalnum(): left += 1 elif not text[right].isalnum(): right -= 1 elif text[left].lower() != text[right].lower(): return False else: left += 1 right -= 1 return True
print(is_palindrome("A man, a plan, a canal: Panama"))print(is_palindrome("Order 66 #"))True False
What this code does
isalnum()is true for letters and digits.- This uses no copy of the text, so extra space is O(1).
Container with most water
Vertical lines have heights. Two lines and the floor hold water of width times the shorter height. Start with the widest pair and move the shorter line inward, because only a taller line can help.
def max_water(heights): left, right = 0, len(heights) - 1 best = 0 while left < right: width = right - left best = max(best, width * min(heights[left], heights[right])) if heights[left] < heights[right]: left += 1 else: right -= 1 return best
print(max_water([1, 8, 6, 2, 5, 4, 8, 3, 7]))Example 7 · Best container
def max_water(heights): left, right = 0, len(heights) - 1 best = 0 while left < right: width = right - left best = max(best, width * min(heights[left], heights[right])) if heights[left] < heights[right]: left += 1 else: right -= 1 return best
print(max_water([4, 3, 2, 1, 4]))16
What this code does
- The widest pair, the two 4s, is 4 apart: 4 * 4 = 16.
- The heights are equal at the start, so the right pointer moves, and no later pair beats 16.
The width always shrinks, so keeping the shorter line can never improve the answer. One pass gives O(n) time and O(1) space.
Next step
Check your understanding
11 quick questions