The two pointers technique solves array and string problems by keeping two positions (indexes) in the data and moving them according to simple rules, instead of checking every pair. It often turns an O(n²) solution into O(n) and needs almost no extra memory. There are three common shapes: pointers that start at opposite ends and move toward each other, pointers that move in the same direction at different speeds or roles, and one pointer per sequence when you walk two lists together.
Below is each shape with working Python code, real output and the reasoning that makes it correct.
In this guide
The short version
- Use two indexes (
loandhi, orreadandwrite) instead of a nested loop. - Opposite ends: works when the data is sorted (or symmetric), so each comparison lets you discard one side.
- Same direction:
readscans,writebuilds the answer in place. - Typical cost: O(n) time, O(1) extra space.
The idea
Take a classic: in a sorted list, find two numbers that add up to a target. The brute-force way tries every pair with two nested loops. With two pointers, you start at the two ends. If the sum is too small, the only way to increase it is to move the left pointer to a bigger number. If it is too big, move the right pointer to a smaller one. Every step rules out a whole group of pairs, so you never need to look at them. Here are both, counting steps for a list of 1,000 sorted numbers where the answer is the very last pair:
def two_sum_brute(nums, target):
steps = 0
for i in range(len(nums)):
for j in range(i + 1, len(nums)):
steps += 1
if nums[i] + nums[j] == target:
return (i, j), steps
return None, steps
def two_sum_pointers(nums, target):
lo, hi, steps = 0, len(nums) - 1, 0
while lo < hi:
steps += 1
s = nums[lo] + nums[hi]
if s == target:
return (lo, hi), steps
if s < target:
lo += 1
else:
hi -= 1
return None, steps
Output
((998, 999), 499500)
((998, 999), 999)
The pair was found after 499,500 steps by brute force and 999 with two pointers. On a million items the difference would be about 500 billion against a million. (If steps and Big O are new, read Big O Notation Explained With Python Examples.)
Pattern 1: pointers from both ends
Two sum on a sorted list
Watch the pointers move on a small example. We want two numbers that add to 10:
nums = [1, 3, 4, 6, 8, 11]
target = 10
lo, hi = 0, len(nums) - 1
while lo < hi:
s = nums[lo] + nums[hi]
print(f"lo={lo} hi={hi} sum={s}")
if s == target:
print("found", nums[lo], nums[hi])
break
elif s < target:
lo += 1
else:
hi -= 1
Output
lo=0 hi=5 sum=12
lo=0 hi=4 sum=9
lo=1 hi=4 sum=11
lo=1 hi=3 sum=9
lo=2 hi=3 sum=10
found 4 6
At each step the sum was either too large (move hi left) or too small (move lo right), until 4 + 6 hit the target.
Why it is correct: when the sum is too big, the number at hi cannot be part of any valid pair with any number at or after lo, because those are all at least as large. So hi can safely move. The same argument works for lo. This only works because the list is sorted.
Is it a palindrome?
A palindrome reads the same backwards. Compare the ends, then move inwards. Skip anything that is not a letter or digit, and ignore case:
def is_palindrome(s):
cleaned = [c.lower() for c in s if c.isalnum()]
lo, hi = 0, len(cleaned) - 1
while lo < hi:
if cleaned[lo] != cleaned[hi]:
return False
lo += 1
hi -= 1
return True
print(is_palindrome("A man, a plan, a canal: Panama"))
print(is_palindrome("hello"))
print(is_palindrome(""))
Output
True
False
True
Reverse in place
Swap the two ends and move inward. It uses no second list:
def reverse_in_place(items):
lo, hi = 0, len(items) - 1
while lo < hi:
items[lo], items[hi] = items[hi], items[lo]
lo += 1
hi -= 1
data = [1, 2, 3, 4, 5]
reverse_in_place(data)
print(data)
Output
[5, 4, 3, 2, 1]
Container with the most water
Given the heights of vertical lines, find the two that, with the x-axis, hold the most water. The area is width × the shorter height. Start with the widest container, then move the shorter side inward, because moving the taller one can never help (the width shrinks and the height is still capped by the shorter line):
def max_area(heights):
lo, hi, best = 0, len(heights) - 1, 0
while lo < hi:
width = hi - lo
best = max(best, width * min(heights[lo], heights[hi]))
if heights[lo] < heights[hi]:
lo += 1
else:
hi -= 1
return best
print(max_area([1, 8, 6, 2, 5, 4, 8, 3, 7]))
Output
49
Pattern 2: pointers moving the same way
Here a read pointer scans every item, while a write pointer marks where the next kept item should go. You modify the list in place without extra memory.
Remove duplicates from a sorted list
def remove_duplicates(nums):
if not nums:
return 0
write = 1
for read in range(1, len(nums)):
if nums[read] != nums[write - 1]:
nums[write] = nums[read]
write += 1
return write
data = [1, 1, 2, 2, 2, 3, 4, 4]
k = remove_duplicates(data)
print(k, data[:k])
Output
4 [1, 2, 3, 4]
write always points at the slot after the last unique item. When read finds a new value, it is copied to write. Only the first k items matter afterwards, which is why we print data[:k].
Move all zeros to the end
def move_zeros(nums):
write = 0
for read in range(len(nums)):
if nums[read] != 0:
nums[write], nums[read] = nums[read], nums[write]
write += 1
data = [0, 1, 0, 3, 12]
move_zeros(data)
print(data)
Output
[1, 3, 12, 0, 0]
Every non-zero item is swapped into the next write slot, so the order of the non-zero items is kept and the zeros drift to the end.
Fast and slow pointers
A variation: one pointer moves one step at a time and the other two. When the fast one reaches the end, the slow one is at the middle. On a plain list you would just use len(items) // 2, but the same idea is essential for linked lists, where you cannot jump to a position, and for detecting cycles (see Linked List in Python):
def middle(items):
slow = fast = 0
while fast + 1 < len(items):
slow += 1
fast += 2
return items[slow]
print(middle([1, 2, 3, 4, 5]))
print(middle([1, 2, 3, 4, 5, 6]))
Output
3
4
Pattern 3: one pointer per sequence
When you have two sorted lists, keep a pointer into each and always advance the one with the smaller item. This is the merge step of merge sort:
def merge(a, b):
i = j = 0
out = []
while i < len(a) and j < len(b):
if a[i] <= b[j]:
out.append(a[i])
i += 1
else:
out.append(b[j])
j += 1
out.extend(a[i:])
out.extend(b[j:])
return out
print(merge([1, 4, 7], [2, 3, 9, 10]))
Output
[1, 2, 3, 4, 7, 9, 10]
The same walk finds the items two sorted lists have in common. When the items are equal, take it and advance both. Otherwise advance the smaller side:
def intersect(a, b):
i = j = 0
out = []
while i < len(a) and j < len(b):
if a[i] == b[j]:
out.append(a[i])
i += 1
j += 1
elif a[i] < b[j]:
i += 1
else:
j += 1
return out
print(intersect([1, 2, 2, 3, 5], [2, 2, 3, 4]))
Output
[2, 2, 3]
How to recognise a two-pointer problem
| Clue in the problem | Likely pattern |
|---|---|
| Sorted array, find a pair or triple with a sum | Opposite ends |
| Palindromes, reversing, symmetric checks | Opposite ends |
| Maximise something using two boundaries | Opposite ends, move the weaker side |
| Remove or partition items in place | Read and write pointers |
| Middle of a linked list, cycle detection | Fast and slow |
| Merge, compare or intersect two sorted lists | One pointer per sequence |
| A contiguous range with a property (sum, distinct items) | Usually a sliding window, see the next guide |
The last row is a close relative: Sliding Window Technique in Python.
Common mistakes
Mistake 1: using opposite-ends pointers on unsorted data
The reasoning behind moving a pointer depends on the order. On unsorted data the same code silently gives the wrong answer. Here there is a pair (3 and 4), but the pointers never find it. After sorting, it works:
def two_sum_brute(nums, target):
steps = 0
for i in range(len(nums)):
for j in range(i + 1, len(nums)):
steps += 1
if nums[i] + nums[j] == target:
return (i, j), steps
return None, steps
def two_sum_pointers(nums, target):
lo, hi, steps = 0, len(nums) - 1, 0
while lo < hi:
steps += 1
s = nums[lo] + nums[hi]
if s == target:
return (lo, hi), steps
if s < target:
lo += 1
else:
hi -= 1
return None, steps
nums = [8, 1, 6, 3, 4]
print(two_sum_pointers(nums, 7))
print(two_sum_pointers(sorted(nums), 7))
Output
(None, 4)
((0, 3), 2)
Note that the indexes in the second answer refer to the sorted list. If you need original positions, sort (value, index) pairs, or use a dictionary approach (see Hash Tables in Python).
Mistake 2: off-by-one at the boundaries
Decide whether the loop condition is lo < hi or lo <= hi. For pair problems, the two pointers must not be the same item, so use <.
Mistake 3: forgetting to move a pointer
If some branch of the loop leaves both pointers unchanged, the loop never ends. Check that every path moves at least one pointer.
Mistake 4: mixing up in-place changes and returned lists
Functions like remove_duplicates modify the list and return a length. Read the original problem statement carefully to see what it expects you to return.
Try it yourself
Work out each answer first, then open the solution.
1. Use the two-pointer method on [2, 7, 11, 15] with target 9. What does it return, and how many steps does it take?
Show answer
Output
((0, 1), 3)The pair at positions 0 and 1 (2 and 7), after 3 steps: it moved hi left twice, then found the sum.
2. Given a sorted list that may contain negatives, return the squares in sorted order in O(n).
Show solution
def sorted_squares(nums):
lo, hi = 0, len(nums) - 1
out = [0] * len(nums)
for i in range(len(nums) - 1, -1, -1):
if abs(nums[lo]) > abs(nums[hi]):
out[i] = nums[lo] ** 2
lo += 1
else:
out[i] = nums[hi] ** 2
hi -= 1
return out
print(sorted_squares([-4, -1, 0, 3, 10]))
Output
[0, 1, 9, 16, 100]The biggest square is always at one of the two ends (a large negative or a large positive). Fill the answer from the back, taking whichever end is larger in absolute value.
3. Remove every occurrence of a value from a list in place, and return the new length.
Show solution
def remove_value(nums, val):
write = 0
for x in nums:
if x != val:
nums[write] = x
write += 1
return write
data = [3, 2, 2, 3, 4]
k = remove_value(data, 3)
print(k, data[:k])
Output
3 [2, 2, 4]The same read and write pattern as removing duplicates, with a different test.
4. What are the time and space complexity of the palindrome check above, ignoring the cleaning step?
Show answer
Time O(n), because each pointer moves at most across half the string. Space O(1), because only two indexes are stored.
Run these in our free Python compiler.
Frequently asked questions
What is the two pointers technique?
It is a way of solving problems on arrays and strings using two indexes that move through the data by simple rules, so you avoid checking every pair and usually get O(n) time instead of O(n²).
Does the array have to be sorted for two pointers?
For the opposite-ends pattern on problems like two sum, yes: sorting is what makes it safe to discard one side. Other patterns, such as read and write pointers or palindrome checks, do not need sorting.
What is the difference between two pointers and a sliding window?
A sliding window is a special case of two pointers, where both pointers move forward together and the range between them is a contiguous chunk you are tracking, such as a running sum or a set of distinct items.
What is the time complexity of two pointers?
Usually O(n), because each pointer moves in one direction and never revisits an item. Sorting first, if needed, adds O(n log n).
What are fast and slow pointers used for?
Finding the middle of a linked list, detecting cycles, and finding the start of a cycle. The fast pointer moves two steps for each step of the slow one.
Related reading
- Sliding Window Technique in Python – two pointers over a moving range.
- Big O Notation Explained With Python Examples – why O(n) beats O(n squared).
- Linked List in Python: Singly and Doubly – where fast and slow pointers shine.
- 10 Coding Interview Patterns in Python – two pointers among the key patterns.
Run this code in your browser
The free Upskly compiler runs Python with nothing to install. Paste the example, change it and see what happens.
Stuck on a traceback?
AI Assist works inside the Python notebook, so you can ask about an error or a concept without leaving the cell.
Test yourself
Timed questions on Arrays, Strings and Two Pointers, with an explanation for every answer.